目錄html
1.代碼分割前端
(1)爲何要進行代碼分割?react
如今前端項目基本都採用打包技術,好比 Webpack,JS邏輯代碼打包後會產生一個 bundle.js 文件,而隨着咱們引用的第三方庫愈來愈多或業務邏輯代碼愈來愈複雜,相應打包好的 bundle.js 文件體積就會愈來愈大,由於須要先請求加載資源以後,纔會渲染頁面,這就會嚴重影響到頁面的首屏加載。git
而爲了解決這樣的問題,避免大致積的代碼包,咱們則能夠經過技術手段對代碼包進行分割,可以建立多個包並在運行時動態地加載。如今像 Webpack、 Browserify等打包器都支持代碼分割技術。github
(2)何時應該考慮進行代碼分割?promise
這裏舉一個平時開發中可能會遇到的場景,好比某個體積相對比較大的第三方庫或插件(好比JS版的PDF預覽庫)只在單頁應用(SPA)的某一個不是首頁的頁面使用了,這種狀況就能夠考慮代碼分割,增長首屏的加載速度。瀏覽器
2.React的懶加載app
示例代碼:dom
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.lazy
和 Suspense
共同一塊兒實現了 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.lazy
與import()
實現了渲染時的動態加載 ,並利用Suspense
來處理異步加載資源時頁面應該如何顯示的問題。
3.參考