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,等组件完成初始化之后,再在 setTimeout 中将组件的宽度设置为传入的 props 上的值,这样就能看到动画了。
调用:
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 开始,第二个元素根本就没有动画。通过查看打印到控制台的信息,可发现在数据发生变化后,<Bar> 组件是没有销毁的,说明该组件在 props 更新时进行了复用,这是观察到的一点线索。
你可能会说,这里应该在每次渲染前,也就是 setTimeout 之前,先重置一下数据将宽度设置为 0,这样便能得到想要实现的效果:每次都从 0 开始动画。同时,为了看清过程,不防将 setTimeout 的时间暂时加大。
useEffect(() => { console.log("score 发生变化"); + setWidth(0); const timer = setTimeout(() => { setWidth(score); - }, 0); + }, 1000); return () => { clearTimeout(timer); }; }, [score]);