在学习React源码之前,需要有一个大概的源码地图,知道React渲染的大致流程和架构,才能从上帝视角看React是怎么更新的。
架构
React的核心可以用ui=fn(state)来完成,也就是把state数据反映到ui上。更详细可以是
const state = reconcile(update)
const UI = commit(state);
上面的fn分为以下几个部分:
Scheduler(调度器):排序优先级,让优先级高的任务进入reconcileReconciler(协调器):找出哪些节点发生了变化,并打上不同的Flags(就版本叫Tag)Renderer(渲染器):将Reconciler中打好标签的节点渲染到视图上
概念
这的很多概念只是做初步了解,后面源码学习中会有更深入的理解
jsx
- jsx是js的语法扩展,React通过babel词法解析,将jsx转换成React.createElement,React-createElement函数返回vdom对象(内存中用来描述dom阶段的对象),所有jsx本质就是React.createElement的语法糖,用声明式的编写我们想要组件呈现什么样子的UI效果
jsx --(babel)--> React.createElement --(执行)--> vdom
Fiber双缓存
- Fiber对象上保存了包括节点的
属性、类型、dom等,Fiber通过child、sibling、return来形成Fiber树 - 同时还保存了更新状态时用于计算state的
updateQueue,updateQueue是一种链表结构,上面可能存在多个未计算的update,update也是一种数据结构,上面包含了更新的数据、优先级等,还有和副作用相关的信息 - 双缓存是指存在两个Fiber,
current Fiber树描述了当前呈现的dom树,workInProgress Fiber是正在更新的Fiber树,两棵树都是在内存中运行的,在workInProgress Fiber构建完成后会将它作为current Fiber应用到dom上 Fiber双缓存
function App() {
return <h1>
<p>111</p>
max
</h1>
}
Scheduler调度任务
需要实现一个异步、可中断的更新
// ReactFiberWorkLoop.old.js
function workLoopConcurrent() {
while(workInProgress !== null && !shouldYield()) { // shouldYield 判读是否暂停任务
workInProgress = performUnitOfWork(workInProgress)
}
}
在Scheduler中的每个任务的优先级是通过过期时间表示的。过期的在taskQueue中,没有过期的在timerQueue中,都是小顶堆
reconciler
发生在render阶段,render阶段分别为节点执行 beginWork和completeWork,或者计算state,对比节点的差异,为节点赋值相应的EffectFlags(标记dom节点的增删改)
简单概括就是:会创建或更新Fiber节点
- 在mount的时间根据jsx生成Fiber对象
- 在update的时候会根据最新的state形成的
jsx对象和current Fiber对象对比构建workInProgress Fiber树,这个对比的过程就是diff算法 - 会给这些Fiber打上
Flags标签
//ReactFiberFlags.js
export const Placement = /* */ 0b0000000000010; // 替换
export const Update = /* */ 0b0000000000100; // 更新
export const PlacementAndUpdate = /* */ 0b0000000000110; // 替换和更新
export const Deletion = /* */ 0b0000000001000; // 删除
遍历Fiber树类型dfs过程,“捕获”阶段发生在beginWork函数中,主要工作是创建Fiber节点,计算state和diff算法,“冒泡”阶段发生在completeWork中,该函数主要是做一些收尾工作,例如处理节点的props和形成一条EffectList链表,该链表是被标记了更新的节点形成的链表
- beginWork
- 捕获阶段
- 创建Fiber节点
- 计算state
- diff算法
- completeWork
- 处理props
- 生成EffectList链表 如下代码
function App() {
const [count, setCount] = useState(0);
return (
<>
<h1
onClick={() => {
setCount(() => count + 1);
}}
>
<p title={count}>{count}</p>
max
</h1>
</>
)
}
如果p和h1节点更新了则effectList如下,从rootFiber->h1->p,顺便说下
fiberRoot:是整个项目的根节点,只存在一个rootFiber:是应用的根节点,可能存在多个,例如多个ReactDOM.render(<App />, document.getElementById("root"));创建多个应用节点
- 这里的h1为什么也在effectList中呢?
因为h1标签接收的onClick是一个匿名函数,每次render的时候,接收的都是一个新的地址,所以有变化
- 为什么firstEffect指向的是p,而不是h1 因为行程effectList是在completeWork阶段做的,是从下往上走
Lane模型
处理优先级
- 2022.3.17:现在也不理解
renderer(commit阶段)
遍历EffectList,执行对应的dom操作或部分生命周期。不同平台Renderer不同,浏览器对应的就是react-dom
分别用三个函数处理EffectList上的几点:commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects