本文目录
React 使用虚拟 DOM 将计算好之后的更新发送到真实的 DOM 树上,减少了频繁操作真实 DOM 的时间消耗,但将成本转移到了 JavaScript 中,因为要计算新旧 DOM 树的差异嘛。所以这个计算差异的算法是否高效,就很关键了。React 中其计算差异的过程叫 Reconciliation,可理解成调和前后两次渲染的差异。 正式讨论前,先来看个问题。 问题假设我们有一个展示百分比的柱状条组件,其宽度由是传入的数值决定。并且它带动画,如果传入的值变化,那么柱状条应该由 0 动画到需要展示的宽度。 即想要实现的效果如下: 预期的百分比柱状条效果 所以我们写了如下的柱状条组件: function Bar({ score }) { const [width, setWidth] = useState(0); // 调试用 useEffect(() => { console.log(\"组件初始化完成\"); return () => { console.log(\"组件即将销毁\"); }; }, []); useEffect(() => { console.log(\"score 发生变化\"); const timer = setTimeout(() => { setWidth(score); }, 0); return () => { clearTimeout(timer); }; }, [score]); const style = { width: `${width}%` }; return ( <div className=\"bar-wrap\'\"> <div className=\"bar\" style={style}> {width} </div> </div> ); } 因为要实现动画,所以一开始我们并不将组件接收到的值应用到样式上,而是先将宽度设置为 0,等组件完成初始化之后,再在 调用: const data1 = [10, 20]; const data2 = [50, 20, 10]; function App() { const [data, setData] = useState(data1); return ( <div> <button onClick={() => { setData(prev => (prev === data1 ? data2 : data1)); }} > switch data </button> {data.map((score, index) => { return ( <div> <Bar score={score} /> </div> ); })} </div> ); } 实际得到的结果: 实际得到的结果 每次的动画不会从 0 开始,第二个元素根本就没有动画。通过查看打印到控制台的信息,可发现在数据发生变化后, 你可能会说,这里应该在每次渲染前,也就是 useEffect(() => { console.log(\"score 发生变化\"); + setWidth(0); const timer = setTimeout(() => { setWidth(score); - }, 0); + }, 1000); return () => { clearTimeout(timer); }; }, [score]); 每次动画前初始化 可以看到,并没有什么用。依然会有一个减小的动画。如果将 React 的 diff 机制对于树的差异检测,按照这个论文中描述的算法实现,其时间复杂度为 O(n3) 。而页面中 DOM 节点很容易上千,这样一次渲染需要 diff 的操作超过十亿,显然不可行。所以 React 在进行 diff 时作了两个假设前提:
基于这两点假设,在进行 diff 时可以少很多工作量,
这样假设之后,React 的 diff 算法做到了时间复杂度为 O(n)。 DOM 节点的 diff区分为节点类型变化与没变化两种情况, 对于前后再次渲染中,同一位置元素类型变化的情况,如前文所述,对该元素及其子节点整个更新。比如由 对于类型没变的情况则比较元素的属性,得出差异后只更新相应属性,比如 组件节点的 diff对于自己写的组件,类型变化时同 DOM 节点一样,将整个组件实例销毁,其中各状态将丢失,所有子节点也都销毁,这些组件的 如果该位置组件类型没变,说明只需要根据变化的 属性上面描述了节点对比后的处理。对于节点内子节点,递归遍历时,应用相同的逻辑。考察下面的示例代码: <ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
+ <li>third</li>
</ul>
React 在遍历 如果新插入的元素不在列表最后,而是在最前面或中间,事情就开始发生变化。 <ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
+ <li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
这时 React 简单地按位置来对比更的模式就变得不那么智能了。由前文所述,
这是 React 真实的流程,并不是我们一眼就能看出来的那个样子,只需要在列表开头插入那个新增的元素,将其他子元素保留即可。 所以,对于这样的列表类型,如果元素频繁变动,势必导致更新的效率会很低。问题的根本在于 React 不能识别前后两次渲染哪些元素其实是同一个,而是根据其在组件树中的位置来进行 diff 的。如果我们手动为元素指定一个唯一标识,这个标识在前后再次渲染时如果不变的话,这样就相当于告诉 React 它们是同一个元素,而不是按照其所在列表中的位置来进行 diff。 这便是元素身上的 再来看上面的示例, <ul> <li key=\"2015\">Duke</li> <li key=\"2016\">Villanova</li> </ul> <ul> <li key=\"2014\">Connecticut</li> <li key=\"2015\">Duke</li> <li key=\"2016\">Villanova</li> </ul> 通过读取元素身上的 所以你通过遍历方式生成一堆子节点时,React 会提示你需要为元素设置
默认情况下,如果没有显式指定 function Item({ name }) { const [score, setScore] = useState(); return ( <div> name:{name} <input type=\"text\" onChange={e => { setScore(e.target.value); }} /> score is: {score} </div> ); } function App() { const [persons, updatePersons] = useState([\"tom\", \"david\"]); return ( <div> <h3>set age for each person</h3> {persons.map((name, index) => { return <Item key={index} name={name} />; })} <div> <button onClick={() => { updatePersons(prev => [\"lily\", ...prev]); }} > add person </button> </div> </div> ); } 上面的示例遍历一个包含了姓名的数组,为每个人生成一行可输入分数的表单项。同时我们将每个生成项的 展示将 `key` 设置成索引导致组件内部状态不对的问题 可以看到,分数设置在列表中子组件中,当添加新的条目后,原来索引位置的组件复用之前的组件状态,因为该位置 修正 function App() { const [persons, updatePersons] = useState([\"tom\", \"david\"]); return ( <div> <h3>set age for each person</h3> {persons.map((name, index) => { - return <Item key={index} name={name} />; + return <Item key={name} name={name} />; })} <div> <button onClick={() => { updatePersons(prev => [\"lily\", ...prev]); }} > add person </button> </div> </div> ); } 这里假设每条数据其 修正 `key` 之后的正常表现 问题的解决回到文章开头的问题,就可以理解其表现了。 const data1 = [10, 20]; const data2 = [50, 20, 10]; 默认情况下,React 使用 index 作为
修正的方法可以为元素指定一个随机的 function App() { const [data, setData] = useState(data1); return ( <div> <button onClick={() => { setData(prev => (prev === data1 ? data2 : data1)); }} > switch data </button> {data.map((score, index) => { return ( - <div> + <div key={Math.random()}> <Bar score={score} /> </div> ); })} </div> ); } 修正后的百分比柱状条效果 将 总结虚拟 DOM 将操作浏览器 DOM 的成本一部分转嫁到了 JavaScript 中,即进行差异计算的成本。提高了渲染的效率,但某些情况下也会是一个坑。 需要注意的是,React 的差异算法高效性是在两个假设前提下进行的,
相关资源 |