写点什么

深度剖析 React 懒加载原理

作者:xiaofeng
  • 2023-03-01
    浙江
  • 本文字数:3116 字

    阅读完需:约 10 分钟

目录


  • 代码分割

  • React 的懒加载

  • import() 原理

  • React.lazy 原理

  • Suspense 原理

  • 参考


1.代码分割


(1)为什么要进行代码分割?


现在前端项目基本都采用打包技术,比如 Webpack,JS 逻辑代码打包后会产生一个 bundle.js 文件,而随着我们引用的第三方库越来越多或业务逻辑代码越来越复杂,相应打包好的 bundle.js 文件体积就会越来越大,因为需要先请求加载资源之后,才会渲染页面,这就会严重影响到页面的首屏加载。


而为了解决这样的问题,避免大体积的代码包,我们则可以通过技术手段对代码包进行分割,能够创建多个包并在运行时动态地加载。现在像 Webpack、 Browserify 等打包器都支持代码分割技术。


(2)什么时候应该考虑进行代码分割?


这里举一个平时开发中可能会遇到的场景,比如某个体积相对比较大的第三方库或插件(比如 JS 版的 PDF 预览库)只在单页应用(SPA)的某一个不是首页的页面使用了,这种情况就可以考虑代码分割,增加首屏的加载速度。


2.React 的懒加载


示例代码:


import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> </div> );}
复制代码


如上代码中,通过 import()React.lazySuspense 共同一起实现了 React 的懒加载,也就是我们常说了运行时动态加载,即 OtherComponent 组件文件被拆分打包为一个新的包(bundle)文件,并且只会在 OtherComponent 组件渲染时,才会被下载到本地。


那么上述中的代码拆分以及动态加载究竟是如何实现的呢?让我们来一起探究其原理是怎样的。


import() 原理


import() 函数是由 TS39 提出的一种动态加载模块的规范实现,其返回是一个 promise。在浏览器宿主环境中一个import()的参考实现如下:


function import(url) {  return new Promise((resolve, reject) => {    const script = document.createElement("script");    const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);    script.type = "module";    script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;
script.onload = () => { resolve(window[tempGlobal]); delete window[tempGlobal]; script.remove(); };
script.onerror = () => { reject(new Error("Failed to load module script with URL " + url)); delete window[tempGlobal]; script.remove(); };
document.documentElement.appendChild(script); });}
复制代码


当 Webpack 解析到该import()语法时,会自动进行代码分割。


React.lazy 原理


以下 React 源码基于 16.8.0 版本


React.lazy 的源码实现如下:


export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {  let lazyType = {    ?typeof: REACT_LAZY_TYPE,    _ctor: ctor,    // React uses these fields to store the result.    _status: -1,    _result: null,  };
return lazyType;}
复制代码


可以看到其返回了一个 LazyComponent 对象。


而对于 LazyComponent 对象的解析:


...case LazyComponent: {  const elementType = workInProgress.elementType;  return mountLazyComponent(    current,    workInProgress,    elementType,    updateExpirationTime,    renderExpirationTime,  );}...
复制代码


function mountLazyComponent(  _current,  workInProgress,  elementType,  updateExpirationTime,  renderExpirationTime,) {   ...  let Component = readLazyComponentType(elementType);  ...}
复制代码


// Pending = 0, Resolved = 1, Rejected = 2export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {  const status = lazyComponent._status;  const result = lazyComponent._result;  switch (status) {    case Resolved: {      const Component: T = result;      return Component;    }    case Rejected: {      const error: mixed = result;      throw error;    }    case Pending: {      const thenable: Thenable<T, mixed> = result;      throw thenable;    }    default: { // lazyComponent 首次被渲染      lazyComponent._status = Pending;      const ctor = lazyComponent._ctor;      const thenable = ctor();      thenable.then(        moduleObject => {          if (lazyComponent._status === Pending) {            const defaultExport = moduleObject.default;            lazyComponent._status = Resolved;            lazyComponent._result = defaultExport;          }        },        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;    }  }}
复制代码


注:如果 readLazyComponentType 函数多次处理同一个 lazyComponent,则可能进入 Pending、Rejected 等 case 中。


从上述代码中可以看出,对于最初 React.lazy() 所返回的 LazyComponent 对象,其 _status 默认是 -1,所以首次渲染时,会进入 readLazyComponentType 函数中的 default 的逻辑,这里才会真正异步执行 import(url)操作,由于并未等待,随后会检查模块是否 Resolved,如果已经 Resolved 了(已经加载完毕)则直接返回moduleObject.default(动态加载的模块的默认导出),否则将通过 throw 将 thenable 抛出到上层。


为什么要 throw 它?这就要涉及到 Suspense 的工作原理,我们接着往下分析。


Suspense 原理


由于 React 捕获异常并处理的代码逻辑比较多,这里就不贴源码,感兴趣可以去看 throwException 中的逻辑,其中就包含了如何处理捕获的异常。简单描述一下处理过程,React 捕获到异常之后,会判断异常是不是一个 thenable,如果是则会找到 SuspenseComponent ,如果 thenable 处于 pending 状态,则会将其 children 都渲染成 fallback 的值,一旦 thenable 被 resolve 则 SuspenseComponent 的子组件会重新渲染一次。


为了便于理解,我们也可以用 componentDidCatch 实现一个自己的 Suspense 组件,如下:


class Suspense extends React.Component {  state = {    promise: null  }
componentDidCatch(err) { // 判断 err 是否是 thenable if (err !== null && typeof err === 'object' && typeof err.then === 'function') { this.setState({ promise: err }, () => { err.then(() => { this.setState({ promise: null }) }) }) } }
render() { const { fallback, children } = this.props const { promise } = this.state return <>{ promise ? fallback : children }</> }}
复制代码


参考 前端进阶面试题详细解答


小结


至此,我们分析完了 React 的懒加载原理。简单来说,React 利用 React.lazyimport()实现了渲染时的动态加载 ,并利用Suspense来处理异步加载资源时页面应该如何显示的问题。


用户头像

xiaofeng

关注

努力写代码中 2022-08-18 加入

努力写代码中

评论

发布
暂无评论
深度剖析React懒加载原理_前端_xiaofeng_InfoQ写作社区