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 <div>{result} world</div>;
};
const SuspenseDemo = () => {
return (<Suspense fallback={<div>loading...</div>}>
<Greeting />
</Suspense>
);
};
export default SuspenseDemo;
这里就有意思了,应用 Suspense 来获取数据,既然数据是在 render 函数(或者像下面例子一样在函数类型组件中)应用,那么获取数据的过程必定是在 render 阶段,然而,获取数据的过程是要调用 AJAX 的啊,AJAX 是副作用操作,这不就和“render 阶段不能做有副作用操作“的规定矛盾了吗?
确实有点矛盾。
Reac 开发人员对这个的解释是这样:
简略说来,就是升高了要求,只须要 render 阶段的操作是”幂等“(indempotent)就能够了。
所谓幂等,就是一次调用和 N 次调用产生一样的后果。
还是举例来说吧。
// 这是纯函数,没有副作用
function foo1(a, b) {return a + b;}
// 这不是纯函数,有副作用,而且不幂等
function foo2(a, b) {sendAjax(a + b);
return a + b;
}
// 这不是纯函数,有副作用,然而——幂等
let called = false;
function foo3(a, b) {if (!called) {sendAjax(a + b);
}
called = true;
return a + b;
}
下面的 foo3 这个函数,确实有副作用,然而,利用代码奇妙地防一手,只让第一次调用收回 AJAX,之后的调用就不发 AJAX 了,这样,调用多少次,产生的成果都一样,这就是”幂等“。
幂等尽管没有纯函数那么纯,然而也足够好了,至多对于 React 这样无奈做到”纯“的框架,这也是最好的后果。
试玩版 react-cache 的 unstable_createResource 函数,承受一个返回 Promise 的函数为参数,获取的 Promise 是会被 cache 住的,所以,尽管会被打断反复屡次,resource.read()如果有返回后果,那么返回的后果都是一样,也就达到了”幂等“的成果。
这么一看,矛盾也就解决了。
总结一下,实际上咱们要把”在 render 阶段的所有生命周期函数不要做有副作用的操作,这些函数必须是纯函数 “这个要求改一下,改成” 在 render 阶段的所有生命周期函数都应该幂等“。
尽管一说 React 往往都说要说“函数式编程”,然而 React 真的先天和崇尚纯函数的“函数式编程”有很大间隔,包含 Hooks,外表上看推崇函数式组件,仿佛是向函数式编程迈进了一步,然而,所有的 Hooks 函数都是有状态的,怎么能算纯函数呢。
当然,有洁癖一样谋求纯函数也没有必要,既然“幂等”可能解决问题,咱们也乐见其成。
印证了那句老话:只有你降低标准,会发现世界恍然大悟:)
P.S. 这篇文章里的背景常识如果不分明,就去看我之前的 Live 吧,该说的知识点我都说过了。
《疾速理解 React 的新性能 Suspense 和 Hooks》
《深刻了解 React v16 新性能》
《帮忙你深刻了解 React》