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 // 任务可供执行的剩余时间
}
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 Current与Fiber 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 的代码。