Virtual DOM、React Diff和React Fiber

615 阅读4分钟

Virtual DOM

virtual dom 是什么?

 const Component() =>
     <div>
         ...
         <div>...</div>
         <ul>
             <li>...</li>
             <li>...</li>
             <li>...</li>
         </ul>
     </div>

一个 react 的返回 jsx 对象的函数, react 内置将其使用React.createElement转化成一个virtual tree。 然后会根据 virtualDom 生成一个或多个 fiber,通过 diff 去更新比较,将更新结果返回给 virtualDom 再绘制页面。

react diff

diff 算法是什么

传统 diff

传统 diff 算法是对两个树进行比较,区分出不同节点(最小编辑距离算法)。 字符串的最小编辑距离算法使用动态规划,时间复杂度为 O(n^2)。该算法的状态转移方程为:

 // 初始化a.length === 0 || b.length === 0的情况
 dp[i][j] = Math.max(i, j)
 // 状态转移方程,表示删除,插入,替换
 if (a[i] === b[j]) {
 	dp[i][j] = dp[i - 1][j - 1]
 } else {
 	dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
 }

树的 diff 也使用了该方法,个人理解因为树不是一条直线走到底,需要判断走左子树还是右子树,所以需要有策略函数映射每个子树到根节点的路径,然后综合考虑是走哪个子树路径。所以时间复杂度升高为O(n^3)

react diff

传统的 diff 算法由于时间复杂度太高,而我们在选择算法时应该平衡选择,在一个前端页面性能可接受范围内的最优算法就可。所以 react 对 diff 算法进行了减法,将大部分求不同节点最小距离的操作改为删除替换整个子树,将性能抛给浏览器引擎渲染 dom,从而减少 diff 的时间复杂度。

  • 不在同一层级 不在同一层级的 Fiber 直接删除其节点及其子节点

  • 在同一层级

    • fiber.type 不同 type 不同的两个 fiber,也是删除原 fiber,再新增
    • fiber.type 相同
      • 无 key (顺序比较) 根据组件列表数组下标判断,超出原 fiber 下标的新增
      • 有 key 根据 key 值判断,同样 key 的 fiber 进行比较

react diff的伪代码

 class FiberNode {
 	// 如果是原生节点,如div,就是string类型的'div',组件则是组件类
 	type = 'div' | SomeComponent
 	children: Array[FiberNode]
 	key: string
 }

 function domDiff(vDomOld, vDomNext) {
 	if (!vDomOld) {
 		insertUpdate(idx, vDomNext)
 		return
 	}

 	if (vDomOld.type === vDomNext.type) {
 		if (vDomOld.key === vDomNext.key) {
 			attributeUpdate(vDomOld, vDomNext)
 			if (vDomOld.children && vDomNext.children) {
 				domDiffArray(vDomOld.children, vDomNext.children)
 				return
 			}
 		}
 	}
 	replaceUpdate(vDomOld, vDomNext)
 }

 function domDiffArray(o_arr, n_arr) {
 	const o_map = addKeys(o_arr)
 	const n_map = addKeys(n_arr)

 	// 找出需要删除的节点
 	const deletes = o_arr.filter((item, i) => {
 		return item.key ? !n_map.has(item.key) : i >= n_arr.length
 	})

 	for (let x of deletes) {
 		replaceUpdate(x, null)
 	}

 	// 其他是需要更新的节点
 	// 剩下的要么是n_arr中有的key,要么是长度小于等于n_arr
 	for (let i = 0; i < o_arr.length; i++) {
 		if (o_arr[i].key) {
 			if (n_map.has(o_arr[i].key)) {
 				domDiff(o_arr[i], n_map.get(o_arr[i].key))
 			}
 		} else {
 			if (i < n_arr.length) {
 				domDiff(o_arr[i], n_arr[i])
 			}
 		}
 	}

 	// 需要新增的节点
 	for (let i = 0; i < n_arr.length; i++) {
 		if (n_arr[i].key) {
 			if (!o_map.has(n_arr[i].key)) {
 				insertUpdate(i, n_arr[i])
 			}
 		} else {
 			if (i >= o_arr.length) {
 				insertUpdate(i, n_arr[i])
 			}
 		}
 	}
 }

 function insertUpdate(idx, vDomNext) {}

 function replaceUpdate(vDomOld, vDomNext) {}

 function attributeUpdate(vDomOld, vDomNext) {}

 function addKeys(arr) {
 	const map = new Map()
 	for (let x of arr) {
 		map.put(x.key, x)
 	}
 	return map
 }

fiber

fiber 是什么?

  • 是 React Element 数据的镜像,一个执行单元
  • 是 diff 工作的流程,一种流程控制,协程?

fiber 的前提

浏览器的主动让出机制(合作式调度),chrome 的实现requestIdleCallback

window.requestIdleCallback(
  callback: (dealine: IdleDeadline) => void,
  option?: {timeout: number}
)

interface IdleDealine {
  didTimeout: boolean // 表示任务执行是否超过约定时间
  timeRemaining(): DOMHighResTimeStamp // 任务可供执行的剩余时间
}

参考:你应该知道的 requestIdleCallback

fiber 的结构

一个链表结构

function FiberNode(...) {
    // 工作类型(fiber类型) => 属性更新,usestate等hooks更新,或者function Component, ComponentClass更新,tag都是不相同的
    this.tag = tag;

    // reactElement.key => diff
    this.key = key;

    // reactElement.type
    this.elementType = null;

    // Fiber
    // 执行完当前工作返回的fiber
    this.return = null;

    // 当前fiber的最左侧子fiber
    this.child = null;

    // 当前fiber下一个同级节点 => 兄弟节点
    this.sibling = null;

    ...
}

fiber 的 workInProgress

Fiber Current => Fiber InProgress 每个 fiber 都会生成一个对应的 workInProgress,当发生变更时,原 fiber 不变,新构造一个WorkInProgress Fiber,原 fiber 与其互相指向。 例如:当组件的state或者props发生变化时,就会生成新的新的WorkInProgress Fiber。 dom diff 就是在Fiber CurrentFiber InProgress之间比较,找出 updates,然后在WIP树上每个需要变更的节点上打上标签,最后一次性提交更新。

这种操作技巧叫做Copy On Write,当有变更的时候,先复制原有对象,修改完成后再替换原有对象。

fiber 更新的两个阶段

  • 协调阶段(计算阶段)=> 可中断

    • 计算 Work In Progress Fiber
    • 进行 DOM DIFF,计算 dom 的更新
    • 该阶段的生命周期
      • constructor
      • componentWillMount 废弃
      • componentWillReceiveProps 废弃
      • static getDerivedStateFromProps
      • shouldComponentUpdate
      • componentWillUpdate 废弃
      • render
  • 提交阶段 => 不可中断

    • 一次性提交所有更新
    • 该阶段的生命周期
      • getSnapshotBeforeUpdate()
      • componentDidMount
      • componentDidUpdate
      • componentWillUnmount

参考: 这可能是最通俗的 React Fiber(时间分片) 打开方式

fiber 为何可以并发、diff 为何可以中断

一个 reactElement 可以生成多个 fibers,甚至一个 fiber 可以拆分成多个 fiber,当某一个 fiber 执行 diff 完成后,就检查时间是否充足,充足就执行下一个 fiber,否则保存现场(链表结构保证了可以从上次未完成的节点继续 diff),将线程控制权交给浏览器,等待下一个任务调度时间。

所以细粒度的 fiber 保证了可以并发,而链表结构保证了可以中断 diff 操作。而中断操作有可能会导致一些协调阶段的生命周期如componentWillMount、componentWillUpdate多次执行,所以不建议在协调阶段执行有 effects 的代码。