【React】React源碼梳理筆記(十四)

前言

  • 本篇看一下Suspense和lazy到底啥玩意。

示例

  • lazy和suspense的組合有點意思,一般用來做代碼分割用,但裏面如何實現的?我只知道分割部分是用webpack的import語句寫的,畢竟這個是用戶自己寫的,裏面感覺像是啥都沒幹一樣。這就有點像webpack的cssloader,你感覺cssloader好像啥都沒幹,畢竟你寫的本來就是css或者被預處理器轉成了css,那麼要cssloader幹啥?實際上cssloader寫的相當複雜。
import React, { Component,lazy,Suspense } from 'react';
import reactDom from 'react-dom';
import {HashRouter,Route,Link}from 'react-router-dom'
class App extends Component{
  render(){
    return(
      <HashRouter>
      <Suspense fallback={null}>
      <Link to='/'>111</Link>
      <Link to='/2'>222</Link>
      <Route path="/" exact component={lazy(() =>import("./a"))}></Route>
      <Route path="/2" exact component={lazy(() =>import("./b"))}></Route>
      </Suspense>
      </HashRouter>
    )
  }
}
reactDom.render(
  <App></App>
,document.getElementById('root')
)

源碼

  • lazy:
function lazy(ctor) {
  var lazyType = {
    $$typeof: REACT_LAZY_TYPE,
    _ctor: ctor,
    // React uses these fields to store the result.
    _status: -1,
    _result: null
  };

  {
    // In production, this would just set it on the object.
    var defaultProps;
    var propTypes;
    Object.defineProperties(lazyType, {
      defaultProps: {
        configurable: true,
        get: function () {
          return defaultProps;
        },
        set: function (newDefaultProps) {
          error('React.lazy(...): It is not supported to assign `defaultProps` to ' + 'a lazy component import. Either specify them where the component ' + 'is defined, or create a wrapping component around it.');

          defaultProps = newDefaultProps; // Match production behavior more closely:

          Object.defineProperty(lazyType, 'defaultProps', {
            enumerable: true
          });
        }
      },
      propTypes: {
        configurable: true,
        get: function () {
          return propTypes;
        },
        set: function (newPropTypes) {
          error('React.lazy(...): It is not supported to assign `propTypes` to ' + 'a lazy component import. Either specify them where the component ' + 'is defined, or create a wrapping component around it.');

          propTypes = newPropTypes; // Match production behavior more closely:

          Object.defineProperty(lazyType, 'propTypes', {
            enumerable: true
          });
        }
      }
    });
  }

  return lazyType;
}

  • suspense居然就一type:
exports.Suspense = REACT_SUSPENSE_TYPE;
  • 我只想說mmp,要知道這個流程只能看一下他到底怎麼工作的。
  • 可以在渲染前打印下,發現suspense就是做成了個vdom
type: Symbol(react.suspense)
props: {fallback: null, children: Array(4)}
$$typeof: Symbol(react.element)
  • 而lazy也做成了個vdom:
component:
$$typeof: Symbol(react.lazy)
_ctor: () => {}
_result: ƒ Aaa(props)
_status: 1
defaultProps: (...)
propTypes: (...)
get defaultProps: ƒ ()
set defaultProps: ƒ (newDefaultProps)
get propTypes: ƒ ()
set propTypes: ƒ (newPropTypes)
__proto__: Object
exact: true
path: "/"
  • 所以看來,要了解原理,又得去看令人崩潰的react-dom。。。
  • 打斷點看一下lazy相關:
    function mountLazyComponent(_current, workInProgress, elementType, updateExpirationTime, renderExpirationTime) {
      if (_current !== null) {
        // A lazy component only mounts if it suspended inside a non-
        // concurrent tree, in an inconsistent state. We want to treat it like
        // a new mount, even though an empty version of it already committed.
        // Disconnect the alternate pointers.
        _current.alternate = null;
        workInProgress.alternate = null; // Since this is conceptually a new fiber, schedule a Placement effect

        workInProgress.effectTag |= Placement;
      }

      var props = workInProgress.pendingProps; // We can't start a User Timing measurement with correct label yet.
      // Cancel and resume right after we know the tag.

      cancelWorkTimer(workInProgress);
      var Component = readLazyComponentType(elementType); // Store the unwrapped component in the type.

      workInProgress.type = Component;
      var resolvedTag = workInProgress.tag = resolveLazyComponentTag(Component);
      startWorkTimer(workInProgress);
      var resolvedProps = resolveDefaultProps(Component, props);
      var child;

      switch (resolvedTag) {
        case FunctionComponent:
          {
            {
              validateFunctionComponentInDev(workInProgress, Component);
              workInProgress.type = Component = resolveFunctionForHotReloading(Component);
            }

            child = updateFunctionComponent(null, workInProgress, Component, resolvedProps, renderExpirationTime);
            return child;
          }

        case ClassComponent:
          {
            {
              workInProgress.type = Component = resolveClassForHotReloading(Component);
            }

            child = updateClassComponent(null, workInProgress, Component, resolvedProps, renderExpirationTime);
            return child;
          }

        case ForwardRef:
          {
            {
              workInProgress.type = Component = resolveForwardRefForHotReloading(Component);
            }

            child = updateForwardRef(null, workInProgress, Component, resolvedProps, renderExpirationTime);
            return child;
          }

        case MemoComponent:
          {
            {
              if (workInProgress.type !== workInProgress.elementType) {
                var outerPropTypes = Component.propTypes;

                if (outerPropTypes) {
                  checkPropTypes(outerPropTypes, resolvedProps, // Resolved for outer only
                    'prop', getComponentName(Component), getCurrentFiberStackInDev);
                }
              }
            }

            child = updateMemoComponent(null, workInProgress, Component, resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too
              updateExpirationTime, renderExpirationTime);
            return child;
          }
      }

      var hint = '';

      {
        if (Component !== null && typeof Component === 'object' && Component.$$typeof === REACT_LAZY_TYPE) {
          hint = ' Did you wrap a component in React.lazy() more than once?';
        }
      } // This message intentionally doesn't mention ForwardRef or MemoComponent
      // because the fact that it's a separate type of work is an
      // implementation detail.


      {
        {
          throw Error("Element type is invalid. Received a promise that resolves to: " + Component + ". Lazy element type must resolve to a class or function." + hint);
        }
      }
    }

    function readLazyComponentType(lazyComponent) {
      initializeLazyComponentType(lazyComponent);

      if (lazyComponent._status !== Resolved) {
        throw lazyComponent._result;
      }

      return lazyComponent._result;
    }
   function initializeLazyComponentType(lazyComponent) {
      if (lazyComponent._status === Uninitialized) {
        lazyComponent._status = Pending;
        var ctor = lazyComponent._ctor;
        var thenable = ctor();
        lazyComponent._result = thenable;
        thenable.then(function (moduleObject) {
          if (lazyComponent._status === Pending) {
            var defaultExport = moduleObject.default;

            {
              if (defaultExport === undefined) {
                error('lazy: Expected the result of a dynamic import() call. ' + 'Instead received: %s\n\nYour code should look like: \n  ' + "const MyComponent = lazy(() => import('./MyComponent'))", moduleObject);
              }
            }

            lazyComponent._status = Resolved;
            lazyComponent._result = defaultExport;
          }
        }, function (error) {
          if (lazyComponent._status === Pending) {
            lazyComponent._status = Rejected;
            lazyComponent._result = error;
          }
        });
      }
    }
  • 可以看見,這個thenable就是我們在lazy中寫的webpack的import,還沒加載時是個pending的promise,然後存到_result上。就是把我們寫的存到vdom屬性上。又加了個then後的內容和狀態,到時候代碼來了,狀態顯示仍未處理,那就把真正結果放到_result上。

  • 當拉完代碼後webpack的promise會調then,就是走一遍這裏,這時這個vnode的_result就是有值的狀態,同樣也會進initializeLazyComponentType方法,但是這裏已經被標記處理過了,然後就會直接退出來返回_result結果。

  • 此時mountLazyComponent中工作的fiber就會取得Component結果,然後存到type上,resolveDefaultProps是判斷有無默認屬性,判斷這個默認導出到底是什麼組件,再後面幾個判斷沒啥用,最後就是更新屬性,得到當前工作fiber的最新狀態,也就是拿到了懶加載後最終要渲染的fiber的樣子。

  • 如果一直沒拉完代碼當然就一直沒人調then,頁面一直維持fallback樣子,有人調then或者rej就繼續走調度。

  • 判斷是suspense的tag會走這個邏輯:

 function updateSuspenseComponent(current, workInProgress, renderExpirationTime) {
      var mode = workInProgress.mode;
      var nextProps = workInProgress.pendingProps; // This is used by DevTools to force a boundary to suspend.
      {
        if (shouldSuspend(workInProgress)) {
          workInProgress.effectTag |= DidCapture;
        }
      }

      var suspenseContext = suspenseStackCursor.current;
      var nextDidTimeout = false;
      var didSuspend = (workInProgress.effectTag & DidCapture) !== NoEffect;

      if (didSuspend || shouldRemainOnFallback(suspenseContext, current)) {
        // Something in this boundary's subtree already suspended. Switch to
        // rendering the fallback children.
        nextDidTimeout = true;
        workInProgress.effectTag &= ~DidCapture;
      } else {
        // Attempting the main content
        if (current === null || current.memoizedState !== null) {
          // This is a new mount or this boundary is already showing a fallback state.
          // Mark this subtree context as having at least one invisible parent that could
          // handle the fallback state.
          // Boundaries without fallbacks or should be avoided are not considered since
          // they cannot handle preferred fallback states.
          if (nextProps.fallback !== undefined && nextProps.unstable_avoidThisFallback !== true) {
            suspenseContext = addSubtreeSuspenseContext(suspenseContext, InvisibleParentSuspenseContext);
          }
        }
      }

      suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
      pushSuspenseContext(workInProgress, suspenseContext); // This next part is a bit confusing. If the children timeout, we switch to
      // showing the fallback children in place of the "primary" children.
      // However, we don't want to delete the primary children because then their
      // state will be lost (both the React state and the host state, e.g.
      // uncontrolled form inputs). Instead we keep them mounted and hide them.
      // Both the fallback children AND the primary children are rendered at the
      // same time. Once the primary children are un-suspended, we can delete
      // the fallback children — don't need to preserve their state.
      //
      // The two sets of children are siblings in the host environment, but
      // semantically, for purposes of reconciliation, they are two separate sets.
      // So we store them using two fragment fibers.
      //
      // However, we want to avoid allocating extra fibers for every placeholder.
      // They're only necessary when the children time out, because that's the
      // only time when both sets are mounted.
      //
      // So, the extra fragment fibers are only used if the children time out.
      // Otherwise, we render the primary children directly. This requires some
      // custom reconciliation logic to preserve the state of the primary
      // children. It's essentially a very basic form of re-parenting.

      if (current === null) {
        // If we're currently hydrating, try to hydrate this boundary.
        // But only if this has a fallback.
        if (nextProps.fallback !== undefined) {
          tryToClaimNextHydratableInstance(workInProgress); // This could've been a dehydrated suspense component.
        } // This is the initial mount. This branch is pretty simple because there's
        // no previous state that needs to be preserved.


        if (nextDidTimeout) {
          // Mount separate fragments for primary and fallback children.
          var nextFallbackChildren = nextProps.fallback;
          var primaryChildFragment = createFiberFromFragment(null, mode, NoWork, null);
          primaryChildFragment.return = workInProgress;

          if ((workInProgress.mode & BlockingMode) === NoMode) {
            // Outside of blocking mode, we commit the effects from the
            // partially completed, timed-out tree, too.
            var progressedState = workInProgress.memoizedState;
            var progressedPrimaryChild = progressedState !== null ? workInProgress.child.child : workInProgress.child;
            primaryChildFragment.child = progressedPrimaryChild;
            var progressedChild = progressedPrimaryChild;

            while (progressedChild !== null) {
              progressedChild.return = primaryChildFragment;
              progressedChild = progressedChild.sibling;
            }
          }

          var fallbackChildFragment = createFiberFromFragment(nextFallbackChildren, mode, renderExpirationTime, null);
          fallbackChildFragment.return = workInProgress;
          primaryChildFragment.sibling = fallbackChildFragment; // Skip the primary children, and continue working on the
          // fallback children.

          workInProgress.memoizedState = SUSPENDED_MARKER;
          workInProgress.child = primaryChildFragment;
          return fallbackChildFragment;
        } else {
          // Mount the primary children without an intermediate fragment fiber.
          var nextPrimaryChildren = nextProps.children;
          workInProgress.memoizedState = null;
          return workInProgress.child = mountChildFibers(workInProgress, null, nextPrimaryChildren, renderExpirationTime);
        }
      } else {
        // This is an update. This branch is more complicated because we need to
        // ensure the state of the primary children is preserved.
        var prevState = current.memoizedState;

        if (prevState !== null) {
          // wrapped in a fragment fiber.


          var currentPrimaryChildFragment = current.child;
          var currentFallbackChildFragment = currentPrimaryChildFragment.sibling;

          if (nextDidTimeout) {
            // Still timed out. Reuse the current primary children by cloning
            // its fragment. We're going to skip over these entirely.
            var _nextFallbackChildren2 = nextProps.fallback;

            var _primaryChildFragment2 = createWorkInProgress(currentPrimaryChildFragment, currentPrimaryChildFragment.pendingProps);

            _primaryChildFragment2.return = workInProgress;

            if ((workInProgress.mode & BlockingMode) === NoMode) {
              // Outside of blocking mode, we commit the effects from the
              // partially completed, timed-out tree, too.
              var _progressedState = workInProgress.memoizedState;

              var _progressedPrimaryChild = _progressedState !== null ? workInProgress.child.child : workInProgress.child;

              if (_progressedPrimaryChild !== currentPrimaryChildFragment.child) {
                _primaryChildFragment2.child = _progressedPrimaryChild;
                var _progressedChild2 = _progressedPrimaryChild;

                while (_progressedChild2 !== null) {
                  _progressedChild2.return = _primaryChildFragment2;
                  _progressedChild2 = _progressedChild2.sibling;
                }
              }
            } // Because primaryChildFragment is a new fiber that we're inserting as the
            // parent of a new tree, we need to set its treeBaseDuration.


            if (workInProgress.mode & ProfileMode) {
              // treeBaseDuration is the sum of all the child tree base durations.
              var _treeBaseDuration = 0;
              var _hiddenChild = _primaryChildFragment2.child;

              while (_hiddenChild !== null) {
                _treeBaseDuration += _hiddenChild.treeBaseDuration;
                _hiddenChild = _hiddenChild.sibling;
              }

              _primaryChildFragment2.treeBaseDuration = _treeBaseDuration;
            } // Clone the fallback child fragment, too. These we'll continue
            // working on.


            var _fallbackChildFragment2 = createWorkInProgress(currentFallbackChildFragment, _nextFallbackChildren2);

            _fallbackChildFragment2.return = workInProgress;
            _primaryChildFragment2.sibling = _fallbackChildFragment2;
            _primaryChildFragment2.childExpirationTime = NoWork; // Skip the primary children, and continue working on the
            // fallback children.

            workInProgress.memoizedState = SUSPENDED_MARKER;
            workInProgress.child = _primaryChildFragment2;
            return _fallbackChildFragment2;
          } else {
            // No longer suspended. Switch back to showing the primary children,
            // and remove the intermediate fragment fiber.
            var _nextPrimaryChildren = nextProps.children;
            var currentPrimaryChild = currentPrimaryChildFragment.child;
            var primaryChild = reconcileChildFibers(workInProgress, currentPrimaryChild, _nextPrimaryChildren, renderExpirationTime); // If this render doesn't suspend, we need to delete the fallback
            // children. Wait until the complete phase, after we've confirmed the
            // fallback is no longer needed.
            // TODO: Would it be better to store the fallback fragment on
            // the stateNode?
            // Continue rendering the children, like we normally do.

            workInProgress.memoizedState = null;
            return workInProgress.child = primaryChild;
          }
        } else {
          // The current tree has not already timed out. That means the primary
          // children are not wrapped in a fragment fiber.
          var _currentPrimaryChild = current.child;

          if (nextDidTimeout) {
            // Timed out. Wrap the children in a fragment fiber to keep them
            // separate from the fallback children.
            var _nextFallbackChildren3 = nextProps.fallback;

            var _primaryChildFragment3 = createFiberFromFragment( // It shouldn't matter what the pending props are because we aren't
              // going to render this fragment.
              null, mode, NoWork, null);

            _primaryChildFragment3.return = workInProgress;
            _primaryChildFragment3.child = _currentPrimaryChild;

            if (_currentPrimaryChild !== null) {
              _currentPrimaryChild.return = _primaryChildFragment3;
            } // Even though we're creating a new fiber, there are no new children,
            // because we're reusing an already mounted tree. So we don't need to
            // schedule a placement.
            // primaryChildFragment.effectTag |= Placement;


            if ((workInProgress.mode & BlockingMode) === NoMode) {
              // Outside of blocking mode, we commit the effects from the
              // partially completed, timed-out tree, too.
              var _progressedState2 = workInProgress.memoizedState;

              var _progressedPrimaryChild2 = _progressedState2 !== null ? workInProgress.child.child : workInProgress.child;

              _primaryChildFragment3.child = _progressedPrimaryChild2;
              var _progressedChild3 = _progressedPrimaryChild2;

              while (_progressedChild3 !== null) {
                _progressedChild3.return = _primaryChildFragment3;
                _progressedChild3 = _progressedChild3.sibling;
              }
            } // Because primaryChildFragment is a new fiber that we're inserting as the
            // parent of a new tree, we need to set its treeBaseDuration.


            if (workInProgress.mode & ProfileMode) {
              // treeBaseDuration is the sum of all the child tree base durations.
              var _treeBaseDuration2 = 0;
              var _hiddenChild2 = _primaryChildFragment3.child;

              while (_hiddenChild2 !== null) {
                _treeBaseDuration2 += _hiddenChild2.treeBaseDuration;
                _hiddenChild2 = _hiddenChild2.sibling;
              }

              _primaryChildFragment3.treeBaseDuration = _treeBaseDuration2;
            } // Create a fragment from the fallback children, too.


            var _fallbackChildFragment3 = createFiberFromFragment(_nextFallbackChildren3, mode, renderExpirationTime, null);

            _fallbackChildFragment3.return = workInProgress;
            _primaryChildFragment3.sibling = _fallbackChildFragment3;
            _fallbackChildFragment3.effectTag |= Placement;
            _primaryChildFragment3.childExpirationTime = NoWork; // Skip the primary children, and continue working on the
            // fallback children.

            workInProgress.memoizedState = SUSPENDED_MARKER;
            workInProgress.child = _primaryChildFragment3;
            return _fallbackChildFragment3;
          } else {
            // Still haven't timed out. Continue rendering the children, like we
            // normally do.
            workInProgress.memoizedState = null;
            var _nextPrimaryChildren2 = nextProps.children;
            return workInProgress.child = reconcileChildFibers(workInProgress, _currentPrimaryChild, _nextPrimaryChildren2, renderExpirationTime);
          }
        }
      }
    }
  • 這玩意真長,註釋都寫下面內容比較亂,不過這個分2部分,就是掛載和更新,就是看current是不是null,current是頁面上狀態,上半部分是未渲染出來時走的fallback邏輯,大概意思就是在當前的工作fiber的孩子的sibiling那用fragment新增個元素,然後存個pendingProps等待屬性,這個pendingProps就是我們寫在fallback裏面的內容,在等待期間會渲染這個,而當前工作fiber的大兒子以及其子節點,就是我們所要真實渲染的元素。
  • current不是null也就是更新邏輯,還得判斷個nextDidTimeout,看是不是仍超時,如果仍超時,做個fallback渲染。
  • 判斷nextDidTimeout的下半段註釋寫了:
 No longer suspended. Switch back to showing the primary children,
          // and remove the intermediate fragment fiber.
  • 不再推遲,換成大兒子進行渲染。大兒子就是要渲染的子節點。
  • 所以這個suspense是個分步渲染的玩意,不管下面組件是不是能立即獲取。裏面只要進行更新,要走它調度,先甩給你個fallback,再看後面。

總結

  • lazy有點相當於給promise後面搞個更新,suspense相當於搞個分段渲染。

附加

  • 看看司徒正美大佬做的react咋實現suspense和lazy的:
function Suspense(props){
    return props.children
}
export {
    Suspense
}
import { miniCreateClass, isFn, get } from "react-core/util";
import { Component } from "react-core/Component";
import { createElement } from "react-core/createElement";
import { Suspense } from "./Suspense";

var LazyComponent = miniCreateClass(function LazyComponent(props, context) {
    this.props = props;
    this.context = context;
    this.state = {
        component: null,
        resolved: false
    }
    var promise = props.children();
    if(!promise || !isFn(promise.then)){
        throw "lazy必須返回一個thenable對象"
    }
    promise.then( (value) =>
        this.setState({
            component: value.default,
            resolved:  true
        })
    )
   
}, Component, {
    fallback(){//返回上層Suspense組件的fallback屬性
        var parent = Object(get(this)).return
        while(parent){
          if( parent.type === Suspense){
              return parent.props.fallback
           }
           parent = parent.return
        }
        throw "lazy組件必須包一個Suspense組件"
    },
    render: function f2(){
        return this.state.resolved ? createElement(this.state.component, this.props) : this.fallback()
    }
});

function lazy(render) {
    return function (props) {
        return createElement(LazyComponent, props, render );
    };
}
export {
    lazy
}
  • 相當精簡啊,直接用找父親方式找有沒有suspensetype然後渲染fallback就行了,當webpack調then,直接setState刷新。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章