读前须知
之前写了五篇关于React的渲染过程的阅读流程,发现其实很多事情都写得比较青涩难懂,当然也可能是我的写作水平问题,中间其实也没有去说一些生命周期的事情。所以将会用一篇比较长的总结文章去说明React16.7.0的代码流程。
个人建议不要单纯的看,结合源码一起看,会比较容易了解到里面的原理和意思。
之前的几篇文章链接:
- 小前端读源码 - React16.7.0(一) —— ReactElement的创建过程
- 小前端读源码 - React16.7.0(二) —— 创建根Fiber过程
- 小前端读源码 - React16.7.0(三) —— render阶段:Fiber树创建过程1
- 小前端读源码 - React16.7.0(四) —— render阶段:Fiber树创建过程2
- 小前端读源码 - React16.7.0(五) —— commit阶段:渲染DOM
基本概念:
- ReactElement
- Fiber
- current树和workInProgress树
什么是ReactElement?
ReactElement是通过babel编译后转换成的react.createElement调用后返回出来的数据结构。
通过$$typeof去标识为一个React元素,并且将对应的props,key,ref,添加上去,最后会以type描述该元素。这些都是通过react.createElement传入的参数决定的。
什么是Fiber节点?
从版本 16 开始, React 推出了内部实例树的新的实现方法,以及被称之为Fiber的算法。这是一种全新的虚拟DOM的实现。
出现它的原因是为17版本的异步渲染机制如果使用以前旧的虚拟DOM的方式无法实现。然后Fiber的出现彻底的改变了整个React的底层架构,所以以前我们所知道的React的很多原理都已经不再一样了。
在React中每一个组件都会先经过react.createElement转换成一个ReactElement,在通过对每一个组件的ReactElement生成一个对应的Fiber节点(包括根节点)。而这些节点将会以链表的方式存放在根Fiber下,形成一个Fiber节点树。
React内有两颗树
刚噶所有说到的,React内部会对每个ReactElement创建一个Fiber节点,从而形成一个Fiber树,而Fiber树分为两种,一种是代表当前页面中每个组件状态的树,我们称为current树,而另外一棵树是在更新节点的时候,React会根据当前的current树,以及一些修改过的参数生成一个workInProgress树,最终React会将workInProgress树渲染到页面中,并且在渲染后,workInProgress树就会变成current树。简单来说,current树是代表当前页面中组件的状态,而workInProgress树是代表之后需要渲染的组件状态。
Fiber生成流程
以下是一个DEMO,之后所有的内容都是基于这个DEMO去展开。
代码语言:javascript复制import React from 'react';
import ReactDOM from 'react-dom';
class Component1 extends React.Component {
render() {
return (
<div>
<p>text1</p>
<p>text2</p>
</div>
)
}
}
class Component2 extends React.Component {
static defaultProps = {
text: 'component2'
}
render() {
return (<p>{this.props.text}</p>)
}
}
class App extends React.Component {
constructor() {
super();
this.state = {
data: 1
}
}
render() {
return (
<div>
<button onClick={() => {this.setState({data: 2})}}>setState</button>
<Component1/>
<Component2/>
</div>
)
}
}
ReactDOM.render(
<App/>,
document.getElementById('root')
)
首先经过JSX编译器后,每一个组件都会变成一个ReactElement。当然子组件并非一开始执行react.createElement的,只有根元素会在一开始传入ReactDOM.render的时候先转换成ReactElement对象。
- ReactDOM.render
- legacyRenderSubtreeIntoContainer
- legacyCreateRootFromDOMContainer
通过以上的函数创建了根Fiber节点。
代码语言:javascript复制container是传入的根元素的DOM对象。
var root = container._reactRootContainer;
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
container是DOM对象,可以通过全局查找到容器DOM对象的_reactRootContainer获取到Fiber树(current树)。 root变量中带有一个_internalRoot属性,同样指向Fiber树(current树)
React的渲染流程可以分为两个主要的阶段。
- render阶段
- commit阶段
render阶段
render阶段开始是从renderRoot函数开始。在这个阶段之前,其实都是对根Fiber节点的一些初始化。但我觉得需要说明一下这个根Fiber节点的一个内容就是现在的根Fiber里面有一个updateQueue属性,里面包含了一下属性:
我们发现在根Fiber中的任务中firstUpdate中的element就是App的ReactElement对象。之后的workLoop就是通过获取这个属性来绑定根Fiber节点和App的Fiber节点的关系。
那么简单说一下这个updateQueue是哪里来的。
- root.render
- updateContainer
- updateContainerAtExpirationTime
- scheduleRootUpdate
- update = createUpdate(创建了一个update对象)
- update.payload = { element: element };
- enqueueUpdate(在这里将update对象赋值到根Fiber的updateQueue中)
可以参考我的第二篇React源码阅读的文章。
Lam:小前端读源码 - React16.7.0(二)
在React的渲染过程中,整个Fiber树是由一个workLoop函数循环构建出来的。代码如下:
在workLoop中主要经过几个关键的步骤:
- performUnitOfWork
- beginWork - 根据不同的类型的Fiber进行不同的操作
- completeUnitOfWork
- completeWork
之前说过根Fiber有一个比较特殊的updateQueue属性,其实就是在beginWork中判断到是根Fiber节点的话,会将updateQueue中的firstUpdate.element转换为Fiber节点,并赋值到根Fiber节点的child中。(App组件)
当前的组件结构:
以下是整个workLoop的渲染流程。
我们先看看performUnitOfWork和beginWork这两个函数:
代码语言:javascript复制function performUnitOfWork(workInProgress) {
// 省略代码....
next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);
// 省略代码....
if (next === null) {
next = completeUnitOfWork(workInProgress);
}
// 省略代码....
return next;
}
function beginWork(current$$1, workInProgress, renderExpirationTime) {
// 省略代码....
switch (workInProgress.tag) {
case IndeterminateComponent:
{
var elementType = workInProgress.elementType;
return mountIndeterminateComponent(current$$1, workInProgress, elementType, renderExpirationTime);
}
case LazyComponent:
{
var _elementType = workInProgress.elementType;
return mountLazyComponent(current$$1, workInProgress, _elementType, updateExpirationTime, renderExpirationTime);
}
case FunctionComponent:
{
var _Component = workInProgress.type;
var unresolvedProps = workInProgress.pendingProps;
var resolvedProps = workInProgress.elementType === _Component ? unresolvedProps : resolveDefaultProps(_Component, unresolvedProps);
return updateFunctionComponent(current$$1, workInProgress, _Component, resolvedProps, renderExpirationTime);
}
case ClassComponent:
{
var _Component2 = workInProgress.type;
var _unresolvedProps = workInProgress.pendingProps;
var _resolvedProps = workInProgress.elementType === _Component2 ? _unresolvedProps : resolveDefaultProps(_Component2, _unresolvedProps);
return updateClassComponent(current$$1, workInProgress, _Component2, _resolvedProps, renderExpirationTime);
}
case HostRoot:
return updateHostRoot(current$$1, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current$$1, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current$$1, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current$$1, workInProgress, renderExpirationTime);
case HostPortal:
return updatePortalComponent(current$$1, workInProgress, renderExpirationTime);
case ForwardRef:
{
var type = workInProgress.type;
var _unresolvedProps2 = workInProgress.pendingProps;
var _resolvedProps2 = workInProgress.elementType === type ? _unresolvedProps2 : resolveDefaultProps(type, _unresolvedProps2);
return updateForwardRef(current$$1, workInProgress, type, _resolvedProps2, renderExpirationTime);
}
case Fragment:
return updateFragment(current$$1, workInProgress, renderExpirationTime);
case Mode:
return updateMode(current$$1, workInProgress, renderExpirationTime);
case Profiler:
return updateProfiler(current$$1, workInProgress, renderExpirationTime);
case ContextProvider:
return updateContextProvider(current$$1, workInProgress, renderExpirationTime);
case ContextConsumer:
return updateContextConsumer(current$$1, workInProgress, renderExpirationTime);
case MemoComponent:
{
var _type2 = workInProgress.type;
var _unresolvedProps3 = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
var _resolvedProps3 = resolveDefaultProps(_type2, _unresolvedProps3);
{
if (workInProgress.type !== workInProgress.elementType) {
var outerPropTypes = _type2.propTypes;
if (outerPropTypes) {
checkPropTypes_1(outerPropTypes, _resolvedProps3, // Resolved for outer only
'prop', getComponentName(_type2), getCurrentFiberStackInDev);
}
}
}
_resolvedProps3 = resolveDefaultProps(_type2.type, _resolvedProps3);
return updateMemoComponent(current$$1, workInProgress, _type2, _resolvedProps3, updateExpirationTime, renderExpirationTime);
}
case SimpleMemoComponent:
{
return updateSimpleMemoComponent(current$$1, workInProgress, workInProgress.type, workInProgress.pendingProps, updateExpirationTime, renderExpirationTime);
}
case IncompleteClassComponent:
{
var _Component3 = workInProgress.type;
var _unresolvedProps4 = workInProgress.pendingProps;
var _resolvedProps4 = workInProgress.elementType === _Component3 ? _unresolvedProps4 : resolveDefaultProps(_Component3, _unresolvedProps4);
return mountIncompleteClassComponent(current$$1, workInProgress, _Component3, _resolvedProps4, renderExpirationTime);
}
default:
invariant(false, 'Unknown unit of work tag. This error is likely caused by a bug in React. Please file an issue.');
}
}
performUnitOfWork从beginWork中接受workInProgress树中的一个Fiber节点,然后将该Fiber节点传入beginWork函数中,最后将beginWork处理完后的Fiber节点的childFiber节点返回出去到workLoop中。
beginWork会根据不同的Fiber节点类型进行不同的处理。以DEMO为例,会使用到3种不同的FIber类型。
- HostRoot
- ClassComponent
- HostComponent
从刚刚的GIF中可以看到,React对于Fiber的构建是深度优先的。在beginWork中,根据不同的Fiber类型进行处理后,都会将处理后的FIber的child返回出来并赋值到next。如果当前的组件已经到达末尾,那么将会返回null。
如果在performUnitOfWork中得到beginWork函数返回是null,并将赋值到next中,将会执行completeUnitOfWork。代表当前的这个组件已经到达末尾了。
代码语言:javascript复制function completeUnitOfWork(workInProgress) {
while (true) {
// 省略代码....
var returnFiber = workInProgress.return;
var siblingFiber = workInProgress.sibling;
// 省略代码....
nextUnitOfWork = completeWork(current$$1, workInProgress, nextRenderExpirationTime);
// 省略代码....
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber. Complete the returnFiber.
workInProgress = returnFiber;
continue;
} else {
return null;
}
}
// 省略代码....
return null;
}
performUnitOfWork中会执行completeWork,但是这个函数我们放到之后再去说明。能进入到completeUnitOfWork函数中,就代表当前的Fiber节点已经没有任何子节点了。那么这个时候就会判断当前的Fiber节点是否存在兄弟节点,如果没有就会将当前workInProgress指向当前Fiber节点的return(当前Fiber节点的父级Fiber节点)。
- 有兄弟节点 —— 返回兄弟节点到workLoop重新进入循环
- 无兄弟节点 —— 修改workInProgress指向,指向到当前Fiber节点的父级Fiber节点,回到第一步。
- 到达根Fiber —— 根Fiber没有任何兄弟和父级,将会return null,结束workLoop,进入commit阶段。
Commit阶段
从何时进入commit阶段呢?在renderRoot函数执行完workLoop之后,返回到performWorkOnRoot函数中执行completeRoot开始。
现在在root对象中有两颗树,一颗是current树(当前展示界面的Fiber树),一颗是finishedWork树(也叫workInProgress树,此树的作用就是本次render出来的Fiber树)。
current树
workInProgress树
因为当前的第一次渲染,所以current树种的child是为null,而workInProgress树经过了刚刚的render阶段,建立的一颗本次渲染的Fiber树,所以child不为null。
代码语言:javascript复制function performWorkOnRoot(root, expirationTime, isYieldy) {
// 省略代码....
renderRoot(root, isYieldy);
finishedWork = root.finishedWork;
if (finishedWork !== null) {
// We've completed the root. Commit it.
completeRoot(root, finishedWork, expirationTime);
}
// 省略代码....
}
因为经过renderRoot函数的render阶段后,root的finishedWork是一个完整的Fiber树,并且会进入completeRoot函数中,现在开始就是进入commit阶段了。
commitRoot是commit阶段的重要函数,所有的生命周期和新增、删除和更新组件都是通过commitRoot函数内执行的。
因为是第一次渲染,所以简单说明一下第一次渲染的函数调用流程:
- commitRoot
- commitAllHostEffects
- commitPlacement
- appendChildToContainer
这时候就会在页面中渲染出真实的DOM。最终会在commitRoot中将workInProgress树赋值到root的current树中,至此完成第一次渲染。
需要补充一些非常重要的属性的说明:
- Fiber.firstEffect从哪里创建的?
- 什么时候创建DOM对象的?
- stateNode是什么?
- nextEffect是什么?
Fiber.firstEffect
大家还记得completeUnitOfWork函数吗,在while的循环中,其实不断对returnFiber.firstEffect引用指向当前执行的Fiber节点的.firstEffect。并且对returnFiber.firstEffect赋值为当前的Fiber节点。
代码语言:javascript复制function completeUnitOfWork(workInProgress) {
while (true) {
var returnFiber = workInProgress.return;
var siblingFiber = workInProgress.sibling;
if (returnFiber !== null && (returnFiber.effectTag & Incomplete) === NoEffect) {
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = workInProgress.firstEffect;
}
if (workInProgress.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
}
returnFiber.lastEffect = workInProgress.lastEffect;
}
var effectTag = workInProgress.effectTag;
// Skip both NoWork and PerformedWork tags when creating the effect list.
// PerformedWork effect is read by React DevTools but shouldn't be committed.
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress;
} else {
returnFiber.firstEffect = workInProgress;
}
returnFiber.lastEffect = workInProgress;
}
}
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber. Complete the returnFiber.
workInProgress = returnFiber;
continue;
} else {
// We've reached the root.
return null;
}
}
最终root.finishedWork.firstEffect指向的是App的Fiber节点。
什么时候创建DOM对象的?
当一个节点分支到了末尾时候,将会把当前的Fiber节点传入completeUnitOfWork中,调用顺序如下:
- completeUnitOfWork
- completeWork
- createInstance
- createElement
最终赋值到当前Fiber节点的stateNode属性中。举个例子,在DEMO中,button原生的Fiber节点最终经过上述的执行顺序后,DOM对象将存在于Fiber.stateNode中。
在completeWork调用完createInstance获取DOM对象后还调用了一个叫appendAllChildren的函数。
代码语言:javascript复制function (parent, workInProgress, needsVisibilityToggle, isHidden) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
var node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
};
- 当前Fiber没有child —— 跳出
- 当前Fiber有child —— 如果child是一个DOM对象,将append到当前Fiber的DOM对象中,否则尝试获取child的child是否存在,存在会一直循环下去直到child为null
- 当前child无兄弟节点 —— 跳出
- 当前child有兄弟节点 —— 回到第二步
最终的workInProgress树的stateNode结构:
stateNode是什么?
stateNode就是当前的FIber对应的实际DOM对象的一个应用。可以理解为当前Fiber渲染出来的DOM就是stateNode中的DOM对象。
nextEffect是什么?
nextEffect是一个全局变量,在函数commitRoot中会将当前的workInProgress树的firstEffect,也就是AppFiber赋值到全局的nextEffect中,并在commitAllHostEffects函数中获取并用作渲染。
接下来说说生命周期,因为React实现了新的Fiber架构,为了以后的异步渲染,有部分生命周期会被移除以及添加了新的生命周期。
在官网中也有介绍新的一些生命周期API和一些会废除的生命周期API。
React.Component – React
梳理了一下16.7版本推介使用的生命周期图:
下面我们来简单看看每个生命周期会在那些地方去触发的。
getDerivedStateFromProps
render阶段中,在beginWork中的执行mountClassInstance中会有这么一段代码:
代码语言:javascript复制var getDerivedStateFromProps = ctor.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, newProps);
instance.state = workInProgress.memoizedState;
}
applyDerivedStateFromProps代码如下:
代码语言:javascript复制function applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, nextProps) {
var prevState = workInProgress.memoizedState;
{
if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {
// Invoke the function an extra time to help detect side-effects.
getDerivedStateFromProps(nextProps, prevState);
}
}
var partialState = getDerivedStateFromProps(nextProps, prevState);
{
warnOnUndefinedDerivedState(ctor, partialState);
}
// Merge the partial state and the previous state.
// 对getDerivedStateFromProps返回的state和现在的state进行合并
var memoizedState = partialState === null || partialState === undefined ? prevState : _assign({}, prevState, partialState);
workInProgress.memoizedState = memoizedState;
// Once the update queue is empty, persist the derived state onto the
// base state.
var updateQueue = workInProgress.updateQueue;
if (updateQueue !== null && workInProgress.expirationTime === NoWork) {
updateQueue.baseState = memoizedState;
}
}
render
render阶段的最后一个生命周期API,要记住执行render并不代表已经渲染到真实的DOM,因为render生命周期只是代表render阶段结束而已。在beginWork函数中最终会调用finishClassComponent函数中,在finishClassComponent函数中会有以下的代码:
代码语言:javascript复制var instance = workInProgress.stateNode;
instance.render();
componentDidMount
commit阶段中,在调用commitAllHostEffects函数后将会渲染DOM完毕,在之后会调用commitAllLifeCycles函数,通过该函数会调用componentDidMount。
代码语言:javascript复制var instance = finishedWork.stateNode;
instance.componentDidMount();
从代码中可以看到,确实componentDidMount是能获取到这次更新的DOM节点。
shouldComponentUpdate
render阶段,在非第一次渲染的情况下,beginWork会调用updateClassInstance函数,并返回一个shouldUpdate 传入到finishClassComponent函数中。
代码语言:javascript复制shouldUpdate = updateClassInstance(current$$1, workInProgress, Component, nextProps, renderExpirationTime);
var nextUnitOfWork = finishClassComponent(current$$1, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime);
在finishClassComponent函数中会调用checkShouldComponentUpdate函数,返回一个布尔值。
代码语言:javascript复制function checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext) {
var instance = workInProgress.stateNode;
if (typeof instance.shouldComponentUpdate === 'function') {
startPhaseTimer(workInProgress, 'shouldComponentUpdate');
var shouldUpdate = instance.shouldComponentUpdate(newProps, newState, nextContext);
stopPhaseTimer();
{
!(shouldUpdate !== undefined) ? warningWithoutStack$1(false, '%s.shouldComponentUpdate(): Returned undefined instead of a ' 'boolean value. Make sure to return true or false.', getComponentName(ctor) || 'Component') : void 0;
}
return shouldUpdate;
}
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState);
}
return true;
}
从代码看到,默认会返回true,如果我们的instance中有shouldComponentUpdate将会返回执行后返回的布尔值。
在finishClassComponent函数中会接收一个shouldUpdate参数,会对该参数做判断。
代码语言:javascript复制if (!shouldUpdate && !didCaptureError) {
// Context providers should defer to sCU for rendering
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);
}
如果shouldUpdate为false,将不会往下执行并返回null,直接结束render阶段。因为经过Diff算法最终全局中的nextEffect为null,导致并不会执行commit阶段,所以也不会有commit阶段触发的生命周期。
getSnapshotBeforeUpdate
从生命周期的流程图可以看出,getSnapshotBeforeUpdate是属于在render生命周期执行后和真正渲染DOM之前的节点触发的,那就意味着getSnapshotBeforeUpdate生命周期还是可以获取到这次更新之前的DOM元素的。
在commit阶段是在commitRoot函数执行的时候开始的,在这个函数内,会执行3个函数,分别是:
- commitBeforeMutationLifecycles - commit阶段渲染DOM前的生命周期
- commitAllHostEffects - commit阶段渲染DOM
- commitAllLifeCycles - commit阶段渲染DOM后的生命周期
所以getSnapshotBeforeUpdate是在commitAllLifeCycles中执行的。
componentDidUpdate
componentDidUpdate和componentDidMount是一样的,都是在渲染完DOM后在commitAllLifeCycles函数中执行的,因为React会判断到这次并非第一次渲染,所以会执行componentDidUpdate而不是componentDidMount。
之后打算还会出以下几篇文章:
- setState源码阅读
- 合成事件源码阅读
- diff算法源码阅读
- key源码阅读
如果觉得这篇文章好不错,点个关注呗。
小前端的一点阅读记录,有不对的地方勿喷,请留言指导,非常感谢!