深入理解React:懒加载(lazy)实现原理
<p><strong>目录</strong></p><ul>
<li>代码分割</li>
<li>React的懒加载
<ul>
<li>import() 原理</li>
<li>React.lazy 原理</li>
<li>Suspense 原理</li>
</ul>
</li>
<li>参考</li>
</ul>
<br>
<p><strong>1.代码分割</strong></p>
<p>(1)为什么要进行代码分割?</p>
<p>现在前端项目基本都采用打包技术,比如 Webpack,JS逻辑代码打包后会产生一个 bundle.js 文件,而随着我们引用的第三方库越来越多或业务逻辑代码越来越复杂,相应打包好的 bundle.js 文件体积就会越来越大,因为需要先请求加载资源之后,才会渲染页面,这就会严重影响到页面的首屏加载。</p>
<p>而为了解决这样的问题,避免大体积的代码包,我们则可以通过技术手段对代码包进行分割,能够创建多个包并在运行时动态地加载。现在像 Webpack、 Browserify等打包器都支持代码分割技术。</p>
<br>
<p>(2)什么时候应该考虑进行代码分割?</p>
<p>这里举一个平时开发中可能会遇到的场景,比如某个体积相对比较大的第三方库或插件(比如JS版的PDF预览库)只在单页应用(SPA)的某一个不是首页的页面使用了,这种情况就可以考虑代码分割,增加首屏的加载速度。</p>
<br>
<p><strong>2.React的懒加载</strong></p>
<p>示例代码:</p>
<pre><code class="language-jsx">import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
</code></pre>
<p>如上代码中,通过 <code>import() </code>、<code>React.lazy</code> 和 <code>Suspense</code> 共同一起实现了 React 的懒加载,也就是我们常说了运行时动态加载,即 OtherComponent 组件文件被拆分打包为一个新的包(bundle)文件,并且只会在 OtherComponent 组件渲染时,才会被下载到本地。</p>
<p>那么上述中的代码拆分以及动态加载究竟是如何实现的呢?让我们来一起探究其原理是怎样的。</p>
<br>
<p><strong>import() 原理</strong></p>
<p>import() 函数是由TS39提出的一种动态加载模块的规范实现,其返回是一个 promise。在浏览器宿主环境中一个<code>import()</code>的参考实现如下:</p>
<pre><code class="language-js">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);
delete window;
script.remove();
};
script.onerror = () => {
reject(new Error("Failed to load module script with URL " + url));
delete window;
script.remove();
};
document.documentElement.appendChild(script);
});
}
</code></pre>
<p>当 Webpack 解析到该<code>import()</code>语法时,会自动进行代码分割。</p>
<br>
<p><strong>React.lazy 原理</strong></p>
<p><em>以下 React 源码基于 16.8.0 版本</em></p>
<br>
<p>React.lazy 的源码实现如下:</p>
<pre><code class="language-js">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;
}
</code></pre>
<p>可以看到其返回了一个 LazyComponent 对象。</p>
<br>
<p>而对于 LazyComponent 对象的解析:</p>
<pre><code class="language-js">...
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
);
}
...
</code></pre>
<pre><code class="language-js">function mountLazyComponent(
_current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
) {
...
let Component = readLazyComponentType(elementType);
...
}
</code></pre>
<pre><code class="language-js">// 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;
}
}
}
</code></pre>
<p><em>注:如果 readLazyComponentType 函数多次处理同一个 lazyComponent,则可能进入Pending、Rejected等 case 中。</em></p>
<p>从上述代码中可以看出,对于最初 <code>React.lazy()</code> 所返回的 LazyComponent 对象,其 _status 默认是 -1,所以<strong>首次渲染</strong>时,会进入 readLazyComponentType 函数中的 default 的逻辑,这里才会真正异步执行 <code>import(url)</code>操作,由于并未等待,随后会检查模块是否 Resolved,如果已经Resolved了(已经加载完毕)则直接返回<code>moduleObject.default</code>(动态加载的模块的默认导出),否则将通过 throw 将 thenable 抛出到上层。</p>
<p>为什么要 throw 它?这就要涉及到 <code>Suspense</code> 的工作原理,我们接着往下分析。</p>
<br>
<p><strong>Suspense 原理</strong></p>
<p>由于 React 捕获异常并处理的代码逻辑比较多,这里就不贴源码,感兴趣可以去看 throwException 中的逻辑,其中就包含了如何处理捕获的异常。简单描述一下处理过程,React 捕获到异常之后,会判断异常是不是一个 thenable,如果是则会找到 SuspenseComponent ,如果 thenable 处于 pending 状态,则会将其 children 都渲染成 fallback 的值,一旦 thenable 被 resolve 则 SuspenseComponent 的子组件会重新渲染一次。</p>
<br>
<p>为了便于理解,我们也可以用 componentDidCatch 实现一个自己的 Suspense 组件,如下:</p>
<pre><code class="language-jsx">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 }</>
}
}
</code></pre>
<br>
<p><strong>小结</strong></p>
<p>至此,我们分析完了 React 的懒加载原理。简单来说,React利用 <code>React.lazy</code>与<code>import()</code>实现了渲染时的动态加载 ,并利用<code>Suspense</code>来处理异步加载资源时页面应该如何显示的问题。</p>
<br>
<p><strong>3.参考</strong></p>
<p>代码分割– React</p>
<p>动态import - MDN - Mozilla</p>
<p>proposal-dynamic-import</p>
<p>React Lazy 的实现原理</p><br><br>
来源:https://www.cnblogs.com/forcheng/p/13132582.html
頁:
[1]