乐趣区

关于前端:深度剖析React懒加载原理

目录

  • 代码宰割
  • 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 = 2
export 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 来解决异步加载资源时页面应该如何显示的问题。

退出移动版