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听说是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 在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分离打包出的体积相差无多,大胆的猜想其实二者用到的原理是同样的。
以这句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自身来控制,bundle-loader?lazy渲染逻辑交由开发者进行控制(即本文提的辅助渲染的HOC)。这意味着,使用bundle-loader?lazy方式时咱们能够有更多的自定义发挥空间。
参考资料