React.lazy與bundle-loader?lazy

前言

React 16已經出一段時間了,React 16.6中新推出的 React.lazy 不知道你們是否已經開始使用?它與之前webpack中用的 bundle-loader?lazy 又有什麼區別?但願這篇文章可以分享清楚。javascript

環境信息:css

系統:macOS Mojave 10.14.2html

Node: v8.12.0java

React相關:react 16.9.0 、 react-router-dom 4.3.1node

做用

咱們都知道單頁應用中,webpack會將全部的JS、CSS加載到一個文件中,很容易形成首屏渲染慢的體驗感。而 React.lazy 與 bundle-loader?lazy 均可以解決該問題,即在真正須要渲染的時候纔會進行請求這些相關靜態資源。react

bundle-loader?lazy

咱們先從老前輩開始講起,bundle-loader?lazy聽說是Webpack2時代的產物,webpack2的中文文檔。使用時須要藉助一個HOC進行輔助渲染,具體例子以下:webpack

// 輔助渲染的HOC
import React from 'react';
import PropTypes from 'prop-types';

class Bundle extends React.Component {
    state = {
        mod: null,
    }

    componentWillMount() {
        // 加載初始狀態
        this.load(this.props);
        // 設置頁面title
        const { name } = this.props;
        document.title = name || '';
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.load !== this.props.load) {
            this.load(nextProps);
        }
    }

    load(props) {
        // 重置狀態
        this.setState({
            mod: null,
        });
        // 傳入的組件
        props.load((mod) => {
            this.setState({
                mod: mod.default ? mod.default : mod,
            });
        });
    }

    render() {
        return this.state.mod ? this.props.children(this.state.mod) : null;
    }
}

Bundle.propTypes = {
    load: PropTypes.func.isRequired,
    children: PropTypes.func.isRequired,
    name: PropTypes.string.isRequired,
};

export default Bundle;
複製代碼

路由配置以下:git

// 該代碼是下方代碼中的RouteConfig
const routesArr = [
	{
	    component: '所用的組件',
	    path: '/xxx',
	    name: "網頁名稱",
	    icon: "所用的icon名稱"
	    
	},
    ...
]
複製代碼

路由Route配置代碼github

RouteConfig.map((item, i) => {
    return (
        <Route
            // RouteConfig是路由配置的數組
            component={props => (
                // Bundle 是上面提到的輔助渲染HOC
                <Bundle 
                    name={item.name} 
                    load={item.component}
                >
                    {Container => <Container {...props} />}
                </Bundle>
            )}
            path={item.path}
            key={`${i}`}
        />
    )
});
複製代碼

最後的效果以下圖,其中6.js、6.css就是對應渲染頁面的JS以及CSS文件web

核心代碼

將bundle-loader?lazy包裹的文件console出來,代碼以下

import notFound from 'bundle-loader?lazy!./containers/notFound';
console.log(notFound)
// 刪除了註釋的輸出內容
module.exports = function(cb) {
    // 這裏 14 的參數含義是 請求14.js
	__webpack_require__.e(14).then((function(require) {
		cb(__webpack_require__("./node_modules/babel-loader/lib/index.js?!./src/containers/notFound/index.js"));
	}).bind(null, __webpack_require__)).catch(__webpack_require__.oe);
}
複製代碼

代碼中的__webpack_require__.e其實是webpack對require.ensure([], function(require){}打包後的結果,且該函數會返回一個Promise

webpack官方是這麼介紹require.ensure的,核心內容大體以下:

Split out the given dependencies to a separate bundle that will be loaded asynchronously. When using CommonJS module syntax, this is the only way to dynamically load dependencies. Meaning, this code can be run within execution, only loading the dependencies if certain conditions are met.

中文翻譯:require.ensure 將給定的依賴項拆分爲一個個單獨的包,且該包是異步加載的。當使用commonjs模塊語法時,這是動態加載依賴項的惟一方法。也就是說,代碼能夠在執行過程當中運行,只有在知足某些條件時才加載這些被拆分的依賴項。

綜上,bundle-loader?lazy利用require.ensure來實現代碼的按需加載JS、CSS文件,待請求到文件內容後再使用HOC來進行渲染。

react.lazy

React 在16.6版本時正式發佈了React.lazy方法與Suspense組件,在這裏簡單介紹下使用方法:

import React, { lazy,Suspense } from 'react'; // 引入lazy
const NotFound = lazy(() => import('./containers/notFound')); // 懶加載組件
// 以一個路由配置爲例
<Route
    component={props => (
        <NotFound {...props} />
    )}
    path='路由路徑'
/>
// Routes.js 全部路由配置
// RouteConfig 是一組內部每一項結構爲上述Route元素的數組
RouteConfig.map((Item, i) => {
    return (
        <Route
            component={props => (
                <Item.component {...props} />
            )}
            path={Item.path}
            key={`${i}`}
        />
    );
});
// 在BrowserRouter中使用
 <BrowserRouter>
    <div className="ui-content">
    <!-- 這裏使用 Suspense 渲染React.lazy方法包裹後的組件 -->
    <!-- fallback 中的內容會在加載時渲染 -->
        <Suspense
            fallback={<div>loading...</div>}
        >
            <Switch>
                {
                    Routes // Routes 是上面RouteConfig
                }
                <Redirect to="/demo" />
            </Switch>
        </Suspense>

    </div>
</BrowserRouter>
複製代碼

效果驗證以下圖:

能夠發現React.lazy與bundle-loader分離打包出的體積相差無多,大膽的猜想其實二者用到的原理是同樣的。

React.lazy 相關源碼

以這句JS代碼爲例進行分析 const NotFound = lazy(() => import('./containers/notFound'));

首先通過webpack處理後的import代碼

ƒ() {
    // 這裏 4 的參數含義是 請求4.js
    return __webpack_require__.e(4).then(__webpack_require__.bind(null,"./src/containers/notFound/index.js"));
}
複製代碼

實際上異步加載與React沒有太大關係,而是由webpack支持。

接下來看React.lazy源碼,下述目錄皆爲相對路徑,源碼爲TS,有興趣的小夥伴也能夠在React官方github上看看

// packages/react/src/ReactLazy.js
import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent';
// 注意,這裏僅是導入類型
import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; 
// 一個Symbol對象
import warning from 'shared/warning';

export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
    // ctor 是一個返回Thenable類型的函數,而整個lazy是返回一個LazyComponent類型的函數
  let lazyType = {
    $$typeof: REACT_LAZY_TYPE,
    _ctor: ctor,
    // React uses these fields to store the result.
    _status: -1,
    _result: null,
  };
  if (__DEV__) {
    // 此處是一些關於lazyType的defaultProps與propTypes的邏輯處理
    // 不是本文的重點,此處忽略
  }

  return lazyType;
}
複製代碼

咱們能夠發現,其實React.lazy整個函數實質上返回了一個對象(下文以LazyComponent命名),咱們能夠在控制檯查看下,如圖:

React封裝好LazyComponent後會在render時進行相關初始化處理(此render非咱們React內常寫的render方法,而是React對組件渲染處理方法),React會判斷對象中的 $$typeof 是否爲 REACT_LAZY_TYPE ,若是是,會進行相應的邏輯處理。(實際上不僅是render,還有在組件類型爲memoComponent時,會在某個時期對LazyComponent處理)

// packages/react-dom/src/server/ReactPartialRenderer.js
import {
  Resolved, // 值爲1
  Rejected, // 值爲2
  Pending, // 值爲0
  initializeLazyComponentType,
} from 'shared/ReactLazyComponent';

render() {
    // ... 省略前置代碼
    case REACT_LAZY_TYPE: {
        const element: ReactElement = (nextChild: any);
        const lazyComponent: LazyComponent<any> = (nextChild: any).type;
        initializeLazyComponentType(lazyComponent); // 初始化LazyComponent
        switch (lazyComponent._status) {
            case Resolved: {
                // 若是是異步導入成功,設置下個渲染的element
                const nextChildren = [
                    React.createElement(
                        lazyComponent._result,
                        Object.assign({ref: element.ref}, element.props),
                    ),
                ];
                const frame: Frame = {
                    type: null,
                    domNamespace: parentNamespace,
                    children: nextChildren,
                    childIndex: 0,
                    context: context,
                    footer: '',
                };
                if (__DEV__) {
                    ((frame: any): FrameDev).debugElementStack = [];
                }
                    this.stack.push(frame);
                    return '';
                };
            case Rejected:
                throw lazyComponent._result;
            case Pending:
            default:
                invariant(
                    false,
                    'ReactDOMServer does not yet support lazy-loaded components.',
                );
        }
    }
}
複製代碼

咱們重點關注initializeLazyComponentType這個函數

// packages/shared/ReactLazyComponent.js
export const Uninitialized = -1;
export const Pending = 0;
export const Resolved = 1;
export const Rejected = 2;

export function initializeLazyComponentType( lazyComponent: LazyComponent<any>,): void {
  if (lazyComponent._status === Uninitialized) { // 若是是未初始化
    lazyComponent._status = Pending;
    const ctor = lazyComponent._ctor;
    // 調用函數 實質調用 () => import('xxx'); 
    // 此處會調用通過webpack打包後的import代碼,即本小節第一部分代碼
    const thenable = ctor();  // 返回了一個promise,上文已經提過
    lazyComponent._result = thenable; // 將這個promise 掛載到LazyComponent的result上
    thenable.then( 
        moduleObject => {
            if (lazyComponent._status === Pending) {
            const defaultExport = moduleObject.default;
            if (__DEV__) {
                if (defaultExport === undefined) {
                    // ... 代碼省略,主要作了些異常處理
                }
            }
                lazyComponent._status = Resolved;
                lazyComponent._result = defaultExport;
            }
        },
        error => {
            if (lazyComponent._status === Pending) {
                lazyComponent._status = Rejected;
                lazyComponent._result = error;
            }
        },
    );
  }
}
複製代碼

initializeLazyComponentType函數咱們能夠看出,作的操做主要是更新LazyComponent中的_status與_result,_status天然不用說,代碼很清晰,重點關注_result。

根據上述代碼,能夠簡單用下述方法查看下實際上的_result

const notFound = lazy(() => {
    const data = import('./containers/notFound');
    return data;
});
notFound._ctor().then((data) => {
    console.log(data);
    // 輸出
    // Module {
        // default: ƒ notFound()
        // arguments: (...)
        // caller: (...)
        // length: 0
        // name: "notFound"
        // prototype: Component {constructor: ƒ, componentDidMount: ƒ, render: ƒ}
        // __proto__: ƒ Component(props, context, updater)
        // [[FunctionLocation]]: 4.js:56
        // [[Scopes]]: Scopes[3]
        // Symbol(Symbol.toStringTag): "Module"
        // __esModule: true
    // }
});
複製代碼

咱們看到data中的數據就是咱們要真正要渲染的組件內容,能夠回頭看看上面的render中的這一段代碼,React將Promise獲得的結果構建成一個新的React.Element。在獲得這個能夠正常被渲染的Element後,React自身邏輯會正常渲染它。至此,完成整個過程。

case Resolved: {
    const nextChildren = [
        React.createElement(
            lazyComponent._result, // 即上述的Module
            Object.assign({ref: element.ref}, element.props),
        ),
    ];
}
複製代碼

可能有點繞,爲此梳理出一個邏輯圖(這裏假設加載文件的條件是路由匹配正確時),以下:

最後

從二者的源碼其實能夠看出,React.lazy與bundle-loader?lazy底層懶加載實現的方法都是依靠webpack的Require.ensure支持。React.lazy與bundle-loader?lazy二者區別主要在於渲染的處理:

  • React.lazy在拿到文件後,還須要進行一次React.createElemnt,接着再渲染;
  • bundle-loader?lazy在拿到請求好的文件時,實際上已是一個能夠直接被React渲染的代碼塊。

除此以外,React.lazy渲染的邏輯都交由React自身來控制,bundle-loader?lazy渲染邏輯交由開發者進行控制(即本文提的輔助渲染的HOC)。這意味着,使用bundle-loader?lazy方式時咱們能夠有更多的自定義發揮空間。

附錄

參考資料

相關文章
相關標籤/搜索