小前端读源码 - React16.7.0(渲染总结篇)

2022-09-26 10:38:39 浏览数 (1)

读前须知

之前写了五篇关于React的渲染过程的阅读流程,发现其实很多事情都写得比较青涩难懂,当然也可能是我的写作水平问题,中间其实也没有去说一些生命周期的事情。所以将会用一篇比较长的总结文章去说明React16.7.0的代码流程。

个人建议不要单纯的看,结合源码一起看,会比较容易了解到里面的原理和意思。

之前的几篇文章链接:

  1. 小前端读源码 - React16.7.0(一) —— ReactElement的创建过程
  2. 小前端读源码 - React16.7.0(二) —— 创建根Fiber过程
  3. 小前端读源码 - React16.7.0(三) —— render阶段:Fiber树创建过程1
  4. 小前端读源码 - React16.7.0(四) —— render阶段:Fiber树创建过程2
  5. 小前端读源码 - React16.7.0(五) —— commit阶段:渲染DOM

基本概念:

  1. ReactElement
  2. Fiber
  3. 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对象。

  1. ReactDOM.render
  2. legacyRenderSubtreeIntoContainer
  3. legacyCreateRootFromDOMContainer

通过以上的函数创建了根Fiber节点。

container是传入的根元素的DOM对象。

代码语言:javascript复制
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是哪里来的。

  1. root.render
  2. updateContainer
  3. updateContainerAtExpirationTime
  4. scheduleRootUpdate
  5. update = createUpdate(创建了一个update对象)
  6. update.payload = { element: element };
  7. enqueueUpdate(在这里将update对象赋值到根Fiber的updateQueue中)

可以参考我的第二篇React源码阅读的文章。

Lam:小前端读源码 - React16.7.0(二)

在React的渲染过程中,整个Fiber树是由一个workLoop函数循环构建出来的。代码如下:

在workLoop中主要经过几个关键的步骤:

  1. performUnitOfWork
  2. beginWork - 根据不同的类型的Fiber进行不同的操作
  3. completeUnitOfWork
  4. 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节点)。

  1. 有兄弟节点 —— 返回兄弟节点到workLoop重新进入循环
  2. 无兄弟节点 —— 修改workInProgress指向,指向到当前Fiber节点的父级Fiber节点,回到第一步。
  3. 到达根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函数内执行的。

因为是第一次渲染,所以简单说明一下第一次渲染的函数调用流程:

  1. commitRoot
  2. commitAllHostEffects
  3. commitPlacement
  4. 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中,调用顺序如下:

  1. completeUnitOfWork
  2. completeWork
  3. createInstance
  4. 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;
    }
  };
  1. 当前Fiber没有child —— 跳出
  2. 当前Fiber有child —— 如果child是一个DOM对象,将append到当前Fiber的DOM对象中,否则尝试获取child的child是否存在,存在会一直循环下去直到child为null
  3. 当前child无兄弟节点 —— 跳出
  4. 当前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个函数,分别是:

  1. commitBeforeMutationLifecycles - commit阶段渲染DOM前的生命周期
  2. commitAllHostEffects - commit阶段渲染DOM
  3. commitAllLifeCycles - commit阶段渲染DOM后的生命周期

所以getSnapshotBeforeUpdate是在commitAllLifeCycles中执行的。

componentDidUpdate

componentDidUpdate和componentDidMount是一样的,都是在渲染完DOM后在commitAllLifeCycles函数中执行的,因为React会判断到这次并非第一次渲染,所以会执行componentDidUpdate而不是componentDidMount。


之后打算还会出以下几篇文章:

  1. setState源码阅读
  2. 合成事件源码阅读
  3. diff算法源码阅读
  4. key源码阅读

如果觉得这篇文章好不错,点个关注呗。

小前端的一点阅读记录,有不对的地方勿喷,请留言指导,非常感谢!

0 人点赞