React 采用的更新机制是一种批量更新的机制,最新的 hooks api 更是把这种机制推到了极致,批量更新的优势在于不需要处理更新细节,和直接操作 dom 元素相比,批量更新的效率更高,在大多数场景下,并不会产生性能瓶颈,尤其是对于移动端页面元素相对较少的情况。
但在 pc 端,越来越大的屏幕和越来越复杂的交互,促使我们开发更过的 react 组件来组成应用,如果你经常使用全局性的数据来渲染你的页面,那么可能很快就会遇到性能瓶颈。
主要的原因还是在于,虽然虚拟 dom 避免了昂贵的 dom 操作,但创建虚拟 dom 的 render 函数本身却无法被省略,例如下面这个场景
一个包含了多行的列表,每行是一个独立的组件,在 render 函数内我们 console.log('render')
当我们试图更新这个列表的时候,通常的做法就是给这个列表重新设置一个 props,类似这样的代码
<Table data={arr}>
通过修改 arr 中的数据来触发 Table 的更新和渲染,从而达到更新内部行组件的目的。
但正如我开篇中提到的,这种模式是一种批量更新模式,虽然变化的只有一行数据,但依然会触发所有行的render 即使 dom 没有变化。
可以看到 log 的 render 从 10次 变成了 20次,在真实的场景下,Table 行内的组件往往会非常复杂,有些场景需要同时渲染大量的数据,即便你使用了类似虚拟滚动,虚拟节点这样的技术,也很难做到将一次批量的更新控制在 16ms 以内, 如果超过了 32ms 那么就会出现明显的卡顿。
提到性能,你可能会想到官方推荐的方案 React.memo, 不过遗憾的是,React.memo 对于复杂类型的 props 也并不起作用,在示例中我试着用 React.memo 来避免批量更新下的 行组件渲染
const MemoCell = React.Memo(Cell)
如果要让 memo 起作用,就必须自行编写比较函数,但即使如此,在例如 useContext 包裹的场景下依然会触发 render,另外额外维护对比函数也是一个不必要的开销。
那是否有什么办法可以实现类似 document.getElementById 那样的精准更新呢?
你可能会想到使用 ref,但是额外维护内部元素的 ref 又成了另一个问题。另外通过 ref 直接操作 dom 会绕开 recat 的更新机制,这是一种 hack,非不得已而为之。
那如何在 react 的更新机制内寻求一种非批量更新的模式,精确触发某个组件的 render 呢?
通过实践我们想到了一个方案。
利用 Rx.js 为每个 react 组件实例建立双向的通信关系
在 Rdeco 内部, 我们利用 Rx.js 强大的订阅和推送能力,为每个 React 组件建立了一个代理组件。相同
name 的组件实例共享同一个代理组件,当 name 唯一的时候,代理组件也是唯一的,因此就可以利用代理组件实现精确的更新,而避免批量更新下不必要的 render, 要做到这一点,你只要将 key 作为代理组件 name 的一部分即可。
最终代码是这样的。
import "./styles.css";
import { createComponent, inject } from "rdeco";
import React, { useState } from "react";
const Cell = createComponent({
name: "cell",
state: {
num: null
},
exports: {
change(num) {
this.setter.num(num);
}
},
view: {
render() {
console.log("render");
return (
<div
style={{
margin: "12px",
padding: "12px",
border: "1px solid #000"
}}
>
1{this.props.children}
{this.state.num}
</div>
);
}
}
});
function createArray(num) {
const arr = [];
for (let index = 0; index < num; index++) {
arr.push(index);
}
return arr;
}
const MemoCell = React.memo(Cell);
const Tabel = createComponent({
name: "table",
view: {
render() {
return (
<div>
{this.props.data.map((d) => {
return (
<MemoCell membrane={{ name: `cell-${d}` }} key={d}>
{d}
</MemoCell>
);
})}
</div>
);
}
}
});
export default function App() {
const [arr, setArr] = useState(createArray(10));
const onClick = () => {
setArr(
arr.map((a) => {
return a;
})
);
};
const onAccurateClick = () => {
inject("cell-5").change(66);
};
return (
<div className="App">
<Tabel data={arr} />
<button onClick={onClick}> 粗放修改第 15 项的内容 </button>
<p />
<button onClick={onAccurateClick}> 精准修改第 15 项的内容 </button>
</div>
);
}
online dome: codesandbox.io/s/wispy-daw… 如果你有开启 React dev tools 可以通过 hightlight render 选项来对比点击两个按钮产生的更新效果。
如果你对这些内容感兴趣,👏 关注我们的项目 github.com/kinop112365…
也欢迎评论区交流😄