最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React 源码解析之协调过程(一)

    正文概述 掘金(Aaaaaaaaaaayou)   2021-02-16   647

    引言

    在React 源码解读之首次渲染流程中我们讲到了 React 在首次渲染过程(其实更新过程也一样)中存在 RenderCommit 两大阶段,其中 Render 阶段又可称为协调阶段,它包括 beginWorkcompleteWork,本文着重讲讲 beginWork

    beginWork

    beginWork 进行简化后,我们发现该函数可以分为两大部分,以 workInProgress.lanes = NoLanes; 为分界线,前面部分是关于复用 Fiber 节点的逻辑,即进入 bailout 流程,后面部分是关于更新当前 Fiber 节点的逻辑。

    function beginWork(
      current: Fiber | null,
      workInProgress: Fiber,
      renderLanes: Lanes,
    ): Fiber | null {
      const updateLanes = workInProgress.lanes;
    
      if (current !== null) {
        const oldProps = current.memoizedProps;
        const newProps = workInProgress.pendingProps;
    
        if (
          oldProps !== newProps ||
          hasLegacyContextChanged() ||
          // Force a re-render if the implementation changed due to hot reload:
          (__DEV__ ? workInProgress.type !== current.type : false)
        ) {
          // If props or context changed, mark the fiber as having performed work.
          // This may be unset if the props are determined to be equal later (memo).
          didReceiveUpdate = true;
        } else if (!includesSomeLane(renderLanes, updateLanes)) {
          didReceiveUpdate = false;
          // This fiber does not have any pending work. Bailout without entering
          // the begin phase. There's still some bookkeeping we that needs to be done
          // in this optimized path, mostly pushing stuff onto the stack.
          switch (workInProgress.tag) {
            case HostRoot:
              ...
            case HostComponent:
              ...
            case ClassComponent:
              ...
            case HostPortal:
              ...
          }
          return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
        } else {
          if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
            // This is a special case that only exists for legacy mode.
            // See https://github.com/facebook/react/pull/19216.
            didReceiveUpdate = true;
          } else {
            // An update was scheduled on this fiber, but there are no new props
            // nor legacy context. Set this to false. If an update queue or context
            // consumer produces a changed value, it will set this to true. Otherwise,
            // the component will assume the children have not changed and bail out.
            didReceiveUpdate = false;
          }
        }
      } else {
        didReceiveUpdate = false;
      }
    
      // Before entering the begin phase, clear pending update priority.
      // TODO: This assumes that we're about to evaluate the component and process
      // the update queue. However, there's an exception: SimpleMemoComponent
      // sometimes bails out later in the begin phase. This indicates that we should
      // move this assignment out of the common path and into each branch.
      workInProgress.lanes = NoLanes;
    
      switch (workInProgress.tag) {
        case IndeterminateComponent: {
          ...
        case LazyComponent: {
          ...
        case FunctionComponent: {
          ...
        case ClassComponent: {
          ...
        case HostRoot:
          ...
      }
    }
    

    这里的第一个问题是:bailout 是在干什么,有什么意义?

    bailout 的意义

    在React 源码解读之首次渲染流程中,我们通过一个动画分析了 React 首次渲染的 Render 过程,在 beginWork 阶段会重新构建一颗 Fiber 树,但是当 命中 bailout 逻辑且子孙节点没有更新任务时,会复用以当前 Fiber 节点为根的整颗子树。

    
    function bailoutOnAlreadyFinishedWork(
      current: Fiber | null,
      workInProgress: Fiber,
      renderLanes: Lanes,
    ): Fiber | null {
      if (current !== null) {
        // Reuse previous dependencies
        workInProgress.dependencies = current.dependencies;
      }
    
      if (enableProfilerTimer) {
        // Don't update "base" render times for bailouts.
        stopProfilerTimerIfRunning(workInProgress);
      }
    
      markSkippedUpdateLanes(workInProgress.lanes);
    
      // Check if the children have any pending work.
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        // The children don't have any work either. We can skip them.
        // TODO: Once we add back resuming, we should check if the children are
        // a work-in-progress set. If so, we need to transfer their effects.
        return null;
      } else {
        // This fiber doesn't have work, but its subtree does. Clone the child
        // fibers and continue.
        cloneChildFibers(current, workInProgress);
        return workInProgress.child;
      }
    }
    

    这里要注意的是是否返回 null 还要看当前 Fiber 节点的子孙节点们中是否有更新任务,如果有则不能直接返回 null,仍然需要对子节点进行处理。这里有个问题,当前节点是怎么知道子孙节点是否有更新任务的?答案是因为当某个节点触发了更新时,会沿着 Fiber 一直往上冒泡,这个过程中每个节点都能收集到自己子孙节点的相关信息:

    function markUpdateLaneFromFiberToRoot(
      sourceFiber: Fiber,
      lane: Lane,
    ): FiberRoot | null {
      // Update the source fiber's lanes
      sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
      let alternate = sourceFiber.alternate;
      if (alternate !== null) {
        alternate.lanes = mergeLanes(alternate.lanes, lane);
      }
      // Walk the parent path to the root and update the child expiration time.
      let node = sourceFiber;
      let parent = sourceFiber.return;
      while (parent !== null) {
        parent.childLanes = mergeLanes(parent.childLanes, lane);
        alternate = parent.alternate;
        if (alternate !== null) {
          alternate.childLanes = mergeLanes(alternate.childLanes, lane);
        } else {
          ...
        }
        node = parent;
        parent = parent.return;
      }
      if (node.tag === HostRoot) {
        const root: FiberRoot = node.stateNode;
        return root;
      } else {
        return null;
      }
    }
    

    接下来就是第二个问题了,什么时候会进入 bailout

    bailout 条件

    从代码中我们可以知道 bailout 的前提是:

    1. oldProps === newProps
    2. hasLegacyContextChanged()false
    3. includesSomeLane(renderLanes, updateLanes)false

    注意,因为我们只考虑生产环境,所以这里忽略 (__DEV__ ? workInProgress.type !== current.type : false),下面来分别分析一下这三种情况:

    oldProps === newProps

    我们通过一个例子来分析一下,下面例子中当 App 触发更新时 Son 对应的 Fiber 节点能复用吗?

    import React from 'react'
    
    function Son() {
      console.log('son render')
      return <div>Son</div>;
    }
    
    
    export default class App extends React.Component {
      state = {
        name: 'a'
      }
    
      componentDidMount() {
        setTimeout(() => {
          this.setState({
            name: 'b'
          })
        }, 1000)
      }
    
      render() {
        return <Son />
      }
    }
    

    答案是不能。因为 return <Son /> 实际是上为转换为 return React.createElement(Son)。两次 render 函数返回的对象完全不同,故这里 oldProps !== newProps

    React 源码解析之协调过程(一)

    若想复用的话,可以这样写:

    import React from 'react'
    
    function Son() {
      console.log('son render')
      return <div>Son</div>;
    }
    
    const memoizedSon = <Son />
    
    export default class App extends React.Component {
      state = {
        name: 'a'
      }
    
      componentDidMount() {
        setTimeout(() => {
          this.setState({
            name: 'b'
          })
        }, 1000)
      }
    
      render() {
        return memoizedSon
      }
    }
    

    这样,每次 render 返回的都是同一个 ReactElement 对象,通过其创建的 Fiber 上的 pendingPropsmemoizedProps 也都指向同一个对象:

    React 源码解析之协调过程(一)

    hasLegacyContextChangedtrue

    这个判断条件是留给已废弃的 context 使用的:

    class Son extends React.Component {
      render() {
        console.log('child render')
        return <span>{this.context.value}</span>
      }
    }
    
    Son.contextTypes = {
      value: PropTypes.string
    };
    
    const memoizedSon = <Son />
    
    export default class App extends React.Component {
      state = {
        value: 'context'
      }
      getChildContext() {
        return this.state
      }
      componentDidMount() {
        setTimeout(() => {
          this.setState({
            value: 'new context'
          })
        }, 1000)
      }
      render() {
        return memoizedSon;
      }
    }
    
    App.childContextTypes = {
      value: PropTypes.string
    }
    

    这里我们跟前面一样缓存了 <Son />,但是由于使用了旧的已废弃的 ContexthasLegacyContextChanged() 会为 true,所以这个例子不会走 bailout。关于 Context,这里还有很多可以展开的内容,这个留给以后单独写一篇 Context 的文章吧。

    includesSomeLane(renderLanes, updateLanes)false

    includesSomeLane(renderLanes, updateLanes) 这句代码是为了判断当前节点上的更新任务的优先级是否包含在了此次更新 的优先级之中。如果当前节点的更新优先级大于等于此次更新的优先级,则 includesSomeLane(renderLanes, updateLanes) 会返回 true

    import React from 'react'
    
    function Son() {
      const [count, setCount] = React.useState(1)
      return <div onClick={() => setCount(2)}>{count}</div>;
    }
    
    const memoizedSon = <Son />
    
    export default class App extends React.Component {
      render() {
        return memoizedSon
      }
    }
    

    上面的例子当点击 div 时会触发一轮更新, App 会进入 bailout 逻辑,且 includesSomeLane(renderLanes, workInProgress.childLanes)true 所以会继续处理子节点 Son。而 Son 节点对应的更新优先级是等于此次更新的优先级的,所以 Son 不会走 bailout

    第四种情况

    到这里,bailout 的三个条件就讨论完了,不过这里还有一种情况,也是我们比较常用的,那就是 shouldComponentUpdateReact.memo,这两个都比较熟悉,就不啰嗦了,直接贴出源代码:

    shouldComponentUpdate

    function checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    ) {
      const instance = workInProgress.stateNode;
      if (typeof instance.shouldComponentUpdate === 'function') {
        const shouldUpdate = instance.shouldComponentUpdate(
          newProps,
          newState,
          nextContext,
        );
    
        return shouldUpdate;
      }
    
      if (ctor.prototype && ctor.prototype.isPureReactComponent) {
        return (
          !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
        );
      }
    
      return true;
    }
    
    function finishClassComponent(
      current: Fiber | null,
      workInProgress: Fiber,
      Component: any,
      shouldUpdate: boolean,
      hasContext: boolean,
      renderLanes: Lanes,
    ) {
      ...
      if (!shouldUpdate && !didCaptureError) {
        // Context providers should defer to sCU for rendering
        if (hasContext) {
          invalidateContextProvider(workInProgress, Component, false);
        }
    
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
      }
      ...
    }
    
    

    React.memo

    
    function updateSimpleMemoComponent(
      current: Fiber | null,
      workInProgress: Fiber,
      Component: any,
      nextProps: any,
      updateLanes: Lanes,
      renderLanes: Lanes,
    ): null | Fiber {
      ...
      if (current !== null) {
        const prevProps = current.memoizedProps;
        if (
          shallowEqual(prevProps, nextProps) &&
          current.ref === workInProgress.ref &&
          // Prevent bailout if the implementation changed due to hot reload.
          (__DEV__ ? workInProgress.type === current.type : true)
        ) {
          didReceiveUpdate = false;
          if (!includesSomeLane(renderLanes, updateLanes)) {
            workInProgress.lanes = current.lanes;
            return bailoutOnAlreadyFinishedWork(
              current,
              workInProgress,
              renderLanes,
            );
          } else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
            // This is a special case that only exists for legacy mode.
            // See https://github.com/facebook/react/pull/19216.
            didReceiveUpdate = true;
          }
        }
      }
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        nextProps,
        renderLanes,
      );
    }
    

    说了这么多,下面我们用一个题目来结束这篇文章。下面的代码,点击 divSon 函数组件会重新执行吗?

    import * as React from 'react'
    
    function Son() {
      console.log('child render!');
      return <p>Son</p>;
    }
    
    
    function Parent(props) {
      const [count, setCount] = React.useState(0);
    
      return (
        <div onClick={() => {setCount(count + 1)}}>
          <span>{count}</span>
          {props.children}
        </div>
      );
    }
    
    
    export default function App() {
      return (
        <Parent>
          <Son />
        </Parent>
      );
    }
    

    答案是不会。下面我们来分析一下:

    • 首次渲染完成后,我们有如下所示的 Fiber 数结构:

    React 源码解析之协调过程(一)

    • 更新时,App 进入 bailout 流程,但是因为子节点有更新任务,所以不会返回 null,会执行 cloneChildFibers(current, workInProgress);,此时,数据结构如下所示:

    React 源码解析之协调过程(一)

    • Parent 上面有更新任务,不会走 bailout 逻辑,会更新 Parent,执行 render 方法,返回新的 divspan 对应的 ReactElement 对象,而返回的 props.children 还是首次渲染的那个 Son ReactElement。所以 divspan 不会走 bailout 流程,而 Son 会走 bailout 流程:

    React 源码解析之协调过程(一)

    结语

    本文介绍了 beginWork 中的前半部分,即 bailout,下一篇会分析下半部分,即组件更新的相关内容,其中会涉及到面试必考的 diff 算法。

    欢迎关注公众号「前端游」,让我们一起在前端的海洋里遨游。


    起源地下载网 » React 源码解析之协调过程(一)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元