react.lazy 路由懒加载_React lazy/Suspense使用及源码解析

React v16.6.0已经发布快一年了,为保障项目迭代发布,没有及时更新react版本,最近由于开启了新项目,于是使用新的react版本进行了项目开发。项目工程如何搭建,如何满足兼容性要求,如何规范化等等这里不作为介绍重点,这里想说一下react的lazy,suspense,这块在react官网作为code-splitting重点说明过,可见其出现的意义。

官网写的比较详细,总结起来就是,如果你项目中使用webpack或browserify进行打包,随着工程项目的增长和大量三方库的引入,会使你打包后的文件逐渐变大,用户加载文件时,会花大量时间去加载他们并不关心的内容,而此时,懒加载React.lazy的概念就应运而生。

注意:官网提示React.lazy并不适合SSR

这里介绍基于路由的懒加载,也是比较常用的方式,React.lazy只要一句话就能实现,如下:

const OtherComponent = React.lazy(async () => import('./OtherComponent'));

lazy中的函数返回Promise对象,引入了导出React Component的文件,并且官方提示为了有过度效果,还提供了Suspense组件,而且如果不引入的话还会报错,如下:

<Suspense fallback={<div>Loading...</div>}>
    <OtherComponent />
</Suspense>

以上都是官网示例,在项目实际使用中,还没有单独对功能组件进行懒加载,可以依据业务租组件的复杂度决定是否使用懒加载,个人觉得路由的懒加载是有必要的。使用中我们用高阶组件进行Suspense的封装:

const WithLazyLoad = (WrappedComponent: React.ComponentType<any>) =>
  class HOC extends React.Component {
    private displayName = `HOC(${getDisplayName(WrappedComponent)})`;

    public render() {
      console.log(this.displayName)

      return (
        <React.Suspense fallback={<div>Loading...</div>} >
            <WrappedComponent {...this.props} />
        </React.Suspense>  
      )
    }
  };

在App.tsx中对路由进行定义,这里假设有三个路由地址:

const About = React.lazy(() => import('./components/About/About'));
const Hello = React.lazy(() => import('./components/Hello/Hello'));
const Home = React.lazy(() => import('./components/Home/Home'));

class App extends React.Component {
  public render() {
    return (
      <BrowserRouter>
        <Switch>
          <Route path="/" exact={true} component={WithLazyLoad(Hello)}  />
          <Route path="/home" exact={true} component={WithLazyLoad(Home)} />
          <Route path="/about" exact={true} component={WithLazyLoad(About)} />
        </Switch>
      </BrowserRouter>
    );
  }
}

以上两步,就完成了基本功能对实现,我们来看下效果

v2-3f502daa07913add3276195b60fb3cca_b.png
使用Lazy

使用lazy后会根据路由打包成多个chunk文件,进行按需加载。我们打印懒加载的组件信息,返回的是个对象,示意如下:

v2-33c2606efaec6bad6535ff66ad31da8a_b.jpg
React.lazy(() =&amp;gt; import(&amp;#39;./components/Home/Home&amp;#39;))返回对象
主要属性说明:
$$typeof:对象类型,包括Symbol(react.lazy)、Symbol(react.element)、Symbol(react.portal)等等,在react源码中有定义
_ctor:懒加载异步函数,返回Promise对象,即 async () => import('./Home')
_result:存储懒加载异步函数执行的结果,可能值为error、moduleObject.default(即ƒ Home())
_status:当前状态,初始值(-1)、Pending(0)、Resolved(1)、Rejected(2)

查看react源码,在react-dom.js文件下的beginWork函数中,可以看到LazyComponent的加载方式其实是调用了mountLazyComponent函数,

switch (workInProgress.tag) {
    // ...
    case LazyComponent:
      {
        var _elementType = workInProgress.elementType;
        return mountLazyComponent(current$$1, workInProgress, _elementType, updateExpirationTime, renderExpirationTime);
      }
    // ...
}

查看mountLazyComponent函数,最重要的地方是,下面会分步解析:

// 解析lazy component
var Component = readLazyComponentType(elementType);
// Store the unwrapped component in the type.
workInProgress.type = Component;
// 获取Component类型,可能值ClassComponent、FunctionComponent、ForwardRef、MemoComponent、IndeterminateComponent
var resolvedTag = workInProgress.tag = resolveLazyComponentTag(Component);
// 初始化props
var resolvedProps = resolveDefaultProps(Component, props);

首先看readLazyComponentType函数,其参数elementType为上面打印出的对象,返回懒加载的组件,下面列出了关键代码,_thenable执行ctor()异步函数,拿到import的组件函数即f home(),拿到后暂存于workInProgress.type:

function readLazyComponentType(lazyComponent) {
  var status = lazyComponent._status;
  var result = lazyComponent._result;
  switch (status) {
    // ...
    default:
      {
        lazyComponent._status = Pending;
        var ctor = lazyComponent._ctor;
        var _thenable = ctor();
        _thenable.then(function (moduleObject) {
          if (lazyComponent._status === Pending) {
            var defaultExport = moduleObject.default;
            {
              if (defaultExport === undefined) {
                warning$1(false, 'lazy: Expected the result of a dynamic import() call. ' + 'Instead received: %snnYour 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;
          }
        });
        // Handle synchronous thenables.
        switch (lazyComponent._status) {
          case Resolved:
            return lazyComponent._result;
          case Rejected:
            throw lazyComponent._result;
        }
        lazyComponent._result = _thenable;
        throw _thenable;
      }
  }
}

v2-5cb0174e36850cc0ba9c4b47c3e098a1_b.jpg
正常返回的lazyComponent._result

随后执行resolveLazyComponentTag函数,入参为readLazyComponentType拿到的结果Component,由于我们的返回的是f home(),所以直接用shouldConstruct判断Component的原型上是否有isReactComponent,如果存在则为class组件,否则为函数组件,代码如下:

function resolveLazyComponentTag(Component) {
  if (typeof Component === 'function') {
    return shouldConstruct(Component) ? ClassComponent : FunctionComponent;
  } else if (Component !== undefined && Component !== null) {
    var $$typeof = Component.$$typeof;
    if ($$typeof === REACT_FORWARD_REF_TYPE) {
      return ForwardRef;
    }
    if ($$typeof === REACT_MEMO_TYPE) {
      return MemoComponent;
    }
  }
  return IndeterminateComponent;
}

之后执行resolveDefaultProps,初始化默认的props

function resolveDefaultProps(Component, baseProps) {
  if (Component && Component.defaultProps) {
    // Resolve default props. Taken from ReactElement
    var props = _assign({}, baseProps);
    var defaultProps = Component.defaultProps;
    for (var propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
    return props;
  }
  return baseProps;
}

执行完上面的方法,懒加载的前期工作就差不多完成了,下面根据resolvedTag进行组件刷新,我们这里是ClassComponent,所以重点看这块的更新方法updateClassComponent,下面我们逐段分析该方法

switch (resolvedTag) {
    // ...
    case ClassComponent:
      {
        child = updateClassComponent(null, workInProgress, Component, resolvedProps, renderExpirationTime);
        break;
      }
    // ...
  }

updateClassComponent方法首先做了propTypes的校验(如果在组件中设置了的话),注意无法在CreateElement中验证lazy组件的属性,只能在updateClassComponent中进行验证。

{
    if (workInProgress.type !== workInProgress.elementType) {
      var innerPropTypes = Component.propTypes;
      if (innerPropTypes) {
        checkPropTypes(innerPropTypes, nextProps, // Resolved props
        'prop', getComponentName(Component), getCurrentFiberStackInDev);
      }
    }
  }

然后检查是否有context,如果有的话则设置Provider,并监听变化,随后执行实例化,最后执行finishClassComponent方法,进行Component的render,即CreateElement,渲染到dom上

 var hasContext = void 0;
  if (isContextProvider(Component)) {
    hasContext = true;
    pushContextProvider(workInProgress);
  } else {
    hasContext = false;
  }
  prepareToReadContext(workInProgress, renderExpirationTime);

// ...

  constructClassInstance(workInProgress, Component, nextProps, renderExpirationTime);
  mountClassInstance(workInProgress, Component, nextProps, renderExpirationTime);
// ...
  var nextUnitOfWork = finishClassComponent(current$$1, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime);

Suspense组件的渲染方式类似,也是用updateSuspenseComponent,只不过里面有nextDidTimeout标志,决定是渲染fallback还是其子组件。

上面就是关于React.lazy的一些想要分享和记录的一些内容,如果存在错误的理解或更好的理解方式,希望多多交流

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值