Skip to content

React.lazy 解析

React.lazy

这个 API 应该并不陌生,在官方文档中有这样一段话👇🏻

对你的应用进行代码分割能够帮助你“懒加载”当前用户所需要的内容,能够显著地提高你的应用性能。尽管并没有减少应用整体的代码体积,但你可以避免加载用户永远不需要的代码,并在初始加载的时候减少所需加载的代码量。

关键词“代码分割”、“懒加载”、“避免加载不需要的代码”。

  1. 代码分割——即它会将你需要“懒加载”的内容,单独分拆成一个文件,方便使用。
  2. 懒加载、避免加载不需要的代码——即只有需要它时候才加载。

代码分割案例

jsx
// 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

打包后lazy打包 对比同步组件打包👇🏻 lazy打包

💡

验证第一个关键词。 当你的文件足够大的时候,这种优势就会凸显出来,它不会阻塞你关键代码的执行,即使可能 Modal.js 很大的时候,只要 index.js 加载完了,页面就可以进行渲染。 但如果打包在一个文件里的时候,可能一开始不需要它,但你也要把它一起下载下来,尤其大项目比较明显。 因此你会发现 webpack 打包后会出现很多个 chunk ,就是实现按需加载(懒加载也可以理解为按需加载)。

缺点:新增多一条 HTTP 请求,因此不要滥用,虽然现在浏览器可以同时并发多个 HTTP 请求。

懒加载案例

上面的案例,你查看控制会发现,怎么它一开始就加载了,不是说懒加载吗? “是的,它是懒加载,只不过你一开始就使用它,当然它加载啦!”

🔧改造一下

jsx
// 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,而是当你点击按钮后,才发起请求加载懒加载

思考一个场景: 假设页面上有一个支付按钮,点击它,弹起弹窗,选择支付方式...等等。 详细这样的需求,大家都遇见过做过,其实这里有一个新人比较容易犯的错误,你以为的懒加载,其实并没有懒加载。

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

懒加载

打开控制台,观察 ElementsNetwork, 虽然没有渲染出 DOM,但实际已经把文件请求回来了! 📩 可以把这一行为理解为简单版的预加载(后面会讲)。

如果我要懒加载呢?

jsx
// 代码片段

// 只需在渲染 Modal 的时候加多一个条件即可
<Suspense fallback={'loading'}>
	{visible && <Modal visible={visible} onCancel={handleClose} />}
</Suspense>

不过这又有一个问题,每次隐藏都会把 Modal 销毁掉,怎么可以仅在第一次显示时生成,隐藏只是 display: none; 这由你们自己思考一下改造一下。🤔🤔🤔

异步、懒加载、预加载

这里先总结三个概念(博主的理解,如有错误,欢迎指出)~

  1. 异步引入or异步加载:即通过异步的方式,不阻塞页面主要逻辑下进行。比如实现异步的方式有 promiseasync/awaitwebworkcallback 等。
  2. 懒加载:在需要的时候才去加载(资源)。
  3. 预加载:提前加载下一步使用的资源——提前加载的懒加载。

上面有一个案例为什么说是简单版的预加载呢? 首先它是异步加载进来的,并且资源也不是主流程必须得,最后它没有渲染,而是单击显示后才进行渲染,即只是单纯把资源加载回来,并没有进行渲染。 只是稍微高级一点的预加载是加入一些手段,比如分析用户常用操作,分析用户惯性操作(如通常进入这个页面后大概率是为了点击什么等),来进行有目的的在空闲时将资源预先加载回来,等用户正在触发到的时候,可以减少加载时间,增加用户良好体验。

懒加载是优化手段中听得最多一种,比如图片的懒加载,就是监听 dom 可见性加图片异步加载完成。(代码略,网上一堆案例)

其实在我的观点上,我更愿意称 React.lazy 是异步加载的手段而不是懒加载,虽然它确实有懒加载的那味。

源码解析

react lazy 源码

🔍🔍🔍 仅贴部分代码

ts
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;
}
ts
// 核心的代码
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;
	}
}

其实这段代码很好理解,本质就是 promiseimport 执行就是返回一个 promise 对象,Suspense 组件通过监听 promise 的状态进行选择渲染。比如执行成功后返回 moduleObject.default 组件的默认导出。

Released under the MIT License.