前言
最近开始深入学习React
的原理了,后面会出一系列关于React
原理的文章,基本都是我学习其他前辈的React
源码分析以及跟随他们阅读源码时的一些思考和记录,内容大部分非原创,但我会用我自己的方式去总结原理以及相关的流程,并加以补充,当作自己的学习总结。
本系列内容偏向底层源码实现,如果你是React
新手,不建议你细看。
本文的内容是关于React
首次渲染的流程。
首次渲染中,React代码变成DOM的流程
这里主要有两个步骤,第一个是从JSX
代码经过React.createElement
方法变成一个虚拟DOM
,第二个步骤是通过ReactDOM.render
方法把虚拟DOM
变成真实DOM
。
React.createElement
对于 React.createElement
方法,很多新人可能并不知道,因为我们在一般的业务逻辑中,比较少会直接使用这个方法。其实在React
官网中,就已经有写明了。
JSX 会被编译为 React.createElement(), React.createElement() 将返回一个叫作“React Element”的 JS 对象。
我们在babel官网里可以写个JSX
试一下。
JSX
在经过babel
的编译之后,变成了嵌套的React.createElement
。JSX 的本质其实就是React.createElement这个 JavaScript 调用的语法糖。 有了JSX
语法糖的存在,我们可以使用我们最为熟悉的类 HTML
标签语法来创建虚拟 DOM
,在降低学习成本的同时,也提升了研发效率与研发体验。
接下来,我们来看下createElement
的源码
export function createElement(type, config, children) {
// propName 变量用于储存后面需要用到的元素属性
let propName;
// props 变量用于储存元素属性的键值对集合
const props = {};
// key、ref、self、source 均为 React 元素的属性
let key = null;
let ref = null;
let self = null;
let source = null;
// config 对象中存储的是元素的属性
if (config != null) {
// 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
if (hasValidRef(config)) {
ref = config.ref;
}
// 此处将 key 值字符串化
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面
for (propName in config) {
if (
// 筛选出可以提进 props 对象里的属性
hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
const childrenLength = arguments.length - 2;
// 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
if (childrenLength === 1) {
// 直接把这个参数的值赋给props.children
props.children = children;
// 处理嵌套多个子元素的情况
} else if (childrenLength > 1) {
// 声明一个子元素数组
const childArray = Array(childrenLength);
// 把子元素推进数组里
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// 最后把这个数组赋值给props.children
props.children = childArray;
}
// 处理 defaultProps
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
复制代码
总结一下这个函数做的一些事情
- 1.二次处理
key
,ref
,self
,source
四个值(将key
字符串化,将config
中的ref
赋值给ref
,self
和source
暂不太清楚其功能,可以忽略,17版本的jsx方法直接删除了这两个参数) - 2.遍历
config
,筛选出可以赋值到props
里的属性 - 3.提取子元素,赋值到
props.children
中(如果只有一个子元素,就直接赋值,如果子元素大于一个,就以数组形式存储) - 4.格式化
defaultProps
(如果没有传入相关的props
,props
就取设置的默认值) - 5.返回一个
ReactElement
方法,并传入刚才处理的参数
createElement
其实就是一个数据处理器,把从JSX
获取到的内容进行格式化,再传入ReactElement
方法中。
注意:在React 17
版本当中,createElement
会被替换成jsx
(源码地址)方法。
import React from 'react'; // 在17版本中,可以不引入这句
function App() {
return <h1>Hello World</h1>;
}
// createElement
function App() {
return React.createElement('h1', null, 'Hello world');
}
// 17版本中的jsx
import {jsx as _jsx} from 'react/jsx-runtime'; // 由编译器引入
function App() {
// 子元素将直接编译成config对象里的children属性,jsx不再接收单独的子元素入参
return _jsx('h1', { children: 'Hello world' });
}
复制代码
ReactElement
我们来看看ReactElement
方法做了哪些事,源码地址。
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 内置属性赋值
type: type,
key: key,
ref: ref,
props: props,
// 记录创造该元素的组件
_owner: owner,
};
//
if (__DEV__) {
// 省略这些不必要的代码
}
return element;
};
复制代码
ReactElement
方法也很简单,其实就是通过这些传入的参数,创建了一个对象,并把它返回出来。这个ReactElement
对象实例就是createElement
方法最终返回出的内容,它就是React
虚拟DOM
的一个节点。虚拟DOM
其实本质上就是一个存储了很多用来描述DOM
的属性的对象。
这里你要注意,因为每个节点都会被createElement
方法调用,所以最终返回回来的应该是一个虚拟DOM
的树。
const App = (
<div className="App">
<h2 className="title">title</h2>
<p className="text">text</p>
</div>
);
console.log(App);
复制代码
如上图所示,所有的节点都会被编译成ReactElement
对象实例(虚拟DOM
)。
ReactDOM.render
虚拟DOM
有了,但我们最终的目的还是要把内容渲染到页面上,所以我们还需要通过ReactDOM.render
方法把虚拟DOM
渲染成真实的DOM
。这一块内容比较多,我会一个函数一个函数的分开讨论。
关于为什么要使用虚拟DOM
,虚拟DOM
有哪些优势,这些内容不在本文的讨论范围内容,网上相关的文章很多,大家可以自行去了解一下。
三种React
启动方式
在React
的16版本以及17版本中,一直都有三种启动方式
legacy
模式,ReactDOM.render(<App />, rootNode)
。目前常用的模式,渲染过程是同步的blocking
模式,ReactDOM.createBlockingRoot(rootNode).render(<App />)
。过渡的模式,基本很少用concurrent
模式,ReactDOM.createRoot(rootNode).render(<App />)
。异步渲染的模式,还可以使用一些新的特性,目前还在实验中。 官方文档
我们是解析ReactDOM.render
的渲染流程,所以其实分析的是一个同步的流程。关于concurrent
的异步渲染流程,时间分片以及优先级,其实也是在同步渲染的基础上做的一些修改,这些内容我会在本系列后续的文章中再总结。
虽然说是一个同步的流程,但是在React 16
版本的时候,已经把整个的渲染链路重构成了Fiber
的结构。Fiber
架构在 React
中并不能够和异步渲染画严格的等号,它是一种同时兼容了同步渲染与异步渲染的设计。
首次render
的三个阶段
ReactDOM.render
方法对应的调用栈很深,涉及到的函数方法也很多,不过我们可以只需要看一些关键的逻辑,了解大致的流程即可。
首次render
可以大体上分成三个阶段
- 初始化阶段,完成
Fiber
树中基本实体的创建。从调用ReactDOM.render
开始,到scheduleUpdateOnFiber
方法调用performSyncWorkOnRoot
结束。 - render阶段,构建和完善
Fiber
树。从performSyncWorkOnRoot
方法开始,到commitRoot
方法结束。 - commit阶段, 遍历
Fiber
树,把Fiber
节点映射为DOM
节点并渲染到页面上。从commitRoot
方法开始,到渲染结束。
上面几个方法现在看不懂没关系,接下来会一步步带你了解。只需要有个大致的印象,知道这三个阶段分别完成的事情。
初始化阶段
现在我们开始初始化阶段,在这个阶段,上文中已经说,主要就是完成Fiber
树中基本实体的创建。但是我们需要知道什么是基本实体?有哪些?我们从源码中去寻找答案。
legacyRenderSubtreeIntoContainer
我们只看关键的逻辑,我们先来看看ReactDOM.render
中调用的legacyRenderSubtreeIntoContainer
方法(源码地址)。
// ReactDOM.render中的调用
return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
// legacyRenderSubtreeIntoContainer源码
function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
// container 对应的是我们传入的真实 DOM 对象
var root = container._reactRootContainer;
// 初始化 fiberRoot 对象
var fiberRoot;
// DOM 对象本身不存在 _reactRootContainer 属性,因此 root 为空
if (!root) {
// 若 root 为空,则初始化 _reactRootContainer,并将其值赋值给 root
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
// legacyCreateRootFromDOMContainer 创建出的对象会有一个 _internalRoot 属性,将其赋值给 fiberRoot
fiberRoot = root._internalRoot;
// 这里处理的是 ReactDOM.render 入参中的回调函数,你了解即可
if (typeof callback === 'function') {
var originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
} // Initial mount should not be batched.
// 进入 unbatchedUpdates 方法
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// else 逻辑处理的是非首次渲染的情况(即更新),其逻辑除了跳过了初始化工作,与楼上基本一致
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
var _originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
_originalCallback.call(instance);
};
} // Update
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
复制代码
这个函数主要做了下面几步
- 1.调用
legacyCreateRootFromDOMContainer
方法创建了container._reactRootContainer
并赋值给root
- 2.将
root
的_internalRoot
属性赋值给fiberRoot
- 3.将
fiberRoot
与一些其他参数传入updateContainer
方法 - 4.把
updateContainer
的回调内容作为参数传入unbatchedUpdates
方法
这里的fiberRoot
的本质是一个FiberRootNode
对象,它的关联对象是真实DOM
的容器节点,这个对象里有一个current
对象
如上图,这个
current
对象是一个FiberNode
实例,其实它就是一个Fiber
节点,而且她还是当前Fiber
树的头部节点。fiberRoot
和它下面的current
对象这两个节点,将是后续整棵Fiber
树构建的起点。
unbatchedUpdates
接下来我们看看unbatchedUpdates
方法(源码地址)。
function unbatchedUpdates(fn, a) {
// 这里是对上下文的处理,不必纠结
var prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
// 重点在这里,直接调用了传入的回调函数 fn,对应当前链路中的 updateContainer 方法
return fn(a);
} finally {
// finally 逻辑里是对回调队列的处理,此处不用太关注
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
复制代码
这个方法比较简单,其实就是直接调用了传入的回调函数fn。而fn,是在legacyRenderSubtreeIntoContainer
中传入的
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
复制代码
所以我们再来看updateContainer
方法
updateContainer
先来看源码(源码地址),我会删除很多无关的逻辑。
function updateContainer(element, container, parentComponent, callback) {
// 这个 current 就是之前说的当前`Fiber`树的头部节点
const current = container.current;
// 这是一个 event 相关的入参,此处不必关注
var eventTime = requestEventTime();
// 这是一个比较关键的入参,lane 表示优先级
var lane = requestUpdateLane(current);
// 结合 lane(优先级)信息,创建 update 对象,一个 update 对象意味着一个更新
var update = createUpdate(eventTime, lane);
// update 的 payload 对应的是一个 React 元素
update.payload = {
element: element
};
// 处理 callback,这个 callback 其实就是我们调用 ReactDOM.render 时传入的 callback
callback = callback === undefined ? null : callback;
if (callback !== null) {
{
if (typeof callback !== 'function') {
error('render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback);
}
}
update.callback = callback;
}
// 将 update 入队
enqueueUpdate(current, update);
// 调度 fiberRoot
scheduleUpdateOnFiber(current, lane, eventTime);
// 返回当前节点(fiberRoot)的优先级
return lane;
}
复制代码
这个方法里的逻辑有点复杂,总的来说可以分为三点
- 1.请求当前
Fiber
节点的lane
(优先级) - 2.结合
lane
(优先级),创建当前Fiber
节点的update
对象,并将其入队 - 3.调度当前节点(
rootFiber
)进行更新
不过因为本文讲解的首次渲染链路是同步的,优先级意义不大,所以我们可以直接看看调度节点的方法scheduleUpdateOnFiber
。
scheduleUpdateOnFiber
这个方法内容有点长,我只列出关键逻辑(源码地址)。
// 如果是同步的渲染,将进入这个条件。如果是异步渲染的模式,将进入它的else逻辑中
// React 是通过 fiber.mode 来区分不同的渲染模式
if (lane === SyncLane) {
if (
// 判断当前是否运行在 unbatchedUpdates 方法里
(executionContext & LegacyUnbatchedContext) !== NoContext &&
// 判断当前是否已经 render
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
schedulePendingInteractions(root, lane);
// 我们要关注的关键步骤,从这个方法开始。开启 render 阶段
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
复制代码
在之前的步骤中,React
已经完成Fiber
树中基本实体的创建,其实就是之前几节说的fiberRoot
和它下面的current
对象这两个节点。在这个方法中,我们只需要关注performSyncWorkOnRoot
方法,从它开始,我们将进入render
阶段。
render阶段
render
阶段要做的事情是构建和完善Fiber
树,其实就是以fiberRoot
和它下面的current
对象这两个节点为顶节点,不断的遍历,把他们的子元素的Fiber
树构建出来。
我们先来看performSyncWorkOnRoot
方法。
performSyncWorkOnRoot
这里重点看两个逻辑
exitStatus = renderRootSync(root, lanes);
...
commitRoot(root);
复制代码
renderRootSync
方法是render
阶段开始的标志,而下面的commitRoot
是commit
阶段开始的标志。我们先进入的是render
阶段,所以我们先看renderRootSync
里的流程。
这个方法里需要看两个逻辑
prepareFreshStack(root, lanes);
...
workLoopSync();
复制代码
我们先走prepareFreshStack
的流程,等它走完了,再进入workLoopSync
的遍历流程。
prepareFreshStack
的作用是重置一个新的堆栈环境,我们也只需要关注一个逻辑
workInProgress = createWorkInProgress(root.current, null);
复制代码
createWorkInProgress
是一个比较重要的方法,我们详细看一下。
createWorkInProgress
精简后的源码如下,源码地址
// 这里入参中的 current 传入的是现有树结构中的 rootFiber 对象
function createWorkInProgress(current, pendingProps) {
var workInProgress = current.alternate;
// ReactDOM.render 触发的首屏渲染将进入这个逻辑
if (workInProgress === null) {
// 这是需要你关注的第一个点,workInProgress 是 createFiber 方法的返回值
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
// 这是需要你关注的第二个点,workInProgress 的 alternate 将指向 current
workInProgress.alternate = current;
// 这是需要你关注的第三个点,current 的 alternate 将反过来指向 workInProgress
current.alternate = workInProgress;
} else {
// else 的逻辑此处先不用关注
}
// 以下省略大量 workInProgress 对象的属性处理逻辑
// 返回 workInProgress 节点
return workInProgress;
}
复制代码
这里事先说明一下,入参current
就是之前的fiberRoot
对象下的current
对象。
总结一下createWorkInProgress
方法做的事情
- 1.调用
createFiber
,workInProgress
是createFiber
方法的返回值 - 2.把
workInProgress
的alternate
将指向current
- 3.把
current
的alternate
将反过来指向workInProgress
- 4.最终返回一个
workInProgress
节点
这里的createFiber
方法,顾名思义,就是用来创建一个Fiber
节点的方法。入参都是current
的值,所以,workInProgress
节点其实就是current
节点的副本。这时候整颗树的结构应该如下所示:
workInProgress
树顶点创建完成了,现在运行之前renderRootSync
方法里第二个关键逻辑workLoopSync
。
workLoopSync
这个方法很简单,就是个遍历的功能
function workLoopSync() {
// 若 workInProgress 不为空
while (workInProgress !== null) {
// 针对它执行 performUnitOfWork 方法
performUnitOfWork(workInProgress);
}
}
复制代码
因为后面列出的方法,都是workLoopSync
中不断遍历的,所以在解析performUnitOfWork
方法及其子方法之前,我要先对整个遍历的流程做一个大致的总结,有了一个大致的了解之后再去分析里面的方法。
workLoopSync
做的事情就是通过while
循环反复判断workInProgress
是否为空,并在不为空的情况下针对它执行performUnitOfWork
函数。而 performUnitOfWork
函数将触发beginWork
的调用,创建新的Fiber
节点。若beginWork
所创建的Fiber
节点不为空,则performUniOfWork
会用这个新的Fiber
节点来更新workInProgress
的值,为下一次循环做准备。
当workInProgress
为空时,意味着已经完成对整棵Fiber
树的构建。
在这个过程中,每一个被创建出来的新Fiber
节点,都会挂载为之前的workInProgress
树的后代节点。我们一步步来看一下。
performUnitOfWork
next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
复制代码
performUnitOfWork
里其实存在有两个流程,一个是beginWork
流程(创建新的Fiber
节点),还有一个completeWork
流程(当beginWork
遍历到当前分支的叶子节点时,next === null,运行completeWork
流程),来负责处理Fiber
节点到DOM
节点的映射逻辑。
我们先来看beginWork
流程
beginWork
beginWork
代码有400多行,实在太多了,只取一些关键逻辑。源码地址
function beginWork(current, workInProgress, renderLanes) {
......
// current 节点不为空的情况下,会加一道辨识,看看是否有更新逻辑要处理
if (current !== null) {
// 获取新旧 props
var oldProps = current.memoizedProps;
var newProps = workInProgress.pendingProps;
// 若 props 更新或者上下文改变,则认为需要"接受更新"
if (oldProps !== newProps || hasContextChanged() || (
workInProgress.type !== current.type )) {
// 打个更新标
didReceiveUpdate = true;
} else if (xxx) {
// 不需要更新的情况 A
return A
} else {
if (需要更新的情况 B) {
didReceiveUpdate = true;
} else {
// 不需要更新的其他情况,这里我们的首次渲染就将执行到这一行的逻辑
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
}
......
// 这坨 switch 是 beginWork 中的核心逻辑,原有的代码量相当大
switch (workInProgress.tag) {
......
// 这里省略掉大量形如"case: xxx"的逻辑
// 根节点将进入这个逻辑
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes)
// dom 标签对应的节点将进入这个逻辑
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes)
// 文本节点将进入这个逻辑
case HostText:
return updateHostText(current, workInProgress)
......
// 这里省略掉大量形如"case: xxx"的逻辑
}
}
复制代码
beginWork
的核心逻辑是根据fiber
节点(workInProgress
树下的节点)的tag
属性(代表当前fiber
属于什么类型的标签)的不同,调用不同的节点创建函数。
这些节点创建函数,最终都会通过调用reconcileChildren
方法,生成当前节点的子节点。
reconcileChildren(beginWork流程)
这个方法也比较简单
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
// 判断 current 是否为 null
if (current === null) {
// 若 current 为 null,则进入 mountChildFibers 的逻辑
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
// 若 current 不为 null,则进入 reconcileChildFibers 的逻辑
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
}
}
复制代码
上面的两个方法,我们也可以找到赋值的地方
var reconcileChildFibers = ChildReconciler(true);
var mountChildFibers = ChildReconciler(false);
复制代码
这两个方法都是通过ChildReconciler
方法创建出来的,只是入参有所区别
ChildReconciler(beginWork流程)
ChildReconciler
的代码量也很大,代码就不放了,源码地址。
这个方法里包含了很多关于Fiber
节点的创建、增加、删除、修改等操作的函数,用来给其他函数调用。返回值是一个名为reconcileChildFibers
的函数,这个函数是一个逻辑分发器,它将根据入参的不同,执行不同的Fiber
节点操作,最终返回不同的目标Fiber
节点。
还有一个很重要的逻辑,这个方法会根据入参shouldTrackSideEffects
来决定“是否需要追踪副作用”,reconcileChildFibers
和mountChildFibers
的不同,主要在于对副作用的处理不同。shouldTrackSideEffects
为true
的话,会给新创建的这个Fiber
节点添加一个flags
属性(17版本之前,这个属性名是effectTag
),并赋值一个常量。
如果是根节点,会赋值一个Placement
常量,这是一个二进制常量,目的是在渲染真实DOM
的时候告诉渲染器,处理这个fiber
节点时是需要新增DOM节点的。
这种类型的常量还有很多,源码地址。
这里先给一个demo,后续的编译都会以这个demo来实现。
function App() {
return (
<div className="App">
<div className="container">
<h1>我是标题</h1>
<p>我是第一段话</p>
<p>我是第二段话</p>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
复制代码
回到我们刚才的渲染链路中来,因为本次循环是第一次,处理的是current
树和workInProgress
树的顶部节点,所以current
是存在的,会进入reconcileChildFibers
方法中,它是允许追踪副作用的。因为当前的workInProgress
是顶部节点,它是没有一个确切的ReactElement
与之映射,所以它会作为是JSX
中根组件的父节点,就是这个App
组件的父节点。然后会基于App
组件的ReactElement
(jsx
编译后的虚拟DOM
)对象信息,创建其对应的FiberNode
,并给它打上Placement
(新增)的副作用标记, 返回给workInProgress.child
。
这样就将JSX
根组件的Fiber
与之前创建的Fiber
树顶点关联起来了,如下图。
这样,第一次循环完成,因为App
还有子元素,所以beginWork
中返回的workInProgress
不为null(workInProgress
其实就是这些jsx
节点编译之后的fiber
节点),workLoopSync
还会继续循环。最终的树结构如下
我们来看看这些标签的fiber
节点对象。
上图分别是App
节点,两个div子节点,以及p标签的节点。可以看到,每一个非文本类型的ReactElement
都有了它对应的Fiber
节点。
这些节点之间都是相互有联系的,它们是通过child
、return
、sibling
这 3 个属性建立关系,其中 child
、return
记录的是父子节点关系,而sibling
记录的则是兄弟节点关系(sibling 指向的是当前节点的第 1 个兄弟节点)。
具体看下图:
以上便是workInProgress Fiber
树的最终形态了。从图中可以看出,虽然人们习惯上仍然将眼前的这个产物称为Fiber
树,但它的数据结构本质其实已经从树变成了链表。
下面来看另一个completeWork
流程。
completeUnitOfWork(completeWork流程)
上文曾经说过,performUnitOfWork
中在beginWork
流程遍历到叶子节点之后,next
就会变成null,当次beginWork
流程结束,进入相对应的``completeWork`流程。
重新引用一下上面用到的performUnitOfWork
方法里的代码
next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
复制代码
completeUnitOfWork
是一个遍历循环的方法,将会遍历循环下面几件事
- 1.调用
completeWork
方法 - 2.将当前节点的副作用链(
EffectList
)插入到其父节点的副作用链(EffectList
)中 - 3.以当前节点为起点,循环遍历其兄弟节点及其父节点。当遍历到兄弟节点时,将
return
掉当前调用,触发兄弟节点对应的performUnitOfWork
逻辑;而遍历到父节点时,则会直接进入下一轮循环,也就是重复 1、2 的逻辑
我们先来看completeWork
方法。
completeWork(completeWork流程)
completeWork
也是一个体量比较大的函数,我们只抽离关键的逻辑
function completeWork(current, workInProgress, renderLanes) {
// 取出 Fiber 节点的属性值,存储在 newProps 里
var newProps = workInProgress.pendingProps;
// 根据 workInProgress 节点的 tag 属性的不同,决定要进入哪段逻辑
switch (workInProgress.tag) {
......
// h1 节点的类型属于 HostComponent,因此这里为你讲解的是这段逻辑
case HostComponent:
{
popHostContext(workInProgress);
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;
// 判断 current 节点是否存在,因为目前是挂载阶段,因此 current 节点是不存在的
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// 针对异常情况进行 return 处理
......
// 接下来就为 DOM 节点的创建做准备了
var currentHostContext = getHostContext();
// _wasHydrated 是一个与服务端渲染有关的值,这里不用关注
var _wasHydrated = popHydrationState(workInProgress);
// 判断是否是服务端渲染
if (_wasHydrated) {
......
} else {
// 这一步很关键, createInstance 的作用是创建 DOM 节点
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
// appendAllChildren 会尝试把上一步创建好的 DOM 节点挂载到 DOM 树上去
appendAllChildren(instance, workInProgress, false, false);
// stateNode 用于存储当前 Fiber 节点对应的 DOM 节点
workInProgress.stateNode = instance;
// finalizeInitialChildren 用来为 DOM 节点设置属性
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
markUpdate(workInProgress);
}
}
......
}
return null;
}
case HostText:
{
......
}
case SuspenseComponent:
{
......
}
case HostPortal:
......
return null;
case ContextProvider:
......
return null;
......
}
}
复制代码
首先我们需要知道,进入这个completeWork
的参数是什么,我们知道只有当beginWork
结束,也就是遍历到第一个叶子节点的时候,才会进入completeWork
方法。所以,第一次运行的时候的参数,其实是demo中的h1
标签相对应的fiber
节点对象。这也是completeWork
的一个特点,是严格自底向上运行的。
然后我们再来看completeWork
方法几个功能要点
- 1.
completeWork
的核心逻辑是一段体量巨大的switch
语句,在这段switch
语句中,completeWork
将根据workInProgress
节点的tag
属性的不同,进入不同的DOM
节点的创建、处理逻辑。 - 2.在
Demo
示例中,h1
节点的tag
属性对应的类型应该是HostComponent
,也就是原生DOM
元素类型。 - 3.
completeWork
中的current
、workInProgress
就是之前说的current
树和workInProgress
树上面的节点。
其中workInProgress
树代表的是“当前正在render
中的树”,而current
树则代表“已经存在的树”。
workInProgress
节点和current
节点之间用alternate
属性相互连接。在组件的挂载阶段,current
树只有一个顶部节点,并没有其他内容。因此h1
这个workInProgress
节点对应的current
节点是null
。
带着这个前提,我们再来看看completeWork
方法,我们可以总结出
completeWork
其实就是负责处理Fiber
节点到DOM
节点的映射逻辑。通过三个步骤
- 1.创建
DOM
节点(CreateInstance
) - 2.将
DOM
节点插入到 DOM 树中(AppendAllChildren
),赋值给workInProgress
节点的stateNode
属性(而且当前节点运行AppendAllChildren时,会逐个向下查找自己的后代子 Fiber 节点,并把所对应的 DOM 节点挂载到其父 Fiber 节点所对应的 DOM 节点里去,所以最上级的节点里的stateNode属性,就是一个完整的dom树) - 3.为
DOM
节点设置属性(FinalizeInitialChildren
)
completeUnitOfWork第2,3步(completeWork流程)
先来看第三步的代码实现
以当前节点为起点,循环遍历其兄弟节点及其父节点。当遍历到兄弟节点时,将return
掉当前调用,触发兄弟节点对应的performUnitOfWork
逻辑;而遍历到父节点时,则会直接进入下一轮循环,也就是重复 1、2 的逻辑
do {
......
// 这里省略步骤 1 和步骤 2 的逻辑
// 获取当前节点的兄弟节点
var siblingFiber = completedWork.sibling;
// 若兄弟节点存在
if (siblingFiber !== null) {
// 将 workInProgress 赋值为当前节点的兄弟节点
workInProgress = siblingFiber;
// 将正在进行的 completeUnitOfWork 逻辑 return 掉
return;
}
// 若兄弟节点不存在,completeWork 会被赋值为 returnFiber,也就是当前节点的父节点
completedWork = returnFiber;
// 这一步与上一步是相辅相成的,上下文中要求 workInProgress 与 completedWork 保持一致
workInProgress = completedWork;
} while (completedWork !== null);
复制代码
功能比较简单,按demo来说,因为beginWork
流程是一个深度优先遍历,当遍历到h1
标签时,遍历中断,开始执行completedWork
流程。h1
的兄弟节点p
标签,其实连beginWork
流程还没有运行过,所以需要重新调用performUnitOfWork
逻辑。
我们再来说一下第二步
将当前节点的副作用链(EffectList
)插入到其父节点的副作用链(EffectList
)中。
这一步的目标其实就是找出界面中需要处理的更新。因为在实际的操作中,并不是所有的节点上都会产生需要处理的更新。比如在挂载阶段,对整棵workInProgress
树递归完毕后,React
会发现实际只需要对App
节点执行一个挂载操作就可以了;而在更新阶段,这种现象更为明显。
怎样做才能让渲染器又快又好地定位到那些真正需要更新的节点呢?这就是副作用链(effectList
)的功能。
每个Fiber
节点都维护着一个属于它自己的effectList
,effectList
在数据结构上以链表的形式存在,链表内的每一个元素都是一个Fiber
节点。这些Fiber
节点需要满足两个共性:
- 都是当前
Fiber
节点的后代节点(并非它自身的更新,而是其需要更新的后代节点) - 都有待处理的副作用
这个effectList
链表在Fiber
节点中是通过firstEffect
和lastEffect
来维护。firstEffect
表示effectList
的第一个节点,而lastEffect
则记录最后一个节点。
因为completeWork
是自底向上执行的,所以在顶部节点上可以拿到一个存储了当前Fiber
树所有effect Fiber
。
按demo来说,只有顶部的节点才会存在副作用链(App组件的fiber
节点),对于App
组件内的所有子节点都不存在副作用链。当首次渲染或者更新的时候,渲染器只会去处理副作用链上的App fiber
节点(App
作为一个最小的更新组件,已经包含了内部子元素的dom节点)。当然如果App
里面还引用了其他组件,App
组件的fiber
中也会包含该组件的副作用链。
commit阶段
commit
会在performSyncWorkOnRoot
中被调用,它是一个绝对同步的过程。
commitRoot(root);
复制代码
从流程上来说,commi
共分为 3 个阶段:before mutation
、mutation
、layout
。
-
before mutation
阶段,这个阶段DOM
节点还没有被渲染到界面上去,过程中会触发getSnapshotBeforeUpdate
,也会处理useEffect
钩子相关的调度逻辑。 -
mutation
,这个阶段负责DOM
节点的渲染。在渲染过程中,会遍历effectList
,根据flags(effectTag)
的不同,执行不同的DOM
操作。 -
layout
,这个阶段处理DOM
渲染完毕之后的收尾逻辑。比如调用componentDidMount/componentDidUpdate
,调用useLayoutEffect
钩子函数的回调等。除了这些之外,它还会把fiberRoot
的current
指针指向workInProgress Fiber
树。
感谢
如果本文对你有所帮助,请帮忙点个赞,感谢!