说明:本文结论均基于 React 16.13.1 得出,若有出入请参考对应版本源码
题目
在开始进行源码分析前,我们先来看几个题目:
题目一:
渲染下面的组件,打印顺序是什么?
1 | import React from 'react' |
答案:4 3 2 1
题目二:
点击 p
标签后,下面事件发生的顺序?
1 | 1. 页面显示 b |
1 | import React from 'react' |
答案:1 2 3 4 5
你是不是都答对了呢?
首次渲染流程
我们以下面这个例子来阐述下首次渲染的流程:
1 | function Name({name}) { |
顺着入口我们回来到 performSyncWorkOnRoot
:
1 | function performSyncWorkOnRoot(root) { |
到这里的时候,我们会得到一个如下的数据结构:
从代码中,我们可以知道 performSyncWorkOnRoot
可以分为两个大的阶段:
- Render
- Commit
Render
首先看看 renderRootSync
:
1 | function renderRootSync(root, expirationTime) { |
这里首先调用 prepareFreshStack(root, expirationTime)
,这一句主要是通过 root.current
来创建 workInProgress
。调用后,数据结构成了这样:
左右两个分别是两棵 Fiber
树,其中左边的是当前正在使用的树,右边是正在构建的树,首次渲染时当前正在使用的树为空。
跳过中间的一些语句,我们来到 workLoopSync
:
1 | function workLoopSync() { |
很简单,就是不停的调用 performUnitOfWork
:
1 | function performUnitOfWork(unitOfWork: Fiber): void { |
这里又分为两个步骤:
beginWork
,传入当前 Fiber 节点,创建子 Fiber 节点。completeUnitOfWork
,通过 Fiber 节点创建真实 DOM 节点。
这两个步骤会交替的执行,其目标是:
- 构建出新的 Fiber 树
- 与旧 Fiber 比较得到 effect 链表(插入、更新、删除、useEffect 等都会产生 effect),该链表会在 commit 阶段使用
beginWork
1 | function beginWork( |
这里因为是 rootFiber
,所以会走到 updateHostRoot
:
1 | function updateHostRoot(current, workInProgress, renderExpirationTime) { |
经过 updateHostRoot
后,会返回 workInProgress.child
作为下一个 workInProgress
,最后的数据结构如下(这里先忽略 reconcileChildren
这个比较复杂的函数):
接着会继续进行 beginWork
,这次会来到 mountIndeterminateComponent
(暂时忽略)。总之,经过不断的 beginWork
后,我们会得到如下的一个结构(这里只列出了右边的部分):
此时 beginWork
返回的 next
为空,我们会走到:
1 | if (next === null) { |
completeUnitOfWork
1 | function completeUnitOfWork(unitOfWork: Fiber): void { |
此时这里的 unitOfWork
是 span
对应的 FiberNode
。从函数头部的注释我们可以大致知道该函数的功能:
1 | // Attempt to complete the current unit of work, then move to the next |
这里一路走下去最后会来到 completeWork
这里 :
1 | case HostComponent: |
执行完后,我们的结构如下所示(我们用绿色来表示真实 dom):
此时 completeWork
返回的 next
将会是 null
,我们需要找到下一个 FiberNode
来进行处理,因为 span
没有兄弟节点,所以只能往上找到它的父节点 Name
进行处理。
因为 Name
是一个 FunctionComponent
,所以在 completeWork
中直接返回了 null
。
1 | function completeWork( |
又因为它有 sibling
,所以会将它的 sibling
赋值给 workInProgress
,并返回对其进行 beginWork
。
1 | const siblingFiber = completedWork.sibling |
1 | function performUnitOfWork(unitOfWork: Fiber): void { |
这样 beginWork
和 completeWork
不断交替的执行,当我们执行到 div
的时候,我们的结构如下所示:
之所以要额外的分析 div
的 complete
过程,是因为这个例子方便我们分析 appendAllChildren
:
1 | appendAllChildren = function ( |
由于 workInProgress
指向 div
这个 fiber
,他的 child
是 Name
,会进入 else if (node.child !== null)
这个条件分支。然后继续下一个循环,此时 node
为 span
这个 fiber
,会进入第一个分支,将 span
对应的 dom
元素插入到 parent
(这里的 parent
就是 div
的真实 dom)之中。
这样不停的循环,最后会执行到 if (node === workInProgress)
退出,此时所有的子元素都 append 到了 parent
之中:
然后继续 beginWork
和 completeWork
,最后会来到 rootFiber
。不同的是,该节点的 alternate
并不为空,且该节点 tag
为 HootRoot
,所以 completeWork
时会来到这里:
1 | case HostRoot: { |
1 | updateHostContainer = function (workInProgress: Fiber) { |
看来几乎没有做什么事情,到这我们的 render
阶段就结束了,最后的结构如下所示:
其中蓝色表示是有 effect 的 Fiber
节点,他们组成了一个链表,方便 commit 过程进行遍历。
可以访问 http://www.paradeto.com/react-render/ ,点击屏幕中的 beginWork 按钮,可以查看 render 过程的动画,建议横屏。
commit
commit
大致可分为以下过程:
- 准备阶段
- before mutation 阶段(执行 DOM 操作前)
- mutation 阶段(执行 DOM 操作)
- 切换 Fiber Tree
- layout 阶段(执行 DOM 操作后)
准备阶段
1 | do { |
准备阶段主要是确定 firstEffect
,我们的例子中就是 Name
这个 fiber
。
before mutation 阶段
before mutation
阶段主要是调用了 commitBeforeMutationEffects
方法:
1 | function commitBeforeMutationEffects() { |
因为 Name
中 effectTag
包括了 Passive
,所以这里会执行:
1 | scheduleCallback(NormalPriority, () => { |
这里主要是对 useEffect
中的任务进行异步调用,最终会在下个事件循环中执行 commitPassiveHookEffects
:
1 | export function commitPassiveHookEffects(finishedWork: Fiber): void { |
其中,commitHookEffectListUnmount
会执行 useEffect
上次渲染返回的 destroy
方法,commitHookEffectListMount
会执行 useEffect
本次渲染的 create
方法。具体到我们的例子:
因为是首次渲染,所以 destroy
都是 undefined,所以只会打印 useEffect ayou
。
mutation 阶段
mutation
阶段主要是执行了 commitMutationEffects
这个方法:
1 | function commitMutationEffects(root: FiberRoot, renderPriorityLevel) { |
其中,Name
会走 Update
这个分支,执行 commitWork
,最终会执行到 commitHookEffectListUnmount
:
1 | function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { |
这里会执行 useLayoutEffect
上次渲染返回的 destroy
方法,我们的例子里是 undefined。
而 App
会走到 Placement
这个分支,执行 commitPlacement
,这里的主要工作是把整棵 dom 树插入到了 <div id='root'></div>
之中。
切换 Fiber Tree
mutation 阶段完成后
,会执行:
1 | root.current = finishedWork |
完成后, fiberRoot
会指向 current Fiber
树。
layout 阶段
layout 阶段会执行 commitLayoutEffects
:1
2
3
4
5
6
7
8
9
10
11
12
13
14do {
if (__DEV__) {
...
} else {
try {
commitLayoutEffects(root, expirationTime);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
...
1 | import { |
commitLayoutEffects
中核心代码是遍历 effect 链表,对符合条件的执行 commitLayoutEffectOnFiber
即 commitLifeCycles
:
1 | function commitLifeCycles( |
其中 commitHookEffectListMount
我们在介绍 before mutation
的时候提到过,这里只是调用的时候所传的参数不同而已(HookLayout
表示当前的 effect 是 useLayoutEffect
产生的)。
所以,具体到我们的例子,这里会执行 useLayoutEffect
中的 create
函数,所以会打印 useLayoutEffect ayou
。
题目解析
现在,我们来分析下文章开始的两个题目:
题目一:
渲染下面的组件,打印顺序是什么?
1 | import React from 'react' |
解析:
useLayoutEffect
中的任务会跟随渲染过程同步执行,所以先打印 4Promise
对象then
中的任务是一个微任务,所以在 4 后面执行,打印 3console.log('1')
和console.log('2')
都会在宏任务中执行,执行顺序就看谁先生成,这里 2 比 1 先,所以先打印 2,再打印 1。
题目二:
点击 p
标签后,下面事件发生的顺序?
1 | 1. 页面显示 b |
1 | import React from 'react' |
解析:
span 这个 Fiber 位于 effect 链表的首部,在 commitMutations 中会先处理,所以页面先显示 b。
Name 这个 Fiber 位于 span 之后,所以 useLayoutEffect 中上一次的 destroy 紧接着其执行。打印 useLayoutEffect destroy a。
commitLayoutEffects 中执行 useLayoutEffect 这一次的 create。打印 useLayoutEffect create b。
useEffect 在下一个宏任务中执行,先执行上一次的 destroy,再执行这一次的 create。所以先打印 useEffect destroy a,再打印 useEffect create b。