preface

React 16 has been released for a while. React 16.6 has react. lazy. Is it different from the bundle-loader used in webPack? What difference does lazy make? Hope this article can be shared clearly.

Environmental information:

System: macOS Mojave 10.14.2

Node: v8.12.0

React 16.9.0, React router-dom 4.3.1

role

We all know that in a single page application, Webpack will load all JS and CSS into a file, which can easily make the first screen rendering experience slow. And the React. Lazy and bundle – loader? Lazy solves this problem by requiring the associated static resources only when rendering is actually needed.

bundle-loader? lazy

Let’s start with the old-timer, bundle-loader, right? Lazy is said to be a product of Webpack2, the Chinese version of Webpack2. When used, it is necessary to use a HOC for auxiliary rendering. Specific examples are as follows:

// HOC to assist rendering
import React from 'react';
import PropTypes from 'prop-types';

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

    componentWillMount() {
        // Load the initial state
        this.load(this.props);
        // Set page title
        const { name } = this.props;
        document.title = name || ' ';
    }

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

    load(props) {
        // Reset the state
        this.setState({
            mod: null});// The component passed in
        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;
Copy the code

The route configuration is as follows:

// This code is RouteConfig in the code below
const routesArr = [
	{
	    component: 'Components used'.path: '/xxx'.name: "Page Name".icon: "Icon name used"},... ]Copy the code

Route Configuration code

RouteConfig.map((item, i) = > {
    return<Route // RouteConfig ={props => (// Bundle = HOC <Bundle name={item.name}) load={item.component} > {Container => <Container {... props} />} </Bundle> )} path={item.path} key={`${i}`} /> ) });Copy the code

The final effect is shown below, where 6.js and 6.css are the JS and CSS files corresponding to the rendered page

The core code

The bundle – loader? The lazy wrapped file console comes out with the following code

import notFound from 'bundle-loader? lazy! ./containers/notFound';
console.log(notFound)
// Delete the output of the comment
module.exports = function(cb) {
    // request 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);
}
Copy the code

The __webpack_require__.e in this code is actually webpack’s result of require.ensure([], function(require){}, and that function returns a Promise.

Webpack officially introduces Require. ensure as follows:

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.

Ensure splits a given dependency into a separate package that is loaded asynchronously. This is the only way to load dependencies dynamically when using commonJS module syntax. That is, the code can run during execution, loading the split dependencies only if certain conditions are met.

In conclusion, the bundle – loader? Lazy uses require.ensure to load JS and CSS files of code on demand, and then renders them using HOC after the file content is requested.

react.lazy

The React. Lazy method and Suspense components are available in version 16.6.

import React, { lazy,Suspense } from 'react'; / / into the lazy
const NotFound = lazy((a)= > import('./containers/notFound')); // Lazy loading of components
// Take a route configuration as an example<Route component={props => ( <NotFound {... RouteConfig ((Item, RouteConfig)) {props} /> // routes.js path=' Route '/> // routes.js i) => { return ( <Route component={props => ( <Item.component {... props} /> )} path={Item.path} key={`${i}`} /> ); }); // Use <BrowserRouter> <div className=" uI-Content "> <! Use Suspense render react. lazy to wrap components in Suspense. In Suspense fallback={<div>loading... </div> <Switch> {Routes // Routes are above RouteConfig} <Redirect to="/ x64 "/ > </ x64 > </div> </BrowserRouter>Copy the code

The effect verification is as follows:

React.lazy and bundle-loader use the same principle.

React.lazy

Const NotFound = lazy(() => import(‘./containers/ NotFound ‘));

First import code processed by Webpack

ƒ () {// request 4.js
    return __webpack_require__.e(4).then(__webpack_require__.bind(null."./src/containers/notFound/index.js"));
}
Copy the code

Actually asynchronous loading doesn’t have much to do with React, but is supported by WebPack.

The React. Lazy directory is TS. You can also see it on Github

// packages/react/src/ReactLazy.js import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent'; Import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; // a Symbol object import warning from 'shared/warning'; export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {// ctor is a function that returns type Thenable and lazy is a function that returns type LazyComponent let lazyType = {? typeof: REACT_LAZY_TYPE, _ctor: ctor, // React uses these fields to store the result. _status: -1, _result: null, }; If (__DEV__) {return lazyType; if (__DEV__) {return lazyType; }Copy the code

The react. lazy function essentially returns an object (named LazyComponent below) that can be viewed in the console, as shown in the figure below:

React encapsulates the LazyComponent and initializes it in render. React will render the LazyComponent. Whether typeof is REACT_LAZY_TYPE. If yes, the corresponding logic processing is performed. (Actually not only Render, but also LazyComponent for a time when the component type is memoComponent)

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

render() {
    / /... Omit pre-code
    case REACT_LAZY_TYPE: {
        const element: ReactElement = (nextChild: any);
        const lazyComponent: LazyComponent<any> = (nextChild: any).type;
        initializeLazyComponentType(lazyComponent); // Initialize LazyComponent
        switch (lazyComponent._status) {
            case Resolved: {
                // If the asynchronous import succeeds, set the element for the next rendering
                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.',); }}}Copy the code

We focus on initializeLazyComponentType this function

// 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) { // If it is uninitialized
    lazyComponent._status = Pending;
    const ctor = lazyComponent._ctor;
    // call () => import(' XXX ');
    // The Webpack-wrapped import code, the first part of this section, is called here
    const thenable = ctor();  // Returns a promise, as mentioned above
    lazyComponent._result = thenable; // Mount the promise to the result of the LazyComponent
    thenable.then( 
        moduleObject= > {
            if (lazyComponent._status === Pending) {
            const defaultExport = moduleObject.default;
            if (__DEV__) {
                if (defaultExport === undefined) {
                    / /... Code omission, mainly to do some exception processing} } lazyComponent._status = Resolved; lazyComponent._result = defaultExport; }},error= > {
            if(lazyComponent._status === Pending) { lazyComponent._status = Rejected; lazyComponent._result = error; }}); }}Copy the code

From initializeLazyComponentType function we can see that the operation of the main is to update the _status LazyComponent and _result _status natural needless to say, the code is very clear, focused on _result.

Based on the code above, you can simply look at the actual _result as follows

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

We can see that the data in data is the component content we’re actually rendering. Look back at the code in Render above. React builds the Promise result into a new React.Element. Once the Element is rendered properly, React logic renders it properly. At this point, complete the process.

case Resolved: {
    const nextChildren = [
        React.createElement(
            lazyComponent._result, // This is the Module above
            Object.assign({ref: element.ref}, element.props),
        ),
    ];
}
Copy the code

It may be a bit convoluted, so I have combed out a logical diagram (assuming that the loading condition of the file is when the route matches correctly) as follows:

The last

React.lazy and bundle-loader? Lazy The underlying lazy load implementation relies on WebPack’s require. ensure support. The React. Lazy and bundle – loader? The difference between lazy and lazy is mainly in the handling of the rendering:

  • After getting the file, react. createElemnt will be rendered again.
  • bundle-loader? Lazy is already a block of code that can be rendered directly by React when it gets the requested file.

In addition, React renders the lazy logic to React itself. Lazy rendering logic is left to the developer to control (that is, HOC to assist rendering, as discussed in this article). What does that mean, using bundle-loader? Lazy gives us more room for customization.

The appendix

The resources

  • React V16.6 Dynamic import, react. lazy(), Suspense, Error boundaries
  • bundle-loader
  • require.ensure
  • code-splitting-in-create-react-app
  • Require. Ensure; Asynchronous loading – code segmentation;