Skip to content

Latest commit

 

History

History
executable file
·
75 lines (44 loc) · 5.38 KB

[email protected]下的栈调和过程.md

File metadata and controls

executable file
·
75 lines (44 loc) · 5.38 KB

调和(Reconciliation)过程与Diff算法

调和:指的是将虚拟DOM映射到真实DOM的过程。

Virtual DOM 是一种编程概念,在这个概念里,UI以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过如ReactDOM等类库使之与“真实的”DOM同步,这一过程叫作协调(调和) -- React 官网

调和是“使一致”的过程,而Diff是“找不同”的过程,它只是“使一致”过程中的一个环节。

React源码中Reconciler(调和器)所做的工作是一系列的,包括组件的挂载、卸载、更新过程,其中更新过程涉及对Diff算法的调用。

Diff策略

要想找出两个树结构之间的不同,传统的方法是通过循环递归进行树节点的一一比对,这个过程的算法复杂度是O(n^3)。

具体来说,若一个页面中有100个节点,100^3算下来就有10万次操作了,这还只是一次Diff的开销。然而OJ中相对理想的时间复杂度是O(1)或者O(n)。React团队从设计层面总结了两条规律为O(n^3) -> O(n)确立了前提:

  • 若两个组件属于同一类型,那么他们将拥有相同的DOM树形结构;
  • 处于同一层级的一组子节点,可通过设置key作为唯一标识,从而维持各个节点在不同渲染过程中的稳定性;
  • DOM节点之间的跨层级操作不多,同层级操作是主流

Diff逻辑

  1. Diff算法性能突破关键在于“分层对比”;
  2. 类型一致的节点才有继续Diff的必要;
  3. key属性的设置,可以帮我们尽可能重用同一层级内的节点

1. 分层对比

React的Diff过程直接放弃了跨层级的节点比较,它只针对相同层级的节点作比较,如下图。这样一来,只需要从上到下的一次遍历,就可以完成对整棵树的对比。

需要注意的是:虽然栈调和将传统的树对比算法优化为了分层对比,但整个算法仍是以递归的形式运转,分层递归也是递归

image-20220331140359107

如果真的发生了跨层级操作(比如将以B节点为根节点的子树从A节点下面移动到C节点下,如下图),在这种情况下React并不能够判断出“移动”这个行为,它只能机械的认为移出子树那一层的组件消失了,对应子树需要被销毁,而移入子树的那一层新增了一个组件,需要重新为其创建一棵子树。

image-20220331140935903

销毁 + 创建的代价是昂贵的,因此React官方也建议开发者尽量不要做跨层级操作,尽量保持DOM结构的稳定性

2. 类型的一致性决定递归的必要性

只有同类型的组件,才有进一步对比的必要。若参与Diff的两个组件类型不同,那么直接放弃比较,原地替换掉旧的节点,如下图。只有确认组件类型相同后,React才会在保留组件对应DOM树(子树)的基础上,尝试更深层Diff。

image-20220331141849147

3. key属性

key 是用来帮助 React 识别哪些内容被更改、添加或者删除。key 需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因为如果 key 值发生了变更,React 则会触发 UI 的重渲染。这是一个非常有用的特性。

试图解决同层级下节点重用问题,假如有如下图情况:

image-20220331142200292

A组件在保持类型和其它属性不变的情况下,在两个子节点B和D之间插入新节点C,按照已知的Diff规则,两棵树之间的Diff过程应该是这样的:

  1. 首先对比位于第一层的节点,发现两棵树的节点类型一致,于是进一步Diff;
  2. 开始对比位于第二层的节点,第一个接受比较的就是B位置,也一致;
  3. 第二个接受比较的是D位置,对比D和C,前后类型不一致,直接删掉D,重建C;
  4. 第三个接受比较的是E节点位置,E和D类型不一致,也删掉E,重建D;
  5. 最后比较的是第二棵树的E位置,在第一棵树里没有,直接新建E节点

无法重用B、D、E三个节点,这时候就要借助key属性:

const todoItems = todos.map(todo => (
  <li key={todo.id}>{todo.text}</li>
))

如果忘记写key,React并不会报错,当我们没有设置key时,Diff过程确实如上所述。但只要加上key,key可以帮助React记住某个节点,从而在后续的更新中实现对这个节点的追踪。比如上图的两棵树,我们给每个节点增加一个key,如下图:

image-20220331143309981

这个key就充当了每个节点的唯一标识,有了这个标识后,当C被插入到B和D之间,React并不会再认为C、D、E这三个节点都需要重建--它会通过识别唯一标识,意识到E、E并没有发生改变(D的仍旧是1,E的仍旧是2),而只是被调整了顺序。这也就能轻松地重用“追踪”到的节点,将D、E移动到新的位置上,并完成对C的插入,使同层级下的元素操作成本降低。