React

代码仓库gitee: https://gitee.com/hey-u/react-dom-diff
虚拟dom,轻量级javascript对象

  1. {
  2. tag: "div",
  3. props: {
  4. class: "box"
  5. },
  6. children: [{
  7. tag: "span",
  8. props: {},
  9. children: ["hello world!"]
  10. }]
  11. }

image.png

比较方式

平级比较,不会跨级比较
同层比较,复用旧结点,换位置
patch补丁
image.png
🌹DOM-DIFF - 图3

差异计算

先序深度优先遍历

简化diff策略(处理场景不完整)

image.png

  1. import { createElement, render, renderDom } from "./element";
  2. import diff from './diff'
  3. import patch from "./patch";
  4. let vertualDom1 = createElement('ul', { class: 'list' }, [
  5. createElement('li', { class: 'item' }, ['a']),
  6. createElement('li', { class: 'item' }, ['c']),
  7. createElement('li', { class: 'item' }, ['b']),
  8. ])
  9. let vertualDom2 = createElement('ul', { class: 'list' }, [
  10. createElement('li', { class: 'item' }, ['a', '1', '2']),
  11. createElement('li', { class: 'item' }, ['c']),
  12. createElement('li', { class: 'item' }, ['b', 'test']),
  13. createElement('div', { class: 'item' }, ['new']),
  14. // createElement('div', { class: 'item' }, ['new']),
  15. // createElement('div', { class: 'item' }, ['new']),
  16. ])
  17. // 平级节点位置互换,会导致重新渲染
  18. // 新增节点不会被触发
  19. // 虚拟dom转化真实dom
  20. let el = render(vertualDom1)
  21. // 渲染页面
  22. renderDom(el, window.root)
  23. // dom diff :两个虚拟dom的对比 -> 两个对象的对比
  24. // 根据两个虚拟对象创建出补丁,描述改变的内容,将这个补丁用来更新dom
  25. // 差异比较 -> 补丁
  26. // jsx语法就是createElement,转成AST, setState 触发 diff
  27. let patches = diff(vertualDom1, vertualDom2)
  28. console.log("patches=", patches);
  29. // 给元素打补丁,重新更新视图
  30. patch(el, patches)

核心处理流程:

  1. 转化虚拟dom: createElement()方法
  2. 虚拟dom转化成真实dom: render()方法
  3. setState更新之后,内部先生成一份新的虚拟dom,跟旧的虚拟dom进行比对diff,实质就是两个对象的比较
    1. 制定diff策略
    2. treeWalker() 方法递归树,先序深度优先遍历,层级比较,得到补丁包:patches
  4. 给旧的真实DOM打补丁: patch()方法

在React中,Virtual Dom和diff的结合大大提高了渲染效率。diff算法由最初的O(n^3)复杂度变为了现在的O(n)
https://juejin.cn/post/6844904165026562056

diff的几种策略

  • Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
  • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  • 对于同一层级的一组子节点,它们可以通过唯一id进行区分。

基于以上三个策略,React分别对tree diff、component diff以及element diff进行了算法优化。

tree diff

基于第一个策略,react只会对同一层次的节点进行比较,如下图中,只对颜色相同框内的DOM节点进行比较,当发现节点不存在时,就会删除整个节点及其子节点,不会再进行比较,这样就只需要遍历一次,就能完成对整个DOM树的比较
🌹DOM-DIFF - 图5
如果出现了DOM节点的跨层级的移动操作,React diff会怎样呢?
React只会简单的考虑同层级节点的位置变换,对于不同层级的节点,只有创建和删除操作。如果A节点整个被移动到D节点下,根节点发现子节点中A不见了,就会销毁A;然后D 发现自己多了一个子节点,就会创建新的子节点(包含其中属于自己的子节点)作为其子节点。react diff就会按照这样的次序执行:craete a -> create b -> create c -> delete a。这种跨层级的节点移动,并不会出现移动的情况,而是会有创建、删除这些操作。这种操作会影响到React的性能,所以React官方也并不建议进行这种操作。在开发组件时,保持稳定的dom结构会有助于性能的提升
🌹DOM-DIFF - 图6

component diff

React对于组件间的比较采取的策略也是简洁高效

  • 如果是同一类型的组件,按照原策略继续比较虚拟dom树
  • 如果不是,则将该组件判断为dirty component,从而替换整个组件下的所有子节点
  • 对于同一类型的组件,有可能其Virtual DOM没有任何变化,如果能够确切的知道这点那可以节省大量的diff运算的时间,因此React允许用户通过shouldComponentUpdate()判断该组件是否需要进行diff

举个例子来说,当下图中componentD改变为componentG时,即使这两个compoent结构很相似,但是react会判断D和G并不是同类型组件,也就不会比较二者的结构了,而是直接删除了d,重新创建G及其子节点,这个时候会影响react的性能
🌹DOM-DIFF - 图7

element diff

当节点处于同一层级时,React diff提供了三种节点操作:插入、移动和删除

  • 插入:新的component类型不在老集合里 -> 全新的节点,需要对新节点执行插入操作
  • 移动:在老集合里有新component类型,且element是可更新的类型,generateComponentChildren已调用receiveComponent,这种情况下prevChild=nextChild,就需要做移动操作,可以复用以前的dom节点
  • 删除:老的component类型,在新集合中也有,但对应的element不同则不能直接复用和更新,需要执行删除操作,或者老component不在新集合里,也需要执行删除操作

举个🌰:看下图中,老集合中包含节点A、B、C、D,更新后的集合中包含节点B、A、D、C,此时进行新老集合差异化对比,发现B不等于A,则创建并插入了B至新集合,删除老集合A,以此类推。。。这样做很繁琐,因为这些都是相同的节点,只是位置发生了变化,针对这一现象,react提出了优化策略,允许开发者对同一层级的同组子节点,添加唯一key进行区分【注意:这里就体现了key的作用~
🌹DOM-DIFF - 图8
上面我们叙述的情况是没key的情况,如果有key了(假设key为上图中每个节点对应的名字,例如节点A,对应key为A),它就会这么对比:
diff会通过key发现新老集合中的节点是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时React给出的diff结果为:B、D不做任何操作,A、C进行移动操作。
首先对新集合的节点进行循环遍历,通过唯一key可以判断新老集合中是否存在相同的节点,如果存在,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与lastIndex进行比较,如果节点当前的位置下面我们来说一下大致的过程(下面所说的_mountIndex是当前节点所在位置,lastIndex为参考位置)
1、从新集合中取到B,判断老集合中存在相同的节点,通过对比节点位置判读是否进行了移动操作,B在老集合中的位置为_mountIndex=1,此时lastIndex = 0,不满足child._mountIndex < lastIndex的条件,因此不对B进行移动操作;此时更新lastIndex = Math.max(prevChild._mountIndex, lastIndex),[其中prevChild.mountIndex表示B在老集合中的位置]。lastIndex=1,并将B的位置更新为新集合中的位置prevChild._mountIndex=nextIndex,此时新集合中B._mountIndex=0,nextIndex++进入下一个节点的判断
2、从新集合中取得A,判断老集合中存在相同节点A,通过对比节点位置判断是否进行进行移动操作,A在老集合中的位置A._mountIndex=0,此时lastIndex=1,满足child.mountIndex3、从新集合中取到D,判断老集合中是否存在相同节点,通过对比位置判断是否进行移动操作,D在老集合中的位置D._mountIndex=3,此时lastIndex=1,不满足child._mountIndex4、C节点同理
如果新老集合中,不只是存在位置互换的关系呢?React diff又如何进行操作?(下图中各节点的key为对应节点名称)
🌹DOM-DIFF - 图9
1、从新集合中取到B,判断老集合中是否存在相同的节点,找到B在老集合中的位置B._mountIndex=1,此时lastIndex=0,因此不对B进行移动操作;更新lastIndex=1,并将B的位置更新为新集合中的位置B._mountIndex=0,nextIndex++进入下一个节点的判断
2、取到新集合中E节点,由于老集合中不存在相同的节点,则创建新节点E;更新lastIndex=1,并将E的位置更新为新集合中的位置,nextIndex++进入下个节点的判断。
3、取到新集合中C节点,老集合中存在相同节点,C._mountIndex=2,lastIndex=1,此时C._mountIndex > lastIndex,因此不对C进行移动操作;更新lastIndex=2,并将C的位置更新为新集合中的位置,nextIndex++进入下一个节点的判断。
4、取到新集合中A节点,老集合中存在相同的节点,A._mountIndex=0,lastIndex=2,此时A._mountIndex < lastIndex,对A进行移动操作;更新lastIndex=2,并将A的位置更新为新集合中的位置,nextIndex++进入下一个节点的判断。
5、当完成新集合中的所有节点的diff时,最后还需要对老集合进行循环遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点D,因此删除节点D,到此diff全部完成。

总结

基于diff这样的策略,所以react建议我们用添加唯一key的方式来进行优化,这里面可以牵扯出来一个问题:
如果用index作为key会有什么问题呢?
index作为key,如果我们删除了一个节点,那么数组的后一项可能会前移,这个时候移动的节点和删除的节点就是相同的key了,在react中,如果key相同,就会视为相同的组件,但这两个组件并不是相同的,这就会出现一些我们不想看到的问题~所以key的值我们要考虑好再确定哦~