第一章
1.1 初识前端框架
1.1.1 如何描述UI
JSX是 JavaScript 语法扩展,可以让你在 JavaScript 文件中书写类似 HTML 的标签。 多年以来,web 开发者都是将网页内容存放在 HTML 中,样式放在 CSS 中,而逻辑则放在 JavaScript 中 —— 通常是在不同的文件中。但随着 Web 的交互性越来越强,逻辑越来越决定页面中的内容。JavaScript 控制着 HTML 的内容!这也是为什么 在 React 中,渲染逻辑和标签共同存在于同一个地方——组件。每个 React 组件都是一个 JavaScript 函数,它会返回一些标签,React 会将这些标签渲染到浏览器上。React 组件使用一种被称为 JSX 的语法扩展来描述这些标签
JSX规则:
1)只能返回一个根元素
JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。
2)标签必须闭合
3)使用驼峰式命名法给大部分属性命名
JSX 最终会被转化为 JavaScript,而 JSX 中的属性也会变成 JavaScript 对象中的键值对
4)在 JSX 中通过大括号使用 JavaScript
5)内联 style 属性 使用驼峰命名法编写
典型的JSX代码如下
// App.js import React from 'react'; function App() { return ( <div> <h1>Hello, World!</h1> <button onClick={() => alert('Clicked!')}> Click me! </button> </div> ); } export default App;
经过babel编译后
// App.js (after Babel) import React from 'react'; const _jsxWrapper = () => { return React.createElement(React.Fragment, null, React.createElement("div", null, React.createElement("h1", null, "Hello, World!"), React.createElement("button", { onClick: () => alert('Clicked!') }, "Click me!"))); }; function App() { return _jsxWrapper(); } export default App;
编译后在React框架运行时生成的数据结构
// 编译后的数据结构示例 { type: 'div', key: null, ref: null, props: { children: [ { type: 'h1', key: null, ref: null, props: { children: 'Hello, World!' }, }, { type: 'button', key: null, ref: null, props: { onClick: [Function], children: 'Click me!', }, }, ], }, ... }
与JSX功能类似的还有模板语言:PHP、JSP、EJS、DTL
1.1.2 如何阻止UI与逻辑
1)逻辑中的自变量变化,导致UI变化 2)逻辑中的自变量变化,导致“无副作用的因变量”变化,导致UI变化 3)逻辑中的自变量变化,导致“有副作用的因变量”变化,导致UI变化
1.1.3 如何在组件之间传输数据
数据在组件之间的传输方式是,组件的自变量或因变量通过UI传递给另一个组件,作为其自变量。为了区分不同方式产生的自变量,在前端框架中,“组件内部定义的自变量”通常被称为state(状态),“其他组件传递而来的自变量”被称为props(属性)。
跨层级传递方式,使用store,在React中使用store步骤: 1)在A的逻辑中调用React.createContext创建context 2)在A的UI中定义context.provider 3)在C的逻辑中通过useContext消费A传递过来的自变量
1.1.4 前端框架的分类依据
UI=f(state)的工作原理概括为两步: 1)根据自变量(state)变化计算出UI变化 2)根据UI变化执行具体的宿主环境API 以前端框架中“与自变量建立对应关系的抽象层级”作为分类依据:
- 应用级框架
- 组件级框架
- 元素级框架
1.1.5 React中的自变量与因变量
根据‘自变量与因变量’理论对常见hook分类:
- useState:定义组件内部的自变量
- useReducer:useState本质是‘内置reducer的useReducer’。如果将useReducer看作“借鉴Redux理念的useState”,也相当于组件内部定义的自变量。
- useContext:React中的store实现,用于跨层级将其它组件的自变量传递到当前组件
- useMemo:采用“缓存的方式”定义组件内部“无副作用因变量”。
- useCallback:采用‘缓存的方式’定义组件内部‘无副作用的因变量’,缓存的值为函数形式
- useEffect:定义组件内部‘有副作用的因变量’
- useRef:用于在组件多次render之间缓存一个‘引用类型的值’
1.2 前端框架使用的技术
1.2.1 编程:细粒度更新
第二章 React理念
2.1 问题与解决思路
React官网提到:
我们认为React是用JavaScript构建快速响应的大型应用程序的首选方式。
实现的关键在于‘快速响应’,我们日常使用App、浏览网页时,有两类场景会制约快速响应: 1)当执行大计算量的操作或者设备性能不足时,页面掉帧,导致卡顿,概括为CPU瓶颈 2)进行I/O操作时,需要等待数据返回才能继续操作,等待期间不能快速响应,概括为I/O的瓶颈
为什么执行一段复杂的js代码会导致页面卡顿?
2.1.1 事件循环
默认情况下,浏览器(以Chrome为例)的每个Tab页对应一个渲染进程,渲染进程包含主线程、合成线程、I/O线程等多个线程。主线程要处理DOM、计算样式、处理布局、处理事件响应、执行JS代码等。事件循环流程如下图:
所有参与调度的任务会加入任务队列中,根据队列的‘先进先出’特性,最早加入队列的任务会被优先处理。 其它进程通过IPC将任务发送给渲染进程的I/O线程,I/O线程再将任务发送给主线程的任务队列,比如:
- 点击鼠标后,浏览器进程通过IPC将“点击事件”发送给I/O线程,I/O线程将其发送给任务队列;
- 资源加载完成后,网络进程通过IPC将‘加载完成时间’发送给I/O线程,I/O线程将其发送给任务队列 除任务队列外,浏览器还根据WHATWG标准实现了延迟队列,用于存放需要被延迟执行的任务(如setTimeout)。当本轮循环任务执行完成后,浏览器将检查是否有延迟任务过期,如果有任务过期则执行。如果本轮循环任务执行时间过长,可能导致在其后的延迟任务无法按期执行。即使延迟任务的延迟时间设置为0,也要等待test所在任务执行完后才能执行。同时,setTimeout并不属于ECMAScript标准,其规范由WHATWG中的HTML标准实现。各宿主环境在实现时预设了最小延迟时间,比如在Chromium中,最小延迟时间为4ms。 加入任务队列中的新任务需要等待队列中的其它任务都执行完后才能执行,这对于‘突发情况下需要优先执行的任务’是不利的。任务队列中的任务被称为宏任务,为了解决时效性问题,在宏任务执行过程中可以产生微任务,保存在该任务执行上下文中的微任务队列中。在宏任务执行结束前,线程会遍历其微任务队列,将该宏任务执行过程中产生的微任务批量执行。 举个例子:用于监控DOM变化的微任务API——MutationObserver。当同一个宏任务中发生多次DOM变化,会产生多个MutationObserver微任务,其执行时机是‘该宏任务执行结束前’,相比重新进入任务队列等待执行,更有时效性。同时,微任务队列中的微任务被批量执行,性能更好。
2.1.2 浏览器渲染
与渲染相关的任务:
- DOM:将HTML解析为DOM树,开发者可以在浏览器控制台输入document感知他们的存在
- Style:解析CSS,开发者可以在浏览器控制台输入document.styleSheets感知
- Layout:构建布局树,布局树会移除DOM树中不可见的部分,并计算可见部分的几何位置
- Layer:将页面划分为多个图层,一些层叠上下文CSS属性(z-index,opacity,position)、‘由于显示不全被裁剪的内容’等会使DOM元素形成独立的图层
- Paint:为每个图层生成包含“绘制信息”的绘制列表,将绘制列表提交给渲染进程的合成线程用于绘制 (注:可以使用Chrome浏览器调试工具中的Performance工具直观体验上述任务的执行过程) 执行上述任务的流程称为渲染流水线:
- 重排:当通过JS或CSS修改DOM元素的几何属性,会触发整个渲染流水线
- 重绘:当修改不涉及几何属性,会省略Layout、Layer过程
- 合成:不涉及重排重绘,省略Layout、Layer、Paint,仅执行合成线程中的绘制工作 绘制的最终产物是一张图片,这张图片被发送到显卡后即可显示在屏幕上。屏幕的刷新频率通常是60Hz,每16.6ms刷新一次。当屏幕刷新频率与显卡更新频率一致时,用户感知不到卡顿,反之,如果js执行时间过长,则会感知卡顿。
2.1.3 CPU瓶颈
在React中,最有可能造成CPU瓶颈的部分是“VDOM相关工作”。React在“运行时”寻求的解决方案具体做法是:将VDOM的执行过程拆分为一个个独立的宏任务,将每一个宏任务的执行时间限制在一定范围内(初始为5ms),一减少掉帧的可能性。这一技术被称为Time Slice(时间切片)。
2.1.4 I/O瓶颈
前端最主要的I/O瓶颈是网络延迟,React的解决方法是:将人机交互的研究成果整合到UI中。人机交互的研究成果表明,用户对不同操作的卡顿感知程度不同。因此:
- 为不同操作造成的‘自变量变化’赋予不同优先级
- 所有优先级统一调度,优先处理高优先级
- 当前操作执行时,有更高优先级操作,中断当前操作,执行更高优先级的操作
需要React底层实现:
- 用于调度优先级的调度器
- 用于调度器的调度算法
- 支持可中断的VDOM实现 2.1.3节中,Time Slice将VDOM工作流程拆分为多个短宏任务,当上一个短宏任务完成后,下一个短宏任务开始前,正是检查是否应该中断的时机。因此,不管是解决CPU瓶颈还是解决I/O瓶颈,底层诉求都是Time Slice。
2.2 底层架构的演进
React从v15升级到v16之后重构了整个架构,主要原因是旧架构无法实现Time Slice。
2.2.1 新旧架构介绍
React 15架构分为两部分:
- Reconciler(协调器):——VDOM的实现,负责根据自变量变化计算出UI变化
- Render(渲染器):——负责将UI变化渲染到宿主环境中 在Reconciler中,mount组件会调用mountComponent,update组件会调用updateComponent,这两个方法都会递归更新子组件,更新流程一旦开始,中途无法中断。 React 16架构分为三部分:
- Scheduler(调度器):——调度任务的优先级,高优先级任务优先进入Reconciler
- Reconciler(协调器):——VDOM的实现,负责根据自变量变化计算出UI变化
- Render(渲染器):——负责将UI变化渲染到宿主环境中 在新架构中,Reconciler中的更新流程从递归变成了‘可中断的循环过程’。每次循环都会调用shouldYield判断当前TimeSlice是否还有剩余时间,没有剩余时间则暂停更新流程,将主线程交给渲染流水线,等待下一个宏任务再继续执行,这就是Time Slice的实现原理
const workLoopConcurrent=()=>{
// 一直执行任务,直到任务执行完或中断
while(workInProgress !==null && !shouldYield){
performUnitOfWork(workInProgress)
}
}
const shouldYield=()=>{
// 当前时间是否大于过期时间
// 其中deadline=getCurrenTime()+yieldInterval
// yieldInterval为调度器预设的时间间隔,默认为5ms
return getCurrentTime()>=deadline
}
当Scheduler将调度后的任务交给Reconciler后,Reconciler最终会为VDOM元素标记各种副作用flags
// 代表插入或移动元素
export const Placement = 0b000000000010
// 代表更新元素
export const Update = 0b0000000000100
// 代表删除元素
export const Deletion =0b00000001000
SCheduler和Reconciler的工作都在内存中进行,只有当Reconciler完成工作后,工作流程才会进入Render,Render根据上述标记执行对应操作 由于前两者工作在内存中,不会更新宿主环境,即使反复中断,用户也不会看到更新不完全的UI
2.2.2 主打特性的迭代
随着React架构的重构,React上层特性也随之迭代: 1)Sync(同步) 2)Async Mode(异步模式) 3)Concurrent Mode(并发模式) 4)Concurrent Feature(并发特性) 从1)到2),Reconciler工作流程从‘同步’变成‘异步,可中断’;从2)到3),多个更新的工作流程可以并发执行
2.2.3 渐进升级策略的迭代
使用旧版本的开发者应该逐步升级,要注意旧版本的部分生命周期钩子在新版本中是‘不安全的’。对此,React团队采用了‘渐进升级的方案’。该方案第一步是规范代码,v16.3新增了StrictMode针对开发者编写的不符合‘并发更新规范的代码’给出提示;下一步,允许‘不同情况的React’在同一个页面存在,具体做法是提供三种开发模式: 1)Legacy模式,通过ReactDOM.render(,rootNode)创建的应用,默认关闭StrictMode 2)Blocking模式,通过ReactDOM.createBlockingRoot(rootNode).render(),默认开启StrictMode 3)Concurrent模式,通过ReactDOM。createRoot(rootNode).render(),默认开启StrictMode 上述渐进式升级的目的并没有达到,因此,团队改变了升级策略:开发者仍可以在默认情况下使用同步更新,在使用并发特性后再开启并发更新 以下示例中,updateComponent函数在startTransition的回调函数中执行,因此会触发并发更新,如果不在startTransition中执行,默认触发同步更新
const App =()=>{
const [count, updateCount]=useSate(0)
const [isPending,startTransition]=useTransition()
const onClick=()=>{
startTransition(()=>{
updateCount((count)=>count++)
})
}
return <h3 onClick={onClick}></h3>
}
2.3 Fiber架构
Fiber是VDOM在React中的实现,也是新架构的基础。React中有三种节点类型:
- React Element(React元素),即createElement方法的返回值;
- React Component(React组件),开发者可以在React中定义函数、类两种类型的Component
- FiberNode,组成Fiber架构的节点 三者关系如下:
// App是React Component
const App =()=>{
return <h3>Hello</h3>
}
// ele是React Element
const ele = <App/>
// 在React运行时内部,包含App对应的FiberNode
ReactDOM.creatRoot(rootNode).render(App)
2.3.1 FiberNode的含义
FiberNode三层含义: 1)作为架构,v15的Stack Reconciler,v16的Fiber Reconciler 2)作为‘静态数据结构’,每个FiberNode对应一个React元素,用于保存React元素的类型、对应的DOM元素等信息 3)作为“动态工作单元”,每个FiberNode用于保存‘本次更新中该React元素变化的数据、要执行的工作(增、删、改、更新Ref、副作用等)’
// FiberNode构造函数
function FiberNode(tag,pendingProps,key,mode){
this.tag=tag
this.key=key
this.elementType=null
...
}
Fiber架构是由多个FiberNode组成的树状结构,FiberNode之间由如下属性连接
// 指向父FiberNode
this.return =null
// 指向第一个子FiberNode
this.child=null
// 指向右边的兄弟FiberNode
this.sibling=null
return指“FiberNode执行完completeWork后返回的下一个FiberNode”
graph TD
App --> div
div --> App
div --> Hello
Hello --> div
Hello --> span
span --> div
‘只有唯一文本节点’的FiberNode不会生成独立FiberNode 作为静态数据结构:
// 对应组件类型Function/Class/Host
this.tag=tag
this.key=key
// 大部分情况下type相同,某些情况如FunctionComponent使用React.memo包裹就不同
this.elementType=null
// 对FunctionComponent指函数本身
// 对ClassComponent指Class
// 对HostComponent指DOM tagName
this.type =null
// FiberNode对应的元素,比如FunctionComponent对应的DOM元素
this.stateNode=null
第三章 Render阶段
Reconciler工作的阶段在React内部称为render阶段,ClassComponent的render函数,FunctionComponent函数本身都在该阶段被调用。根据Scheduler调度的结果不同,render阶段可能开始于performSyncWorkOnRoot(同步更新流程)或者performConcurrentWorkOnRoot(并发更新流程)方法
// performSyncWorkOnRoot会执行该方法
function workLoopSync(){
while(workInProgress !== null){
performUnitOfWork(workInProgress)
}
}
// performConcurrentWorkOnRoot会执行该方法
function workLoopConcurrent(){
while(workInProgress!==null && !shouldYield()){
performUnitOfWork(workInProgress)
}
}
workInProgress变量代表“生成Fiber Tree”工作已经进行到“wip fiberNode”。performUnitOfWork方法会创建下一个fiberNode并赋值给wip,并将wip与已创建的fiberNode连接起来构成Fiber Tree。wip===null代表“Fiber Tree的构建工作结束”。上述两个方法的唯一区别是“是否调用shouldYield(是否哦可中断)”
3.1 流程概览
Fiber Reconciler是从Stack Reconciler重构而来,通过遍历的方式实现可中断的递归,因此performUnitOfWork的工作可以分为两部分:“递”和“归” “递”阶段会从HostRootFiber开始向下以DFS的方式遍历,为“遍历到每个fiberNode”执行beginWork方法。该方法会根据传入的fiberNode创建下一级fiberNode。有两种情况: 1)下一级只有一个元素,这时beginWork方法会创建子fiberNode,并与wip连接 如下例子中,如果wip为ULFiberNode,则会创建“LiFiberNode”,同时两者产生连接
// JSX情况
<ul><li></li></ul>
LiFiber.return = ULFiber
2)下一级有多个元素,这时beginWork方法会依次创建所有子fiberNode并连接在一起,为首的子fiberNode会与wip链接
// JSX情况
<ul><li></li><li></li><li></li></ul>
Li0Fiber.sibling = Li1Fiber
Li1Fiber.sibling =Li2Fiber
Li0Fiber.return = ULFiber
当遍历到叶子元素(不包含子fiberNode)时,performUnitOfWork就会进入“归”阶段,“归”阶段会调用completeWork方法处理fiberNode。当某个fiberNode执行完completeWork方法之后,如果其存在兄弟fiberNode,则进入其兄弟fiberNode的“递”阶段,否则进入其父fiberNode的‘归’阶段。“递”阶段和“归”阶段会交错执行直至HostRootFiber的“归”阶段。render阶段工作结束。 render阶段执行流程如下例子所示:
const App =()=>{
return <div>
Hello
<span></span>
</div>
}
HostRootFiber beginWork // 生成App fiberNode
App fiberNode beginWork // 生层DIV fiberNode
DIV fiberNode beginWork // 生成"Hello"、SPAN fiberNode
"Hello" fiberNode beginWork // 叶子元素
"Hello" fiberNode completeWork
SPAN fiberNode beginWork // 叶子元素
SPAN fiberNode completeWork
DIV fiberNode completeWork
App fiberNode completeWork
HostRootFiber completeWork
如果将performUnitOfWork方法改写为“递归”版本,大致如下:
functionn performUnitOfWork(){
// 省略执行beginWork工作
if(fiberNode.child){
performUnitOfWork(fiberNode.child)
}
// 省略执行completeWork工作
if(fiberNode.sibling){
performUnitOfWork(fiberNode.sibling)
}
}
3.2 beginWork
beginWork工作流程如下图所示
首先判断当前流程属于mount还是update,判断依据为‘Current fiberNode是否存在’:
if(current !== null){
// 省略update流程
}
如果当前流程是update流程,则wip fiberNode存在对应的Current fiberNode。如果本次更新不影响fiberNode.child,则可以复用对应Current fiberNode,这是一条render阶段的优化路径 如果无法复用Current fiberNode,则mount与update的流程大体一致,包括: 1) 根据wip.tag“不同类型元素的处理分支” 2) 使用reconcile算法生成下一级的fiberNode 两个流程的区别在于“最终是否会为‘生成的子fiberNode’标记‘副作用flags’”
function beginWork(current, workInProgress, renderLanes) {
if (current !== null) {
// 省略代码update时判断是否可复用
} else {
// 省略代码
}
// 根据tag不同,进入不同处理逻辑
switch (workInProgress.tag) {
case IndeterminateComponent:
//
case LazyComponent:
case FunctionComponent:
case ClassComponent:
case HostRoot:
case HostComponent:
case HostText:
}
}
- HostComponent代表原生Element类型(比如DIV、SPAN);
- IndetermintaeComponent是FC mount时进入的分支,update时则进入FunctionComponent分支;
- HostText代表文本元素类型 如果常见类型(FunctionComponent、ClassComponent、HostComponent没有命中优化策略,它们最终会进入reconcileChildren方法),也是Reconciler模块的核心部分:
export function reconcileChildren(
current,
workInProgress,
nextChildren,
renderLanes
) {
if (current === null) {
// mount时
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes)
}
}else {
// update时
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes)
}
mountChildFibers与reconcileChildFibers方法都是ChildReconciler方法的返回值
var reconcileChildFibers=childReconciler(true)
var mountChildFibers =childReconciler(false)
function childReconciler(shouldCheckSideEffects){
}
shouldTrackSideEffects代表“是否追踪副作用”,即是否标记flags。在执行mountChildFibers方法时,以其内部的placeChild方法举例:
// 标记“要插入UI”的fiberNode
function placeChild(newFiber, lastPlacedIndex, newIndex) {
newFiber.index = newIndex
if (!shouldCheckSideEffects) {
newFiber.flags |= Forked;
return lastPlacedIndex
}
newFiber.flags |= Placement
}
reconcileChildFibers中标记flags主要与元素位置有关,包括:
- 标记ChildDeletion,代表删除操作
- 标记Placement,代表插入或移动操作
3.3 React中的位运算
上述所有flags都在packages/react-reconciler/src/ReactFiberFlags.js中定义,以Int32(32位有符号整数)的形式参与运算。“标记flags”的本质是二进制数的位运算。
3.3.1 基本的三种位运算
按位与(&):如果两个二进制操作数的每个bit都为1,则结果为1,否则为0. 按位或(|):如果两个二进制操作数的每个bit都为0,则结果为0,否则为1 按位非(~):对一个二进制操作数逐位进行取反操作(0、1互换)
let x = new Int32Array([3]) // 3的Int32
let y = new Int32Array([2]) // 2的Int32
let a = new Int32Array([10]) // 10的Int32
let res1 = x & y
let res2 = a | x
let res3 = ~x
console.log(res1, res2, res3);
3.3.2 位运算在“标记状态”中的应用
React源码内部有多个上下文环境,在执行方法时经常需要判断“当前处于哪个上下文环境中”:
// 未处于React上下文
const NoContext = /* */ 0b0000
// 处于batchedUpdates上下文
const BatchContext = /* */ 0b0001
// 处于render阶段
const RenderContext = /* */ 0b0010
// 处于commit阶段
const CommitContext = /* */ 0b0100
当执行流程进入render阶段,会使用按位或标记进入对应的上下文:
let executionContext = NoContext
executionContext |= RenderContext
此时可以结合按位与和NoContext来判断“是否处在某一上下文中”
// 是否处在RenderContext上下文中,结果为true
(executionContext & RenderContext) !== NoContext
// 是否处在CommitContext上下文中,结果为false
(executionContext & CommitContext) !== NoContext
离开RenderContext上下文后,结合按位与、按位非移除标记
// 从当前上下文中移除RenderContext上下文
executionContext &= ~RenderContext
// 是否处在RenderContext上下文中,结果为false
(executionContext & RenderContext) !== NoContext
3.4 completeWork
流程大体包括: 1)创建或者标记元素更新 2)flags冒泡
3.4.1 flags冒泡
当更新流程经过Reconciler后,会得到一棵Wip Fiber Tree,其中部分fiberNode被标记flags。fiberNode.subtreeFlags记录了该fiberNode的‘所有子孙fiberNode上被标记的flags’。每个fiberNode经由如下操作,可以将子孙fiberNode中“标记的flags”向上冒泡一层:
let subtreeFlags = NoFlags
// 收集子fiberNode的子孙fiberNode中标记的flags
subtreeFlags |= child.subtreeFlags
// 收集子fiberNode标记的flags
subtreeFlags |= child.flags
// 附加在当前fiberNode的subtreeFlags上
completeWork.subtreeFlags |= subtreeFlags
当 HostRootFiber完成completeWork,整棵Wip Fiber Tree中所有“被标记的flags”都在HostRootFiber.subtreeFlags中定义。在Renderer中,通过任意一级fiberNode.subtreeFlags都可以快速确定“该fiberNode所在子树是否存在副作用需要执行”
3.4.2 mount概览
completeWork流程,以HostComponent为例,如下图:
与beginWork相同,completeWork通过current !== null判断是否处于update流程。在mount流程中,其首先通过createInstance方法创建“fiberNode对应的DOM元素”:
然后执行appendAllChildren方法,将下一层DOM元素插入“createInstance方法创建的DOM元素”中,具体逻辑为:
1)从当前fiberNode向下遍历,将遍历到的第一层DOM元素类型(HostComponent、HostText)通过appendChild方法插入 parent末尾;
2)对兄弟fiberNode执行步骤(1);
3)如果没有兄弟fiberNode,则对父fiberNode的兄弟执行步骤(1);
4)当遍历流程回到最初执行步骤(1)所在层或者parent所在层时终止。
相关代码如下:
const appendAllChildren = function (parent, workInProgress,/*省略参数*/) {
let node = workInProgress.child
while (node !== null) {
// 步骤1,向下遍历,对第一层DOM元素执行appendChild
if (node.tag === HostComponent || node.tag === HostText) {
// 对HostComponent、HostText进行appendChild
appendInitialChild(parent, node.stateNode)
} else if (node.child !== null) {
// 继续向下遍历,直到找到第一层DOM元素类型
node.child.return = node
node = node.child
continue
}
// 终止情况1:遍历到parent对应FiberNode
if (node === workInProgress) { return }
// 如果没有兄弟fiberNode,则向父fiberNode遍历
while (node.sibling === null) {
// 终止情况2:回到最初执行步骤1所在层
if (node.return === null || node.return === workInProgress) {
return
}
node = node.return;
}
// 对兄弟fiberNode执行步骤1
node.sibling.return = node.return
node = node.sibling
}
}
completeWork流程接下来会执行finalizeInitialChildren方法完成属性的初始化,包括如下几类属性:
- styles,对应 setValucForStyles方法:
- innerHTML,对应setInnerHTML方法;
- 文本类型 children,对应 setTextContent方法;
- 不会在DOM中冒泡的事件,包括cancel、close、invalid、load、scroll、toggle,对应 listenToNonDelegatedEvent 方法;
- 其他属性,对应setValueForProperty方法。 最后,执行bubbleProperties方法将 flags 冒泡。completeWork在mount时的流程总结如下: 1)根据wip.tag进入不同处理分支; 2)根据current !=nu11区分mount与update流程; 3)对于HostComponent,首先执行createInstance方法创建对应的DOM元素; 4)执行appendAllChildren将下一级DOM元素挂载在步骤(3)创建的DOM元素下; 5)执行 finalizeInitialChildren 完成属性初始化; 6)执行bubbleProperties 完成 flags冒泡。 如果mountChildFibers 不标记 flags,mount时如何完成UI渲染? 由于appendAllChildren 方法的存在,当completeWork执行到HostRootFiber时,已经形成一棵完整的离屏DOM Tree。由于HostRootFiber存在alternate(即HostRootFiber.current!==null),因此HostRootFiber在beginWork时会进入reconcileChildFibers而不是mountChildFibers,它的子fiberNode会被标记Placement。Renderer发现本次更新流程中存在一个Placement flag,执行一次parentNode.appendChild,即可将已经构建好的离屏DOM Tree插入页面。
3.4.3 update概览
completeWork的mount流程完成了属性的初始化,update流程将完成标记属性的更新。updateHostComponent的主要逻辑在diffProperties方法中,该方法包括两次遍历:
- 第一次遍历,标记删除“更新前有,更新后没有”的属性
- 第二次遍历,标记更新“update流程前后发生改变”的属性
functionn diffProperties(domElement, tag, lastRawProps, nextRawProps, rootContainerElement){
// 保存变化属性的key、value
let updatePayload = null
// 更新前的属性
let lastProps;
// 更新后的属性
let nextProps;
// 省略代码
// 标记删除更新前后,更新后没有的属性
for (propKey in lastProps) {
if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) {
continue
}
// 处理style
if (propKey === STYLE) {
// 省略代码
} else {
// 其他属性
(updatePayload = updatePayload || []).push(propKey, null)
}
}
// 标记更新“update流程前后发生改变”的属性
for (propKey in nextProps) {
let nextProp = nextProps[propKey]
let lastProp = lastProps != null ? lastProps[propKey] : undefined
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) { continue }
if (propKey === STYLE) {
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
} else if (propKey === CHILDREN) {
} else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
}
} else { }
}
return updatePayload
}
所有变化属性的key、value会保存在fiberNode.updateQueue中。同时,该fiberNode会标记Update: workInProgress.flags |=Update 在fiberNode.updateQueue中,数据以key,value作为数组的相邻两项。
第四章 commit阶段
Renderer工作的阶段被称为commit阶段。在这个阶段,会将各种副作用commit到宿主环境UI中。render阶段流程可能被打断,而commit阶段一旦开始就会同步执行直到完成。整个阶段可以分为三个子阶段: 1)BeformMutation 2)Mutation 3)Layout
4.1 流程概览
commit阶段起始于commitRoot方法的调用:
commitRoot(root)
- root代表本次更新所属的FiberRootNode
- root.finishedWork代表Wip HostRootFiber 三个子阶段开始前会判断是否有与三个子阶段相关的副作用。
4.1.1 子阶段的执行流程
render阶段的completeWork会完成自下而上的subtreeFlags标记过程,而commit阶段的三个子阶段会完成自下而上的subtreeFlags消费过程。每个子阶段都遵循三段式: 1)commitXXXEffects 入口函数,finishedWork会作firstChild参数传入。将firstChild赋值给全局变量nextEffext,执行commitXXXEffects_begin 2)commitXXXEffect_begin 向下遍历直到第一个满足如下条件的fiberNode:
- 当前fiberNode的子fiberNode不包含该子阶段对应的flags;
- 当前fiberNode是叶子元素 对目标fiberNode执行commitXXXEffect_complete 3)commitXXXEffect_complete 执行flags对应操作的函数,包含三个步骤:
- 对当前fiberNode执行flags对应操作,即执行commitXXXEffectsOnFiber;
- 如果当前fiberNode存在兄弟fiberNode,则对兄弟执行commitXXXEffects_begin;
- 如果不存在兄弟fiberNode,则对父fiberNode执行commitXXXEffects_complete;
4.1.2 Effect list
Fiber架构早期版本不使用subtreeFlags,而是Effect list。在completeWork阶段,会把存在副作用的fiberNode插入Effect list,commit阶段只需遍历Effect list。然而由于是否开启并发更新的Suspense的区别,将Effect list重构为subtreeFlags