React Lazy 和 Suspense
在React应用中,有些组件可能不经常用到,比如法律条款的弹窗,我们几乎不看,这些组件也就没有必要首次加载,可以在点击它们的时候再加载,这就需要动态引入组件,需要组件的时候,才引入组件,加载它们,进行渲染,也称为懒加载。怎么动态引入组件呢?先看在普通的JS中,如何动态引入一个模块?是使用import()函数。比如,在下面的index.html中,
<body> <div id="message"></div> <button id="clickShow">点击显示</button> <script src="./index.js"></script> </body>
点击button,id为message的div会显示‘Hello’,假设显示内容的JS文件是showMessage.js
export function sayHi(id, msg) { document.getElementById(id).innerHTML = msg; }
那么在index.js中,button的click事件处理函数中,import('./showMessage.js'),
function handleClick() { import("./showMessage.js") .then((module) => { module.sayHi("message","Hello"); }); } document .getElementById("clickShow") .addEventListener("click", handleClick);
import()函数返回的是promise, promise resolve后返回的是module对象(showMessage.js中暴露出来的对象),通过module对象就可以调用showMessage中暴露的方法。在React中,也是通过添加点击事件来动态引入组件吗?有更好的方法,因为React是数据驱动,状态的改变,渲染不同的组件,就可以了。比如,弹窗,在包含弹窗组件的父组件中,设置state来控制弹窗的显示和隐藏,state是true,弹窗显示,state为false, 弹窗隐藏,但组件怎么表示出它是动态引入的组件?React.lazy()方法,它接受一个函数,返回一个组件,表示组件是动态加载的。函数的格式是() => import(要引入组件所在的js文件),js文件必须用export default 暴露出组件。假设Model.js中 export default function Module() {},
const LazyModel = React.Lazy(() => import('./Model.js');
LazyModel组件就表示它要动态加载。当state是true时,显示LazyModel就可以了。但这会引出另外一个问题,state为true,React就会渲染LazyModel组件,但组件并没有加载完成,因为它是动态加载的,React是无法渲染的,但React肯定要渲染点什么,怎么办?怎么才能把动态加载的组件和React的渲染过程结合起来?需要告诉React,当组件没有加载完成时,无法渲染的时候,它要渲染什么,这就是suspense组件。用suspense组件把动态渲染的组件包起来,同时给suspense组件的fallback属性赋值,当React渲染动态组件的时候,它就会到组件的上层找Suspense组件,把fallback渲染出来。
npx create-react-app react-lazy, 创建如下项目
点击button的时候,动态显示日历。日历组件使用react-datepicker,npm install react-datepicker --save。App.js
import './App.css'; import { CalendarWrapper } from './CalenderWrapper' function App() { return ( <div> <main>Main App</main> <aside> <CalendarWrapper /> <CalendarWrapper /> </aside> </div> ); } export default App;
App.css
main { width: 400px; height: 60px; background-color: #643797; text-align: center; line-height: 60px; } aside { display: flex; justify-content: space-between; width: 400px; margin-top: 10px; } .CalendarWrapper { display: flex; justify-content: center; align-items: center; width: 180px; height: 100px; background-color: #8aceea; }
CalendarWrapper.js
import { lazy, useState } from 'react'; const LazyCalendar = lazy(() => import("./Calendar.js")); export function CalendarWrapper() { const [isOn, setIsOn] = useState(false); return <div className='CalendarWrapper'> { isOn ? ( <LazyCalendar /> ) : ( <div> <button onClick={() => setIsOn(true)}>Show Calendar</button> </div> ) } </div> }
Calendar.js
import DatePicker from "react-datepicker";
export default function Calendar() { const startDate = new Date(); return ( <DatePicker selected={startDate} /> ); }
const LazyCalendar = lazy(() => import("./Calendar.js")); 可以看到React.lazy函数接受一个函数,返回一个promise,拆分一下
const getPromise = () => import('./Calendar.js'); const LazyComponent = lazy(getPromise);
传给lazy函数getPromise()函数,React第一次渲染组件的时候,就会调用这个函数。getPromise()函数,返回一个promise,promise resolve后是一个模块,模块就是Calendar.js export default出来的模块。但是单击button后,有报错,因为React需要时间去加载组件,当渲染组件时,组件又没有加载完时,我们应该怎么做,最好显示一个loading提示,表示组件正在加载中。怎么样显示loading提示呢?使用Suspense组件,把Lazy组件包起来,
<aside> <Suspense fallback={<div>Loading...</div>}> <CalendarWrapper /> </Suspense> <Suspense fallback={<div>Loading...</div>}> <CalendarWrapper /> </Suspense> </aside>
使用fallback属性来表示Suspense组件将要渲染什么,直到所有后代Lazy组件都加载完成,返回UI。点击左边Show Calendar按钮,可以看到先渲染loading,再显示日历组件。当点击右边的Show Calendar按钮,你会发现,直接就渲染出了。异步加载的组件如果已经加载了,它就不需要再加载了。所以这两个异步加载的组件也可以放到一个<Suspense> 下面
<Suspense fallback={<div>Loading...</div>}> <CalendarWrapper /> <CalendarWrapper /> </Suspense>
当一个lazy组件第一次渲染时,React会沿着组件树向上查找Suspense组件,然后使用第一个找到的Suspense组件。Suspense组件就会在它子组件的地方渲染它的fallback UI,如果没有找到Suspense组件,React就会报错。
但React是怎么实现组件的动态加载的?我们可以认为动态加载的组件有一个内部状态,uninitialized, pending, resolved, or rejected。当React第一次渲染动态加载的组件时,组件是uninitialized状态,但它返回了一个函数,React会调用函数,来加载组件。函数就像getPromise
const getPromise = () => import("./Calendar"); const LazyCalendar = lazy(getPromise);
函数返回了一个promise,组件的状态就变成了pending 状态,等待promise完成。promise应该能resolve成一个模块,模块的default属性是一个component。一旦promise被resolve,React就会设置组件的状态是resolved,然后返回组件,表示可以渲染了。
if (status === "resolved") { return component; } else { throw promise; }
else则是和suspense组件进行沟通的关键,如果promise 没有resolve呢?它就会抛出promise, suspense组件就是捕获promise,然后渲染fallback UI(如果 promsise的状态是pending)。 所以对一个动态渲染的组件来说,如果它已经包含了一个组件,就是组件已经resolve了,React就会直接渲染组件;如果它包含一个正在pending的promise,组件就会抛出promise,Suspense组件就会捕获它。如果它包含一个函数,它就会调用函数来获取promise,把promise存在组件对象上,抛出promise,等到promise resolve后,调用promise的then方法 把promise resolve的组件存储在组件对象上。
如果promsise 被rejected,比如网络错误,suspense component 不会处理error,需要error boundary。react并没有提供一个组件来捕获子组件抛出来的错误,但是它提供了一系列的生命周期函数,如果在类组件中想要捕获错误,就要实现这些生命周期函数,如果一个类实现一个或几个这样的生命周期函数,它们就称为错误边界。如果你把组件包到错误边界中,如果被包裹的组件中抛出错误,它就是渲染fallback UI。
<ErrorBoundary> <Suspense fallback={<div>Loading...</div>}>
<CalendarWrapper />
<CalendarWrapper />
</Suspense>
</ErrorBoundary>
Error buond 组件
import { Component } from "react"; export default class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError() { return { hasError: true }; } render() { const { children, fallback = <h1>Something went wrong.</h1> } = this.props; return this.state.hasError ? fallback : children; } }
当React去渲染动态加载的组件时,它先判断组件的状态,如果动态引入的组件已经加载完了,直接渲染组件。如果组件还在pending状态,React就会抛出动态引入的promise,如查promise rejected,需要一个error bound 来捕获异常,并渲染fallback UI。