你应该知道的React源码
前端研发模式的发展
石器时代 ===> JSP/JQuery ===> React/Vue
这背后的本质是一个对“DOM操作”的演化过程
虚拟DOM
本质是JS对象和DOM之间的映射,通过JS描述DOM的结构
React中的虚拟DOM
挂载阶段
React通过JSX的描述,构建出虚拟DOM树(React.createElement),然后通过ReactDOM.render实现虚拟DOM到DOM的映射(触发渲染)
更新阶段
页面的变化会先作用于虚拟DOM,虚拟DOM将在JS借助Diff算法对比出需要更新的部分,然后将这些更新作用于DOM
为什么需要虚拟DOM?
标准回答:“DOM操作慢,性能低下,导致大量的回流与重绘,JS性能更好”
虚拟 DOM 真正解决的:
- 数据驱动视图,研发效能的提升,同时带来不错的性能
- 增量更新伴随的批量更新
- 跨平台,编码与视图层解耦,一次编写多端运行
Reconciler
关于Reconciliation
React的官方定义:
即将虚拟 DOM 映射为DOM的过程
Reconciler ≠ Diff,Diff仅作为Reconciler的代表性环节,其中还包括挂载、卸载、更新(包含Diff)等过程
Diff的优化策略
前面也提到了,谈到reconciler,Diff是绕不开的话题
计算机科学中对比两棵树的常见方法是递归的对节点做一一对比,但带来的时间复杂度是爆炸的O(n^3)💥
O(n^3)即两棵树嵌套循环寻找不同的节点:O(n^2),寻找到不同的节点后,需要再遍历得到最小的转换消耗O(n)得来
React对Diff的优化三要素:
- 分层求异
减少时间复杂度的关键
不建议做跨层级比较
- 类型一致才继续进行 Diff
一刀切手段,减少冗余的递归
- 通过key区分节点
优化同层级节点位置交换时,第二点优化带来的弊端
关于batchUpdate
解决多次setState,导致的生命周期钩子多次执行带来的低下性能
setState的工作流
enqueueSetState
- 将state放入组件状态队列中
- 调用enqueueUpdate
enqueueUpdate
决定是否进行批量更新
这里不得不先谈谈Transaction机制,这是React中的一种黑盒机制,可用来封装任意方法,并绑定函数执行前后需要运行的方法。
方法:
const handleClick = ()=>{
this.setState({
count:this.state.count+1
})
}
经过transaction机制:
const handleClick = ()=>{
isBatchingUpdates = true
this.setState({
count:this.state.count+1
})
isBatchingUpdate = false
}
所以,这也是为什么当setTimeOut中执行setState会呈现出同步的效果
StackReconciler的局限性到底在哪
背景: JavaScript 线程 与浏览器渲染线程互斥(串行) (JS可以操作DOM,若二者同时工作,渲染结果难以预测)
核心问题:stackReconciler对主线程的超时占用(同步递归过程,无法打断)
根本原因:stackReconciler机制的Diff算法,是树的深度优先遍历过程
Fiber如何破局
Fiber 的“增量渲染”,分解渲染任务,分散到多个帧内
⚠️注:Fiber ≠ 异步,而是一种兼容同步、异步渲染的数据结构
核心
可中断、可恢复、优先级
核心架构:
stack
fiber
泛生命周期:
stack
fiber
ReactDOM.render首次渲染链路
首先看看首次渲染的入口ReactDOM.render渲染链路
调用栈:
关键信息:
performSyncWorkOnRoot开启了render阶段
commitRoot开启了真实DOM的渲染过程即commit阶段
初始化阶段
基本实体创建(如下图)
调度updateContainer
- 获取当前Fiber节点的lane
- 结合lane创建当前Fiber节点的update对象并入队
- 调度当前节点
render阶段
调度performSyncWorkOnRoot开启同步任务
beginWork + completeWork是一个模拟递归的过程,同步模式下与stack Reconciler相似
workInProgress Node
即rootFiber节点的副本
workLoopSync
通过while循环判断workInProgress是否为空,继而执行performUnitOfWork(workInProgress)
performUnitOfWork
- 触发beginWork的调用,创建新Fiber节点,放入workInProgress tree中
- 若新Fiber节点不为空,则更新workInProgress
workInProgress tree
render
commit
beginWork(递)
beginWork 创建Fiber节点
调用reconcileChildren,首次渲染时生成当前节点的子节点
reconcileChildren
effectTag为记录副作用类型的标记
首次渲染直接生成子Fiber节点,更新阶段为子Fibe节点打上effectTag
completeWork(归)
completeWork 负责将Fiber映射为DOM节点
performUnitOfWork函数中,若next为空(即当前为叶子节点),则调用completeWork
关键动作:
- 创建DOM节点
- 将DOM节点插入DOM树中
- 为DOM节点设置属性
- 创建好的DOM节点赋值给workInProgress.stateNode
更新阶段则会将当前节点的副作用链(effectList)插入到父节点对于的副作用链中
effectList
带effectTag属性的Fiber节点链表
维护firstEffect和lastEffect属性,effectList最终仅挂载在App的父节点(rootFiber)上
commit阶段
绝对同步的过程
在performSyncWorkOnRoot中被调用
- before mutation
- mutation 该阶段渲染DOM
- layout 将fiberRoot.current指向workInProgress tree
双缓存机制
current树与workInProgress树之间的转换
下面个例子:
首次渲染
render阶段完成后,commit执行前
commit执行后
更新
commit执行前
commit执行后
update对象
大致流程如下:
首次渲染
updateContainer
- 创建update
- enqueueUpdate将update入队
- 开始调度(scheduleUpdateOnFiber)
更新
dispatchAciton
- 创建update对象
- 将update入队fiber.updateQueue
- 开始调度当前fiber
小结
总结一下几个关键函数的作用,以便于对于react有一个宏观上的认知
workLoopSync
作用:循环调用performUnitOfWork,并赋值给workInProgress,一直到workInProgress为空
performUnitOfWork
作用:
递阶段,调用beginWork
符合条件时进入归阶段,调用competeWork。
递归交替,直到回到root节点
beginWork
作用:主要调用reconcileChildren方法。
渲染阶段生成子Fiber节点。
更新阶段为子Fiber节点添加effectTag属性
completeWork
作用:
渲染阶段将FIber映射为真实的DOM节点。即创建DOM节点并为其添加属性后赋值给workInProgress.stateNode。
更新阶段则是不断生成effectList链表,最终挂载在rootFiber上。
Scheduler
时间切片是如何实现的?
function workLoopConcurrent(){
while(workInProgress !== null && !shouldYield()){
performUnitOfWork(workInProgress)
}
}
shouldYield
由Scheduler提供,判断当前时间切片是否到期
时间切片的长度:根据浏览器性能计算得出
优先级调度如何实现
- startTime:任务开始时间
- expirationTime:越小优先级越高
- timerQueue:一个以startTime为值的小顶堆(startTime>now,即待执行任务)
- taskQueue:一个以expirationTime为值的小顶堆(startTime<now,即已过期任务)
取出timerQueue的堆顶,当任务到期后加入taskQueue中,触发对flushWork的调用
flushWork中将调用workLoop,workLoop将会逐一执行taskQueue中的任务,直到调度过程被暂停(时间切片到期)或任务清空。
转载自:https://juejin.cn/post/7117583318228402183