最近React源码群里有个同学去大厂面试被问到一道经常在各种面经中出现的问题:
据说标准答案是:React是异步更新,依据是:
触发如下点击事件后console.log打印的结果不是1。
onClick() {
this.setState({a: 1});
console.log(this.state.a);
}
紧接着这题,面试官又会问:“那如何同步更新呢?”
据说标准答案是:把setState放到setTimeout中:
onClick() {
setTimeout(() => {
this.setState({a: 1});
console.log(this.state.a);
})
}
如果能回答到这,面试官会露出欣慰的笑:小伙子很懂React嘛。
可是,我们真的会用到这么hack的写法么?这个所谓“标准答案”又一定是对的么?
setState是同步还是异步的?
首先这个问法就很有问题。这个问法想表达的是:
在某个组件中调用
this.setState会让该组件对应视图同步更新还是异步更新?
这里隐含的前提是:视图更新是以组件为粒度更新。
我们可以用一个公式描述React:
UI = f(state)
视图(UI)可以表示为状态(state)通过某个函数(f)的映射。
其中:
-
UI是反映页面的DOM树 -
f是React的内部运行流程 -
state是状态的集合
从公式可以看出,每次调用this.setState,整个React应用会执行一遍更新流程,将状态映射为视图。
只不过恰巧在映射过程中,这个组件的state改变,所以组件对应的视图会映射为新的视图。
最终表现为:视图其他部分不变,该组件视图更新。
从这个角度看,这道面试题就完全没有意义了。
既然每次更新都是整个视图层面,而不是某个组件,那么更新是同步还是异步都无所谓了。
毕竟对组件的操作完全应该在各个生命周期函数(或者hooks)中进行。
从源码角度讲
那为什么被setTimeout包裹的this.setState可以在当前调用栈获取到更新后的state?
其实这么问也是有问题的。
之所以会有这样的现象,是因为老版本React内部实现的原因。
在v17以后,开启Concurrent Mode,即使在setTimeout中调用this.setState,在当前调用栈获也无法获取更新后的state。
简单讲一下,在老版React中,事件回调会被包裹在batchedUpdates函数中执行。
代码类似如下:
function batchedUpdates(fn) {
let prevContext = context;
context |= batchedContext;
try {
fn();
} finally {
context = prevContext;
}
}
被包裹的事件回调fn通过全局变量context就能获取当前是否处于batchedContext的上下文环境。
如果处于该环境就执行一些批处理操作。
而是否用setTimeout包裹this.setState影响的,就是在执行this.setState时全局变量context是否包含batchedContext。
在新版React中,batchedUpdates已经被lane优先级模型替代,不会再有batchedContext的限制。
可见,仅仅是React内部实现的缺陷,却被拿来当高深的面试题,只能说,