React.lazy 解析
React.lazy
这个 API 应该并不陌生,在官方文档中有这样一段话👇🏻
对你的应用进行代码分割能够帮助你“懒加载”当前用户所需要的内容,能够显著地提高你的应用性能。尽管并没有减少应用整体的代码体积,但你可以避免加载用户永远不需要的代码,并在初始加载的时候减少所需加载的代码量。
关键词“代码分割”、“懒加载”、“避免加载不需要的代码”。
- 代码分割——即它会将你需要“懒加载”的内容,单独分拆成一个文件,方便使用。
- 懒加载、避免加载不需要的代码——即只有需要它时候才加载。
代码分割案例
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.jsxconst Modal = () => { return <div> Modal组件 </div>}export default Modal


💡
验证第一个关键词。
当你的文件足够大的时候,这种优势就会凸显出来,它不会阻塞你关键代码的执行,即使可能 Modal.js
很大的时候,只要 index.js
加载完了,页面就可以进行渲染。
但如果打包在一个文件里的时候,可能一开始不需要它,但你也要把它一起下载下来,尤其大项目比较明显。
因此你会发现 webpack
打包后会出现很多个 chunk
,就是实现按需加载(懒加载也可以理解为按需加载)。
缺点:新增多一条 HTTP 请求,因此不要滥用,虽然现在浏览器可以同时并发多个 HTTP 请求。
懒加载案例
上面的案例,你查看控制会发现,怎么它一开始就加载了,不是说懒加载吗? “是的,它是懒加载,只不过你一开始就使用它,当然它加载啦!”
🔧改造一下
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

思考一个场景: 假设页面上有一个支付按钮,点击它,弹起弹窗,选择支付方式…等等。 详细这样的需求,大家都遇见过做过,其实这里有一个新人比较容易犯的错误,你以为的懒加载,其实并没有懒加载。
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.jsximport { 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
组件的默认导出。