13 KiB
React 从 16 开始,对底层架构做了一次重构,和 15 不同,渲染 vdom 的时候一改以往的递归执行,引入了一个新的概念,叫做 Fiber,虽然最后渲染到页面的时候还是递归,但是 Fiber 的递归是可以中断的,根据优先级由浏览器优先执行任务,保证在大量视图需要更新的时候,浏览器仍然能保证快速的响应
在 React 中,视图的更新使用了双缓存的方式,也就是说在 React 运行时,同时有着两棵 Fiber 树,一颗是当前视图上的 Fiber 树,叫 current
,另外一棵是存在内存当中的下一次视图更新时用的叫做 workInProgress
,React 在构建时会创建整个 app 唯一的根 Fiber 节点,叫做 FiberRootNode
,这个节点上有一个 current 指针
,指向的是当前正在页面上显示的 Fiber 树也就是 current
,当 workInProgress
递归生成完毕,指针会立即指向 workInProgress ,而旧的 current
就会在下一次渲染中变成 workInProgress
,就这样循环交替完成页面的递归渲染。
本次源代码解读的 DEMO 只用了最简单的 useState
做计数器修改触发页面刷新,并没有新增新的 DOM 节点之外的其他操作,第一次和第二次和之后页面刷新实际上都只是 useState
触发变化并在页面上显示计数内容出现变化
React 17
首屏渲染
React 的首屏渲染时会交由 createWorkInProgress 函数生成 WorkInProgress 的第一个 Fiber 节点,这个节点就是 FiberNode,所以我们从 createWorkInProgress 函数开始讲解 React 中 Fiber 创建的流程,这个函数的主要工作就是根据传入的 Fiber 节点的 alternate 属性是否存在判断复用 Fiber 节点还是创建新的 Fiber 节点,具体的函数深入解析可以看 React 的深入探索 - createWorkInProgress。
第一次 createWorkInProgress 执行完毕之后,接下来就会交给 beginWork 开始正式的 Fiber 递归,在 React 中 Fiber 的创建使用递归实现的深度优先遍历算法,即尽可能深的探索树的分支,探索完毕后再回溯,在这一过程中负责探索阶段的就是 beginWork 函数。
beginWork 执行在递归节点的 Fiber 创建之前,主要是为传入的 Fiber 节点创建第一个子 Fiber 节点,其内部可以看作一个大的 switch 语句,根据传入的 Fiber 节点的子元素类型不同执行不同的 Fiber 创建逻辑,不管父元素拥有多少个子元素,它最终都会创建并返回第一个子元素的 Fiber 节点,直到并不存在子元素为止,React 的深入探索 - beginWork。
如果探索到了可达的最后一个子元素,那么就会结束探索阶段,进入回溯阶段:执行completeWork 函数逻辑
completeWork
completeWork 执行在递归节点的 Fiber 创建之后,主要是为创建好的 Fiber 节点插入内容和插入真实 DOM 树
函数开始,会执行 popTreeContext 并传入 workInProgress,这个函数应该也和 Context 相关,然后进入到 swtich 逻辑,根据 WorkInProgress Fiber 节点的 tag 属性进入不同的 case 逻辑,这里和 beginWork 基本上类似 React 的遍历顺序是从父到子,最终再从子回到父,所以首屏渲染中首先进入 completeWork 的 WorkInProgress 不一定会是 FiberNode ,在这里是 HostComponent 进入到 HostComponent 的 case 之后,又执行了一遍 popTreeContext,官方的注释也写明似乎是有一些考虑在 在 HostComponent 的 case 中,会判断 current 和 WorkInProgress.stateNode 是否为空,在首屏渲染的 completeWork 节点,这里都是空的,所以进入为空的逻辑:先检查当前 Fiber 节点是否存在 props 属性,最后调用 createInstance 创建一个 DOM 实例 createInstance 会调用 createElement 方法创建一个 DOM 实例,并调用 precacheFiberNode 和 updateFiberProps 方法,这两个方法都是在插入属性,一个在当前 WorkInProgress Fiber 节点上插入 DOM,一个是在 DOM 上插入 props,最后将处理好的 DOM 返回
appendAllChildren
函数首先会判断 WorkInProgress 的 child 属性是否为空,如果不为空就进入 while 循环,因为这里是首屏渲染的第一次 completeWork 所以进入到一定是最小的子组件,不会存在 child 属性,所以会跳过循环,也就会跳过插入逻辑,回到 completeWork 的调用栈中
然后回到 completeWork 的调用栈,它会将处理完毕的 DOM 示例插入到 WorkInProgress 的 stateNode 属性中,然后会调用 inalizeInitialChildren ,这个函数会为创建的 DOM 元素,插入已有的 props,内部也根据 Fiber 节点的 tag 区分不同的处理逻辑,还有对 props 是否合法的校验,甚至根据 props 的属性也做了不同逻辑的处理,最后会返回一个 Boolean 用于判断是否进入 markUpdate 逻辑,这个函数会把 Fiber 打上 Update 标记
最后再执行 bubbleProperties 进行一些处理,不谈 整个 completeWork 的流程就走完了
第一次触发更新
createWorkInProgress
还是交由 createWorkInProgress 这个函数,这个函数会从 current 中取出 alternate 属性,用于判断是否已经存在 WorkInProgress 树,在本次更新,当前的 current 就是上一次渲染的 WorkInProgress,首屏渲染完成之后,实际上只渲染完成了一棵树,但是这次的 WorkInProgress 并不会走等于 null 的逻辑,因为在首屏渲染的时候,那个还是 current 的 WorkInProgress 实际上已经有一个 Fiber 节点了,那就是 FiberNode ,所以在这次渲染,它也还是有那一个 FiberNode 的,所以会走不为空的逻辑:复用现有的 Fiber 节点,其他和首屏渲染是一样的,这里不多赘述
beginWork
第一次触发更新,进入这个函数的逻辑前边和首屏渲染是都是一样的,但是在判断组件是否发生修改的时候逻辑和首屏渲染并不同,在首屏渲染,beginWork 会走到 switch 逻辑,并根据 Fiber tag 不同分别执行不同的 mount l逻辑,在这里会经过一些判断最终进入 attemptEarlyBailoutIfNoScheduledUpdate 这个函数,这个函数内部又有着一个 switch 逻辑,也是根据 workInProgress tag 属性分别进入不同的 case,在这个例子中进入的是 HostRoot 对应的 case,首先会执行 pushHostRootContext 函数,Context 相关 不谈,最后继续处理服务端渲染的逻辑最后返回交由 bailoutOnAlreadyFinishedWork 函数处理,bailoutOnAlreadyFinishedWork 会对 WorkInProgress 赋上 current 的 dependencies 属性,然后就是 bailoutOnAlreadyFinishedWork 最终的目的: 调用 cloneChildFibers
cloneChildFibers
这个函数从名字上就能看出来,目的是为了 clone Fiber 的 child 属性,这个函数会调用 createWorkInProgress 函数,从上边可以知道,这个函数可以根据传入的 current 和 WorkInProgress 进行判断是否创建一个新的 Fiber 节点或者复用已有的 current Fiber 节点,并将其互相链接,最终将结果返回,cloneChildFiber 接收到返回的 Fiber 子节点,将其链接到当前 WorkInProgress Fiber 的 child 属性上,并把 Fiber 子节点的 return 赋值 WorkInProgress Fiber
也就是说,如果在 beginWork 中当前,如果组件并没有发生变化且没有要执行的逻辑,那么他会直接 clone 已有的 Fiber 节点,而不会走到 reconcileChildren 函数创建新的 Fiber 节点
最后返回到 bailoutOnAlreadyFinishedWork 在由它返回到 beginWork,beginWork 流程执行结束
completeWork
和首屏渲染不同,这次 current 和 WorkInProgress.stateNode 不为空,所以会进入和首屏渲染时不一样的逻辑:调用 updateHostComponent 函数
updateHostComponent
函数内部会对当前 WorkInProgress Fiber 节点的新旧 props 进行对比,如果完全相同会直接返回,这里的完全相同实际上是指引用地址也相同,所以在本次就算新旧 props 相同也并会被 return,然后取出 Fiber 中的 stateNode 传递给 prepareUpdate 函数,然后被调用 diffProperties 函数,这里还有一个针对 props 的 children 属性是否为字符串或者数字的判断,暂时还不清楚具体目的是什么
diffProperties
函数开始会先执行对 props 属性的校验方法:validatePropertiesInDevelopment
方法,然后根据 Fiber tag 进入不同的 case,这里只有针对三种 tag有特殊的处理,分别是:input、select 和 textarea,本次进入 completeWork 的 Fiber 节点 tag 为 img,所以不会进入 特殊处理的 case,最终进入到 default 逻辑,然后被直接 break,去往 assertValidProps 函数
这里还有一个针对 Fiber 旧 props 属性 onClick 不是 function 且 新 props 属性 onClick 是 function 的一个特殊处理:就是直接给 DOM 的 onclick 属性赋值为一个空函数 ,从官方留下的注释中大概可以知道,这似乎是为了修复 Safari 浏览器的问题的特殊处理
assertValidProps
函数一开始会检查 props 是否存在,不存在直接 return 然后就是一大串的 if 用于对 props 是否合法的兜底操作,比如说 dangerouslySetInnerHTML 属性和 children 属性智能存在一个之类的,如果找到不合法的操作会直接通过 throw Error 被抛出
之后就进入 diffProperties 最主要的任务:对新旧 props 进行对比然后生成 updateQueue
这里有两个 for in 循环:循环旧 props
属性和循环新 props
属性
警告:以下解析受限于我的表达能力,可能会有读起来一头雾水的情况,建议和源码一起看
循环旧 props 属性:
- 以下条件跳出当前循环:
- 新 props 自身不存在相同属性
- 属性非旧 props 自身属性
- 该属性在旧 props 的值为 null
- 当找到 style 属性,会遍历 style 属性,初始化
styleUpdates
对象为空,并在上边新增当前 style 属性的 key 并赋值空字符串 - 当找到一些特殊属性如
dangerouslySetInnerHTML
等,会初始化updatePayload
为空数组 - 如果没有以上属性,即没有 style 和 特殊属性,会进入最后的 else 逻辑:给
updatePayload
数组push
进属性名和 null,进入下一轮循环
循环新 props 属性:
- 以下条件跳出当前循环:
- 新 props 自身不存在当前属性
- 新 props 属性值和旧 props 属性值相同(引用相同)
- 新旧 props 属性值都为 null
- 当找到 style 属性:
- 如果新 props 属性值存在且值合法(就是转换成 boolean 不为 false),会调用 Object.freeze 函数冻结当前属性值
- 如果旧 props 也存在 style 属性且不等于 null:
- 开始遍历旧 props style
- 如果出现旧
props style
有 style 样式,但是新props style
没有的也就是新增样式的情况,那么会初始化styleUpdates
对象为空(如果已经初始化过就会跳过),并在上边新增当前 style 属性的 key 并赋值空字符串
- 如果出现旧
- 开始遍历新 props style
- 如果找到新
props style
属性存在,但是与旧props style
属性不同,也就是样式发生修改的情况,那么会初始化styleUpdates
对象为空(如果已经初始化过就会跳过),并在上边对当前 style 属性的 key 做新增或是修改,值为新props style
属性
- 如果找到新
- 开始遍历旧 props style
- 如果旧
props
不存在style 属性
或者等于null
:- 如果
styleUpdates
不为null
,且是数组时长度不等于 0,给updatePayload
push
进style
和styleUpdates
- 否则给
styleUpdates
赋值props style
属性
- 如果
- 当找到特殊属性
dangerouslySetInnerHTML
:- 会与旧 props 的
dangerouslySetInnerHTML
对比,发生变化则 push 进 updatePayload 数组
- 会与旧 props 的
- 当找到 children 属性,会将其转换为字符串和 属性名一起
push
进updatePayload
- 当找到除
dangerouslySetInnerHTML
之外的特殊属性,会对其进行专属的逻辑 - 最后没有找到以上属性,会进入最后的 else 逻辑:给
updatePayload
数组push
进 props 属性名和 对应的属性值,进入下一轮循环
当所有的循环结束会判断 styleUpdates 是否合法(不为 null,且数组长度不为0),合法则进入将调用 validateShorthandPropertyCollisionInDev 函数,传递 styleUpdates 和 新 props style 属性,最后将 style 字符串
和 styleUpdates
推入 updatePayload
返回 updatePayload
回到 updateHostComponent
函数,将 prepareUpdate
也就是 diffProperties
返回的 updatePayload
赋值给 workInProgress
的 updateQueue
属性,如果它不为空则执行 markUpdate
函数
markUpdate
为传入的 workInProgress 打上 Update 标记
返回 null,回到 completeWork
的调用栈,如果 current ref 和 workInProgress ref 不同,调用 markRef 函数
markRef
为传入的 workInProgress 打上 Ref 和 RefStatic 标记
最后调用 bubbleProperties 函数,这又是一个比较长的函数,暂且不谈
completeWork 逻辑结束
你可以会问我,到这里就没有了吗?,是的没有了,这就是一个简单例子下 React Fiber
递归的全部流程,从 beginWork
到 completeWork
,从首屏渲染到触发更新,没有涉及到新增节点和删除属性的情况,但是简单的递归逻辑已经是到这里就结束了,后边还会写更加详细的单个 render 阶段函数的详细解析和 commit 阶段的流程解析