1. React的架构
React16 架构可以分为三层:
- Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
Scheduler(调度器)
以浏览器是否有剩余时间作为任务中断的标准,当浏览器有剩余时间时通知我们。
由于以下因素,React放弃使用浏览器API requestIdleCallback:
- 浏览器兼容性
- 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换 tab 后,之前 tab 注册的
requestIdleCallback触发的频率会变得很低
Reconciler(协调器)
- 更新工作从递归变成了可以中断的循环过程。每次循环都会调用
shouldYield判断当前是否有剩余时间。 - 当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟 DOM 打上代表增/删/更新的标记
- 整个Scheduler与Reconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。
Renderer(渲染器)
Renderer根据Reconciler为虚拟 DOM 打的标记,同步执行对应的 DOM 操作。
2. Fiber架构
Fiber本质上是虚拟DOM。Fiber节点可以保存对应的DOM节点;相应的,Fiber节点构成的Fiber树就对应DOM树。
Fiber的含义
- 从架构来说,
React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。 - 从数据结构来说,每个
Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件)、对应的DOM节点等信息。 - 从工作单元来说,每个
Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新)。
注释:
Fiber数据结构里,type属性,值为字符串即原生标签、值为函数且type.prototype.isReactComponent即类组件、值为函数且!type.prototype.isReactComponent即函数组件、值为undefined即文本。
多个Fiber节点是如何连接形成树?
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
3. Fiber架构的工作原理
什么是“双缓存?
在内存中构建并直接替换的技术叫做[双缓存]
双缓存 Fiber 树
在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。
初次渲染的构建流程
- 首次执行
ReactDOM.render会创建fiberRootNode和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是<App/>所在组件树的根节点。- 由于是首屏渲染,页面中还没有挂载任何
DOM,所以fiberRootNode.current指向的rootFiber没有任何子Fiber节点(即current Fiber树为空)。
- 由于是首屏渲染,页面中还没有挂载任何
- render阶段: 接下来进入render阶段,根据组件返回的
JSX在内存中依次创建 Fiber 节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。- 在构建
workInProgress Fiber树时会尝试复用current Fiber树中已有的 Fiber 节点内的属性,在首屏渲染时,只有rootFiber存在对应的current fiber(即rootFiber.alternate)。
- 在构建
- commit阶段: 已构建完的
workInProgress Fiber树在commit阶段渲染到页面。
更新阶段的构建流程
- render阶段: 会开启一次新的
render阶段并构建一棵新的workInProgress Fiber 树。workInProgress fiber的创建可以复用current Fiber树对应的节点数据。
- commit阶段:
workInProgress Fiber 树在render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为current Fiber 树。
JSX 与 Fiber 节点
区别:
JSX是一种描述当前组件内容的数据结构,他不包含组件schedule、reconcile、render所需的相关信息。
不包括在JSX中的信息:
- 组件在更新中的
优先级 - 组件的
state - 组件被打上的用于Renderer的
标记
初次渲染和更新阶段:
在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。
在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。
4. render阶段
render阶段的工作可以分为“递”阶段和“归”阶段。其中“递”阶段会执行beginWork,“归”阶段会执行completeWork。
“递”阶段
beginWork的工作可以分为两部分:
mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child。
“归”阶段
由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,已经有一个构建好的离屏DOM树。
-
当
mount时,主要逻辑包括三个:- 为
Fiber节点生成对应的DOM节点 - 将子孙
DOM节点插入刚生成的DOM节点中 - 与
update逻辑中的updateHostComponent类似的处理props的过程
- 为
-
当
update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:onClick、onChange等回调函数的注册- 处理
style prop - 处理
DANGEROUSLY_SET_INNER_HTML prop - 处理
children prop
commit阶段
effectList
在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。
effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。
所以,在commit阶段只需要遍历effectList就能执行所有effect了。
commit 阶段的工作流程
commit阶段的主要工作(即Renderer的工作流程)分为三部分:
- before mutation 阶段(执行
DOM操作前) - mutation 阶段(执行
DOM操作) - layout 阶段(执行
DOM操作后)
1. before mutation 阶段
在before mutation阶段,会遍历effectList,依次执行:
- 处理
DOM节点渲染/删除后的autoFocus、blur逻辑 - 调用
getSnapshotBeforeUpdate生命周期钩子 - 调度
useEffect
2. mutation 阶段
mutation阶段会遍历effectList,“根据effectTag调用不同的处理函数处理Fiber。
3. layout 阶段
该阶段触发的生命周期钩子和hook可以直接访问到已经改变后的DOM,即该阶段是可以参与DOM layout的阶段。
layout阶段会遍历effectList,“根据effectTag调用不同的处理函数处理Fiber并更新ref。