|
|
@ -4,8 +4,6 @@ |
|
|
|
|
|
|
|
|
|
|
|
本次源代码解读的 DEMO 只用了最简单的 `useState` 做计数器修改触发页面刷新,并没有新增新的 DOM 节点之外的其他操作,第一次和第二次和之后页面刷新实际上都只是 `useState` 触发变化并在页面上显示计数内容出现变化 |
|
|
|
本次源代码解读的 DEMO 只用了最简单的 `useState` 做计数器修改触发页面刷新,并没有新增新的 DOM 节点之外的其他操作,第一次和第二次和之后页面刷新实际上都只是 `useState` 触发变化并在页面上显示计数内容出现变化 |
|
|
|
|
|
|
|
|
|
|
|
## React 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Fiber 递归开始首先会交由 createWorkInProgress 函数生成 WorkInProgress 的第一个 Fiber 节点,这个节点就是 FiberNode,所以我们也从 createWorkInProgress 函数开始讲解 React 中 Fiber 递归的流程,**这个函数的主要工作就是根据传入的 Fiber 节点的判断复用 Fiber 节点还是创建新的 Fiber 节点**,而这个判断的条件就是传入的 Fiber 节点的 alternate 属性是否存在。 |
|
|
|
Fiber 递归开始首先会交由 createWorkInProgress 函数生成 WorkInProgress 的第一个 Fiber 节点,这个节点就是 FiberNode,所以我们也从 createWorkInProgress 函数开始讲解 React 中 Fiber 递归的流程,**这个函数的主要工作就是根据传入的 Fiber 节点的判断复用 Fiber 节点还是创建新的 Fiber 节点**,而这个判断的条件就是传入的 Fiber 节点的 alternate 属性是否存在。 |
|
|
|
|
|
|
|
|
|
|
|
在第一次进入到这个函数时也就是首屏渲染中传入的 FiberNode 是不存在这个属性的,所以会进入到 createFiber 函数:即执行创建新 Fiber 节点的逻辑。 |
|
|
|
在第一次进入到这个函数时也就是首屏渲染中传入的 FiberNode 是不存在这个属性的,所以会进入到 createFiber 函数:即执行创建新 Fiber 节点的逻辑。 |
|
|
@ -60,91 +58,5 @@ 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 属性: |
|
|
|
|
|
|
|
- 以下条件跳出当前循环: |
|
|
|
|
|
|
|
1. 新 props 自身不存在相同属性 |
|
|
|
|
|
|
|
2. 属性非旧 props 自身属性 |
|
|
|
|
|
|
|
3. 该属性在旧 props 的值为 null |
|
|
|
|
|
|
|
- 当找到 style 属性,会遍历 style 属性,初始化 `styleUpdates` 对象为空,并在上边新增当前 style 属性的 key 并赋值空字符串 |
|
|
|
|
|
|
|
- 当找到一些特殊属性如 `dangerouslySetInnerHTML` 等,会初始化 `updatePayload` 为空数组 |
|
|
|
|
|
|
|
- 如果没有以上属性,即没有 style 和 特殊属性,会进入最后的 else 逻辑:给 `updatePayload` 数组 `push` 进属性名和 null,进入下一轮循环 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
循环新 props 属性: |
|
|
|
|
|
|
|
- 以下条件跳出当前循环: |
|
|
|
|
|
|
|
1. 新 props 自身不存在当前属性 |
|
|
|
|
|
|
|
2. 新 props 属性值和旧 props 属性值相同(引用相同) |
|
|
|
|
|
|
|
3. 新旧 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 属性` 或者等于 `null`: |
|
|
|
|
|
|
|
- 如果 `styleUpdates` 不为 `null`,且是数组时长度不等于 0,给 `updatePayload` `push` 进 `style` 和 `styleUpdates` |
|
|
|
|
|
|
|
- 否则给 `styleUpdates` 赋值 `props style` 属性 |
|
|
|
|
|
|
|
- 当找到特殊属性 `dangerouslySetInnerHTML` : |
|
|
|
|
|
|
|
- 会与旧 props 的 `dangerouslySetInnerHTML` 对比,发生变化则 push 进 updatePayload 数组 |
|
|
|
|
|
|
|
- 当找到 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 阶段的流程解析 |
|
|
|
你可以会问我,到这里就没有了吗?,是的没有了,这就是一个简单例子下 `React Fiber` 递归的全部流程,从 `beginWork` 到 `completeWork`,从首屏渲染到触发更新,没有涉及到新增节点和删除属性的情况,但是简单的递归逻辑已经是到这里就结束了,后边还会写更加详细的单个 render 阶段函数的详细解析和 commit 阶段的流程解析 |