React原理二
这里是对React 执行中某个阶段具体解释,大概工作机制请看React原理一
首先触发更新
创建update对象
const update = {lan, //优先级标签suspenseConfig,tag:updateState, // 更新类型(0:UpdateState,1: ReplaceState, 2: ForceUpdate )payload: null,action: null, //更新内容 例如 setState(1)next: null, //指向下一个Update 形成链表
}
updateQueue
创建update对象后,然后把它插入到对应fiber 节点的updateQueue 的链表里;
const queue = {baseState: fiber.memoizedState, //本次更新浅垓fiber 节点的statefirstBaseUpdate: null, // 本次更新开始时,尚未处理的更新链表的头节点lastBaseUpdate: null, // 本次更新开始时,尚未处理的更新链表的尾节点shared: {pending: null, // 触发更新时产生的环状链表,永远指向最新的update}
}
- firstBaseUpdate和lastBaseUpdate 表示在上次渲染过程中因为任务优先级不足等原因未能处理,被遗留到这次的更新
- 在render 阶段 会将queue.shared.pending的环状列表剪断将其变成一个单向链表,保证执行的顺序
- 在render阶段, update 更新对象中的 lan 优先级并不会影响执行的顺序,会严格安装链表的执行顺序(前提是lan 在此次 pendingLanes中)
Scheduler任务调度详解
结合以上触发更新后,React 会把这次更新向上冒泡到FiberRoot,标记到FiberRootNode.pendingLanes,然后通过调用 scheduleUpdateOnfiber 和 ensuerRootisScheduled函数将整个FiberRoot包装成一个回调函数以及它的优先级交给Scheduler, Scheduler并不知道这个函数是干嘛的,它只是管理这个函数何时执行,然后,Scheduler 会在它认为合适的时机调用这个函数。
- scheduleUpdateOnfiber:这个函数的作用是 将当前更新调度到正确的 Fiber 上,并标记该 Fiber 需要重新渲染。它会把更新与优先级信息记录到调度器中
- ensuerRootisScheduled: 确保 根节点的调度是活跃的,其职责是为一个根节点安排一次渲染任务,如果已经有任务又来一个任务并且新的任务优先级更高,他会取消低优先级任务然后根据新的最高优先级重新安排一个任务
- 高优先级任务可以打断低优先级任务示例
-
请求数据后更新页面数据,这个更新数据的操作就相当于上面说的往Scheduler放了一个渲染任务;
-
然后在协调执行任务的阶段之前你突然点击按钮或者输入框输入操作;
-
这个时候同样会给FiberRoot打上lan优先级的标记,通过比较任务优先级,Scheduler取消低优先级任务(这里的取消不代表中断正在执行的任务),Scheduler放入高优先级任务,在每个任务单元(即每个 Fiber 节点)处理完之后会进行检查,这里贴上源码片段
while (nextUnitOfWork !== null && !shouldYield()) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); }
- performUnitOfWork(nextUnitOfWork)相当于一次任务单元的执行(或者是fiber节点)
- shouldYield的判断逻辑是
- 检查时间片:判断当前时间片(通常为 5ms)是否已经用完
- 检查是否有更高优先级的任务
-
如果存在高优先级任务,当前任务让出线程去执行高优先级任务,待高优先级任务执行完再进行调度渲染(相当于执行完后会检查FiberRoot.pendingLanes,如果存在lans,会重新发起一次渲染更新流程)
-
Reat diff算法
-
三个核心策略
- 只对同级元素进行比较
- 两个不同类型的元素产生不同的树
- 使用唯一的 key来帮助React识别哪些元素是稳定的、被移动的,而不是被销毁或新建的。
-
diff的两轮遍历(这里简单用cfiber 指代 current fiber; child指代对比的新的React元素树, WIP fiber 指代 WorkInProgress fiber)
-
第一轮遍历
- 遍历cfiber 的子节点链表和child数组
- 依次比较key type
- 如果相同:复用旧的fiber, 继续下一个
- 如果不同 :跳出第一轮遍历,意味着这个节点可能移动、替换、删除
-
第二轮遍历
- 当child还有数据,current fiber已经没有数据了,WIP fiber直接新建对应child剩下的节点数据
- 当cfiber 还有数据,child没有数据,cfiber 剩下的节点打上删除的标记
- 当cfiber 和 child 都存在数据的时候
- React 会将剩下的fiber节点数据包装成一个以key 为键的 Map,然后继续遍历child
- 对于每个新的child,用它的key 去 Map里查找:
- 如果存在,说明节点是存在的,但可能移动了位置,WIP fiber会复用这个旧fiber 节点,并从Map 中移除,React 会记录这个节点得到位置变化(index),最后统一操作
- 如果不存在:说明是新增的节点,进行WIP fiber创建节点的操
- 遍历结束,还留在Map 中的旧fiber节点都会打上删除标记
-
-
React 通过 lastPlacedIndex 变量来追踪最后一个被复用的旧 Fiber 节点在旧列表中的索引。注意是记录上一次的最大索引,如果当前复用的节点在旧列表中的索引(oldIndex)小于 lastPlacedIndex,说明这个节点在旧列表中的位置比上一个复用的节点更靠前,但在新列表中却更靠后了——这意味着它需要向右移动。否则,说明它不需要移动。
- 这里举例说明
- 旧列表(Old List)[A, B, C, D] (对应的旧索引oldIndex: A=0, B=1, C=2, D=3)
- 新列表(New List): [B,D, A, C] (对应的新索引: B=0, D=1, A=2, C=3)
- lastPlacedIndex初始值是0,首先遍历到 B, oldIndex 1 大于 lastPlacedIndex 0 所以B不用移动;lastPlacedIndex = 1;
- 遍历到D,3 > 1 所以,D不用动,这时候 lastPlacedIndex = 3
- 遍历到A,0 < 3 所以,A需要往右移动,打上标记,commit 阶段就 需要A 节点需要插入D节点后面
- 遍历到C,2 < 3 所以,C 也往右移动,打上标记,commit阶段 C节点插入A节点后面
这个过程看着感觉是多此一举,感觉直接遍历后利用Map 数据再按child中的顺序构建WIP
fiber不就行了,实际上这里只是打上移动的标记,对于以上例子相当于B、D节点在commit 阶段不需要动它们的
DOM,只需要移动A、C的DOM就好了