要想理解眼前发生的这魔幻的一切,我们还得从 setState 的工作机制里去找线索。
异步的动机和原理——批量更新的艺术
我们首先要认知的一个问题:在 setState 调用之后,都发生了哪些事情?你可能会更倾向于站在生命周期的角度去思考这个问题,得出一个如下图所示的结论:
从图上我们可以看出,一个完整的更新流程,涉及了包括 re-render(重渲染) 在内的多个步骤。re-render 本身涉及对 DOM 的操作,它会带来较大的性能开销。假如说“一次 setState 就触发一个完整的更新流程”这个结论成立,那么每一次 setState 的调用都会触发一次 re-render,我们的视图很可能没刷新几次就卡死了。这个过程如我们下面代码中的箭头流程图所示:
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate
});
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate
});
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate
});
事实上,这正是 setState 异步的一个重要的动机——避免频繁的 re-render
。
在实际的 React 运行时中,setState 异步的实现方式有点类似于 Vue 的 $nextTick
和浏览器里的 Event-Loop
:每来一个 setState,就把它塞进一个队列里“攒起来”
。等时机成熟,再把“攒起来”的 state
结果做合并,最后只针对最新的 state 值走一次更新流程
。这个过程,叫作“批量更新
”,批量更新的过程正如下面代码中的箭头流程图所示:
this.setState({
count: this.state.count + 1 ===> 入队,[count+1的任务]
});
this.setState({
count: this.state.count + 1 ===> 入队,[count+1的任务,count+1的任务]
});
this.setState({
count: this.state.count + 1 ===> 入队, [count+1的任务,count+1的任务, count+1的任务]
});
↓
合并 state,[count+1的任务]
↓
执行 count+1的任务
值得注意的是,只要我们的同步代码还在执行,“攒起来”这个动作就不会停止。(注:这里之所以多次
+1
最终只有一次生效,是因为在同一个方法中多次 setState 的合并动作不是单纯地将更新累加。比如这里对于相同属性的设置,React 只会为其保留最后一次的更新)。因此就算我们在 React 中写了这样一个 100 次的 setState 循环:
test = () => {
console.log(‘循环100次 setStat