最近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
内部实现的缺陷,却被拿来当高深的面试题,只能说,