💡 The better you understand an abstraction, the more effective you will be at using it.
首先,我们来达成一个共识:延迟协调 (reconciliation) 来进行批量 (batch) 更新是必要的。也就是说:如果 setState
会触发同步的重渲染 (re-rendering),则会导致页面性能下降。
举个例子:我们在 Parent
和 Child
组件分别注册了 onClick
回调函数来调用 setState
,当点击 Child
时,由于事件冒泡机制,两者的 setState
都会被调用,我们不想重渲染 Child
两次,所以会选择将它们标记为 dirty
,然后在合适的时机同时重渲染这两个组件。
你可能会有疑惑:为什么我们不能在做批处理(不立即重渲染组件)的同时,调用 setState
同步更新 state
呢?主要有以下两点考量:
一、保证内部一致性
state
可以保证被同步更新,props
却不能:你不知道下一个状态的 props
除非重渲染父组件,但如果同步渲染父组件的话批处理就不复存在了。
现在 React 提供的 state
, props
, refs
是保证内部一致性的。这意味着,如果组件只使用这些值,它们肯定会引用一棵完全协调的树(即使它是该树的旧版本)。 为什么这很重要?FIXME
当组件只使用 state
,而且 setState
同步更新 state
,会出现如下的表现:
console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2
然而,当这个 state
需要在多个兄弟组件内使用时,通常的做法是进行变量提升,把 state
提升至父组件处:
onIncrement() { // 定义在父组件中
this.setState({ value: this.state.value + 1 });
}
this.props.onIncrement(); // onIncrement 作为 props 被传给子组件来更新 state
以上的 变量提升 **是 React 开发中常使用的模式,但它不符合同步更新子组件的预期!
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
这是因为在上面提到的模型中:state
会被立即更新,而 props
不会。如果要同步更新 props
,就要重渲染父组件,这意味着我们要放弃批量更新策略,导致性能严重下降。
这个例子并非只存在于理论中,事实上 React Redux 的变量绑定曾经就有这种问题,因为 Redux 将 React props
和 非 React state
混合在一起。
那么 React 是如何解决上述问题的呢?在 React 内部,state
和 props
只会在协调阶段结束后同时更新。所以在上面的例子中,不管在变量提升前后,value
的值都会是 0
。
总结:React 不总会带来最简洁的代码,但它拥有内部一致性来保证变量提升是安全的。
二、支持并发更新
React18 采取 “异步渲染” (async rendering)机制。React 会根据不同的任务如:DOM事件回调,网络请求回调、动画回调等,给 setState
赋予不同的优先级。
举例来说:当你在 联想输入框 输入时,onInput
的 setState
回调应该被同步更新,而从网络返回的联想词列表最好在输入停止后一定时间内延迟渲染 (denounce)。避免列表渲染占用主线程导致输入卡顿。
通过指定一些任务拥有低优先级,我们可以把这个任务分散成几个小任务在短时间内执行,使用户无感知。
可能这种性能优化听上去不是非常令人激动,但异步渲染的优点不只是性能优化,我们认为它是 React 组件模型表达能力的根本性转变。
考虑这样一个场景:当你要从一个视图跳转到另一个视图时,你通常会展示一个 spinner 来完成过渡,以此优化用户体验。
但如果这个跳转的动作足够快,突然出现又消失的 spinner 反而会使用户体验下降。更糟的是:如果你有好几层异步组件嵌套,就会出现一系列的 spinner 闪现。这即会让用户有一个不好的视觉体验,也会让你的应用变得更慢(DOM 的回流与重绘)。同时还会产生一堆样板代码。
如果现在告诉你:只要写一个 setState
来控制渲染新视图,React 可以在后台渲染并更新这个视图,是不是很酷。想象一下:你不用写额外的协调逻辑就可以让 React 展示一个 spinner(如果渲染超过了一定的时限),或者让 React 无缝过渡到新视图(当异步组件 ready)。更棒的是:当页面在准备跳转的时候,旧视图还是可交互的(用户可以选择切换到另一个视图)。