React.lazy 解析
React.lazy
这个 API 应该并不陌生,在官方文档中有这样一段话👇🏻
对你的应用进行代码分割能够帮助你“懒加载”当前用户所需要的内容,能够显著地提高你的应用性能。尽管并没有减少应用整体的代码体积,但你可以避免加载用户永远不需要的代码,并在初始加载的时候减少所需加载的代码量。
关键词“代码分割”、“懒加载”、“避免加载不需要的代码”。
- 代码分割——即它会将你需要“懒加载”的内容,单独分拆成一个文件,方便使用。
- 懒加载、避免加载不需要的代码——即只有需要它时候才加载。
代码分割案例
// App.jsx
import { Suspense, lazy } from 'react'
const Modal = lazy(() => import('./Modal'))
function App() {
return (
<div className="App">
<Suspense fallback={'loading'}>
<Modal />
</Suspense>
</div>
)
}
export default App
// Modal.jsx
const Modal = () => {
return <div>
Modal组件
</div>
}
export default Modal
打包后: 对比同步组件打包👇🏻
💡
验证第一个关键词。 当你的文件足够大的时候,这种优势就会凸显出来,它不会阻塞你关键代码的执行,即使可能 Modal.js
很大的时候,只要 index.js
加载完了,页面就可以进行渲染。 但如果打包在一个文件里的时候,可能一开始不需要它,但你也要把它一起下载下来,尤其大项目比较明显。 因此你会发现 webpack
打包后会出现很多个 chunk
,就是实现按需加载(懒加载也可以理解为按需加载)。
缺点:新增多一条 HTTP 请求,因此不要滥用,虽然现在浏览器可以同时并发多个 HTTP 请求。
懒加载案例
上面的案例,你查看控制会发现,怎么它一开始就加载了,不是说懒加载吗? “是的,它是懒加载,只不过你一开始就使用它,当然它加载啦!”
🔧改造一下
// App.jsx
import { Suspense, lazy, useState } from 'react'
const Modal = lazy(() => import('./Modal'))
function App() {
const [visible, setVisible] = useState(false)
return (
<div className="App">
<button onClick={() => setVisible(!visible)}>{ visible ? '隐藏' : '显示' } modal</button>
<Suspense fallback={'loading'}>
{ visible && <Modal />}
</Suspense>
</div>
)
}
export default App
点击前并没有加载 Modal.jsx
,而是当你点击按钮后,才发起请求加载。
思考一个场景: 假设页面上有一个支付按钮,点击它,弹起弹窗,选择支付方式...等等。 详细这样的需求,大家都遇见过做过,其实这里有一个新人比较容易犯的错误,你以为的懒加载,其实并没有懒加载。
// App.jsx
import { Suspense, lazy, useState, useCallback } from 'react'
const Modal = lazy(() => import('./Modal'))
function App() {
const [visible, setVisible] = useState(false)
const handleClose = useCallback(() => {
setVisible(false)
}, [])
return (
<div className="App">
<button onClick={() => setVisible(!visible)}>{visible ? '隐藏' : '显示'} modal</button>
<Suspense fallback={'loading'}>
<Modal visible={visible} onCancel={handleClose} />
</Suspense>
</div>
)
}
export default App
// Modal.jsx
import { Modal } from 'antd';
const PayModal = (props) => {
const { visible, onCancel } = props
return <Modal title="Pay Modal" open={visible} onCancel={onCancel}>
<p>支付弹窗</p>
</Modal>
}
export default PayModal
打开控制台,观察 Elements
和 Network
, 虽然没有渲染出 DOM,但实际已经把文件请求回来了! 📩 可以把这一行为理解为简单版的预加载(后面会讲)。
如果我要懒加载呢?
// 代码片段
// 只需在渲染 Modal 的时候加多一个条件即可
<Suspense fallback={'loading'}>
{visible && <Modal visible={visible} onCancel={handleClose} />}
</Suspense>
不过这又有一个问题,每次隐藏都会把 Modal 销毁掉,怎么可以仅在第一次显示时生成,隐藏只是
display: none;
这由你们自己思考一下改造一下。🤔🤔🤔
异步、懒加载、预加载
这里先总结三个概念(博主的理解,如有错误,欢迎指出)~
- 异步引入or异步加载:即通过异步的方式,不阻塞页面主要逻辑下进行。比如实现异步的方式有
promise
、async/await
、webwork
、callback
等。 - 懒加载:在需要的时候才去加载(资源)。
- 预加载:提前加载下一步使用的资源——提前加载的懒加载。
上面有一个案例为什么说是简单版的预加载呢? 首先它是异步加载进来的,并且资源也不是主流程必须得,最后它没有渲染,而是单击显示后才进行渲染,即只是单纯把资源加载回来,并没有进行渲染。 只是稍微高级一点的预加载是加入一些手段,比如分析用户常用操作,分析用户惯性操作(如通常进入这个页面后大概率是为了点击什么等),来进行有目的的在空闲时将资源预先加载回来,等用户正在触发到的时候,可以减少加载时间,增加用户良好体验。
懒加载是优化手段中听得最多一种,比如图片的懒加载,就是监听 dom 可见性加图片异步加载完成。(代码略,网上一堆案例)
其实在我的观点上,我更愿意称 React.lazy
是异步加载的手段而不是懒加载,虽然它确实有懒加载的那味。
源码解析
🔍🔍🔍 仅贴部分代码
export function lazy<T>(
ctor: () => Thenable<{default: T, ...}>, // 返回 Thenable 可以查看 shared/ReactTypes,其实对应 lazyInitializer 各种状态的返回值
): LazyComponent<T, Payload<T>> { // 返回一个懒加载组件
const payload: Payload<T> = {
// We use these fields to store the result.
_status: Uninitialized, // 当前状态
_result: ctor, // 函数,如 ()=>import('./Modal.jsx')
};
const lazyType: LazyComponent<T, Payload<T>> = {
$$typeof: REACT_LAZY_TYPE,
_payload: payload,
_init: lazyInitializer, // 核心代码
};
return lazyType;
}
// 核心的代码
function lazyInitializer<T>(payload: Payload<T>): T {
// 假如当前状态是没有初始化
if (payload._status === Uninitialized) {
// 获取当期结果,此时是 ctor,即()=>import('./xxx.jsx')
const ctor = payload._result;
// 执行函数
const thenable = ctor();
// Transition to the next state.
// This might throw either because it's missing or throws. If so, we treat it
// as still uninitialized and try again next time. Which is the same as what
// happens if the ctor or any wrappers processing the ctor throws. This might
// end up fixing it if the resolution was a concurrency bug.
thenable.then(
moduleObject => {
// 无论当前状态是等待还是未初始化, 执行完毕都修改为 成功状态,结果为 组件返回值
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
const resolved: ResolvedPayload<T> = (payload: any);
resolved._status = Resolved;
resolved._result = moduleObject;
}
},
error => {
// 转换状态,报错
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
const rejected: RejectedPayload = (payload: any);
rejected._status = Rejected;
rejected._result = error;
}
},);
// 如果当前状态是未初始化,则修改为等待状态,结果为 promise 对象
if (payload._status === Uninitialized) {
// In case, we're still uninitialized, then we're waiting for the thenable
// to resolve. Set it as pending in the meantime.
const pending: PendingPayload = (payload: any);
pending._status = Pending;
pending._result = thenable;
}
}
if (payload._status === Resolved) {
const moduleObject = payload._result;
return moduleObject.default;
} else {
throw payload._result;
}
}
其实这段代码很好理解,本质就是 promise
;import
执行就是返回一个 promise
对象,Suspense
组件通过监听 promise
的状态进行选择渲染。比如执行成功后返回 moduleObject.default
组件的默认导出。