React的Suspense功能,简单说就是让组件渲染遇到需要异步操作的时候,可以无缝地“悬停”(suspense)一下,等到这个异步操作有结果的时候,再无缝地继续下去。
这里所说的异步操作,可以分为两类:
- 异步加载代码
- 异步加载数据
很好辨认,写程序嘛,折腾的无外乎就是“代码”和“数据”这两样东西。
为什么要异步加载代码呢?
本来,代码打包成一个文件就行,但是当代码量很庞大,而且也不是所有代码都是页面加载的时候就用得上,把他们拉进唯一的打包文件,除了打包过程简单没有任何好处。所以,为了榨取性能,就要考虑把代码打包成若干文件,这样每个打包文件就可以比较小,根据需要来加载。这是一个好主意,不过让每个开发人员都实现这套机制也是够扯的,所以,React就用Suspense提供了统一的无缝的代码分割(Code Splitting)兼异步加载方法,在v16.6.0就实现了这样的Suspense功能。
大家有兴趣自己去玩,这种Suspense不是今天要讲的重点,今天要讲的是“异步加载数据”的Suspense,也就是利用Suspense来调用服务器API之类的操作。
根据React官方的路线图,利用Suspense来做数据加载,要等到今年(2019)的中期才发布,你如果看到这篇文章比较晚,可能已经发布了。
今天要说的,是Suspense做数据加载,和React v16的重头戏异步渲染有一点矛盾的地方。
在之前的Live 《深入理解React v16新功能》中我说过,从React v16开始,一个组件的生命周期可以分为两个阶段:render阶段+commit阶段。在render阶段的生命周期函数,因为Fiber的设计特点,可能会被打断,被打断之后,会重新被调用;而commit阶段一旦开始,就绝不会被打断。render阶段和commit阶段的分界线是render函数,注意,render函数本身属于render阶段。
举个例子,一个组件被渲染,执行到render函数里面,这时候用户突然在某个input控件里输入了什么,这时候React决定去优先处理input控件里的按键事件,就会打断这个组件的渲染过程,也就是不管render返回啥,渲染过程都就此打住,不画了,专心去处理input控件的事情去了。等到那边的事情处理完,再来渲染这个组件,但是这时候从原来位置重新开始,那肯定是不靠谱的,因为刚才的按键事件处理可能改变了一些状态,为了保证绝对靠谱,React决定……还是从头走一遍吧, 于是,重新去调getDerivedStateFromProps、shouldComponentUpdate然后调用render。
看到没哟,render之前的生命周期函数都会被调用,而且,因为这种“打断”是完全是不可预期的,所以,现在就要求在render阶段的所有生命周期函数不要做有副作用的操作。
什么叫副作用?就是纯函数不该做的操作。
什么叫纯函数?就是除了根据输入参数返回结果之外,不做任何多与事情的操作。
如果一个函数修改全局变量,那就不是一个纯函数;如果一个函数修改类实例状态,那就不是一个纯函数;如果一个函数抛出异常,那就不是一个纯函数;如果一个函数通过AJAX访问服务器API,那就不是一个纯函数。
就拿访问服务器API为例,假如render阶段的生命周期函数做了访问服务器API的AJAX操作,那么,很有可能产生连续对服务器的访问,因为异步渲染下render阶段会被打断而重复执行啊。
class Foo extends React.Component {
shouldComoponentUpdate() {
callAPI(); // 你只看到了一行代码,但是可能会被多次调用
return true;
}
render() {
callAPI(); // 你只看到了一行代码,但是可能会被多次调用
return JSX;
}
}
再说一遍,在render阶段的所有生命周期函数不要做有副作用的操作,这些函数必须是纯函数。
那么,现在问题来了,使用Suspense来获取数据,会不会违反者这个规定呢?
虽然Suspense这方面的API还没有确定,但是代码形式还是明确的,利用试玩版的react-cache展示一下。
import React, { Suspense } from "react";
import { unstable_createResource as createResource } from "react-cache";
// 模拟一个AJAX调用
const mockApi = () => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("Hello"), 1000);
});
};
const resource = createResource(mockApi);
const Greeting = () => {
// 注意看这里,这里依然是在render阶段,可能会被重复调用哦!
const result = resource.read();
return {result} world;
};
const SuspenseDemo = () => {
return (
loading...