最近开始深刻学习React
的原理了,后面会出一系列关于React
原理的文章,基本都是我学习其余前辈的React
源码分析以及跟随他们阅读源码时的一些思考和记录,内容大部分非原创,但我会用我本身的方式去总结原理以及相关的流程,并加以补充,看成本身的学习总结。html
本系列内容偏向底层源码实现,若是你是React
新手,不建议你细看。react
本文的内容是关于React
首次渲染的流程。git
这里主要有两个步骤,第一个是从JSX
代码通过React.createElement
方法变成一个虚拟DOM
,第二个步骤是经过ReactDOM.render
方法把虚拟DOM
变成真实DOM
。github
对于 React.createElement
方法,不少新人可能并不知道,由于咱们在通常的业务逻辑中,比较少会直接使用这个方法。其实在React
官网中,就已经有写明了。数组
JSX 会被编译为 React.createElement(), React.createElement() 将返回一个叫做“React Element”的 JS 对象。babel
咱们在babel官网里能够写个JSX
试一下。 markdown
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,
);
}
复制代码
总结一下这个函数作的一些事情app
key
,ref
,self
,source
四个值(将key
字符串化,将config
中的ref
赋值给ref
,self
和source
暂不太清楚其功能,能够忽略,17版本的jsx方法直接删除了这两个参数)config
,筛选出能够赋值到props
里的属性props.children
中(若是只有一个子元素,就直接赋值,若是子元素大于一个,就以数组形式存储)defaultProps
(若是没有传入相关的props
,props
就取设置的默认值)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
方法作了哪些事,源码地址。
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
)。
虚拟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
结束。Fiber
树。从performSyncWorkOnRoot
方法开始,到commitRoot
方法结束。Fiber
树,把Fiber
节点映射为DOM
节点并渲染到页面上。从commitRoot
方法开始,到渲染结束。上面几个方法如今看不懂不要紧,接下来会一步步带你了解。只须要有个大体的印象,知道这三个阶段分别完成的事情。
如今咱们开始初始化阶段,在这个阶段,上文中已经说,主要就是完成Fiber
树中基本实体的建立。可是咱们须要知道什么是基本实体?有哪些?咱们从源码中去寻找答案。
咱们只看关键的逻辑,咱们先来看看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);
}
复制代码
这个函数主要作了下面几步
legacyCreateRootFromDOMContainer
方法建立了container._reactRootContainer
并赋值给root
root
的_internalRoot
属性赋值给fiberRoot
fiberRoot
与一些其余参数传入updateContainer
方法updateContainer
的回调内容做为参数传入unbatchedUpdates
方法这里的fiberRoot
的本质是一个FiberRootNode
对象,它的关联对象是真实DOM
的容器节点,这个对象里有一个current
对象 如上图,这个
current
对象是一个FiberNode
实例,其实它就是一个Fiber
节点,并且她仍是当前Fiber
树的头部节点。fiberRoot
和它下面的current
对象这两个节点,将是后续整棵Fiber
树构建的起点。
接下来咱们看看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
方法
先来看源码(源码地址),我会删除不少无关的逻辑。
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;
}
复制代码
这个方法里的逻辑有点复杂,总的来讲能够分为三点
Fiber
节点的lane
(优先级)lane
(优先级),建立当前Fiber
节点的update
对象,并将其入队rootFiber
)进行更新不过由于本文讲解的首次渲染链路是同步的,优先级意义不大,因此咱们能够直接看看调度节点的方法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
阶段要作的事情是构建和完善Fiber
树,其实就是以fiberRoot
和它下面的current
对象这两个节点为顶节点,不断的遍历,把他们的子元素的Fiber
树构建出来。
咱们先来看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
是一个比较重要的方法,咱们详细看一下。
精简后的源码以下,源码地址
// 这里入参中的 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
方法作的事情
createFiber
,workInProgress
是createFiber
方法的返回值workInProgress
的alternate
将指向current
current
的alternate
将反过来指向workInProgress
workInProgress
节点这里的createFiber
方法,顾名思义,就是用来建立一个Fiber
节点的方法。入参都是current
的值,因此,workInProgress
节点其实就是current
节点的副本。这时候整颗树的结构应该以下所示:
workInProgress
树顶点建立完成了,如今运行以前renderRootSync
方法里第二个关键逻辑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
树的后代节点。咱们一步步来看一下。
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
代码有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
方法,生成当前节点的子节点。
这个方法也比较简单
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
的代码量也很大,代码就不放了,源码地址。
这个方法里包含了不少关于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
流程。
上文曾经说过,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
是一个遍历循环的方法,将会遍历循环下面几件事
completeWork
方法EffectList
)插入到其父节点的反作用链(EffectList
)中return
掉当前调用,触发兄弟节点对应的performUnitOfWork
逻辑;而遍历到父节点时,则会直接进入下一轮循环,也就是重复 一、2 的逻辑咱们先来看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
方法几个功能要点
completeWork
的核心逻辑是一段体量巨大的switch
语句,在这段switch
语句中,completeWork
将根据workInProgress
节点的tag
属性的不一样,进入不一样的DOM
节点的建立、处理逻辑。Demo
示例中,h1
节点的tag
属性对应的类型应该是HostComponent
,也就是原生DOM
元素类型。completeWork
中的current
、workInProgress
就是以前说的current
树和workInProgress
树上面的节点。其中workInProgress
树表明的是“当前正在render
中的树”,而current
树则表明“已经存在的树”。
workInProgress
节点和current
节点之间用alternate
属性相互链接。在组件的挂载阶段,current
树只有一个顶部节点,并无其余内容。所以h1
这个workInProgress
节点对应的current
节点是null
。
带着这个前提,咱们再来看看completeWork
方法,咱们能够总结出
completeWork
其实就是负责处理Fiber
节点到DOM
节点的映射逻辑。经过三个步骤
DOM
节点(CreateInstance
)DOM
节点插入到 DOM 树中(AppendAllChildren
),赋值给workInProgress
节点的stateNode
属性(并且当前节点运行AppendAllChildren时,会逐个向下查找本身的后代子 Fiber 节点,并把所对应的 DOM 节点挂载到其父 Fiber 节点所对应的 DOM 节点里去,因此最上级的节点里的stateNode属性,就是一个完整的dom树)DOM
节点设置属性(FinalizeInitialChildren
)先来看第三步的代码实现
以当前节点为起点,循环遍历其兄弟节点及其父节点。当遍历到兄弟节点时,将return
掉当前调用,触发兄弟节点对应的performUnitOfWork
逻辑;而遍历到父节点时,则会直接进入下一轮循环,也就是重复 一、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
会在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
树。
若是本文对你有所帮助,请帮忙点个赞,感谢!