Daily-Interview-Question
Daily-Interview-Question copied to clipboard
第 19 题:React setState 笔试题,下面的代码输出什么?
1、第一次和第二次都是在 react 自身生命周期内,触发时 isBatchingUpdates 为 true,所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0。
2、两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。
3、setTimeout 中的代码,触发时 isBatchingUpdates 为 false,所以能够直接进行更新,所以连着输出 2,3。
输出: 0 0 2 3
1、第一次和第二次都是在 react 自身生命周期内,触发时 isBatchingUpdates 为 true,所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0。
2、两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。
3、setTimeout 中的代码,触发时 isBatchingUpdates 为 false,所以能够直接进行更新,所以连着输出 2,3。
输出: 0 0 2 3
请问一下大佬,isBatchingUpdates的判断条件是什么呀?
我理解的是:isBatchingUpdates 默认值为 false,当 react 自身的事件处理函数或 react 生命周期触发时,isBatchingUpdates 会被赋值为 true,当更新完成时又会被复原为 false。 @code-coder
1、第一次和第二次都是在 react 自身生命周期内,触发时 isBatchingUpdates 为 true,所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0。 2、两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。 3、setTimeout 中的代码,触发时 isBatchingUpdates 为 false,所以能够直接进行更新,所以连着输出 2,3。 输出: 0 0 2 3
请问一下大佬,isBatchingUpdates的判断条件是什么呀?
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/17
具体可以见 ReactFiberScheduler 中的 performWork 和 performSyncWork。 楼主说的基本是对的,如果有这种 async 的 work 就不执行 batch update 如果没有 async 的就执行 batch update,setTimeout 和 promise 这些要进入 EventLoop 队列的都会被认为是 async work。
哦也我做对了
@azl397985856 在React的setState函数实现中,会根据一个变量 isBatchingUpdate 来判断是直接同步更新this.state还是放到队列中异步更新 。React使用了事务的机制,React的每个生命周期和合成事件都处在一个大的事务当中。在事务的前置钩子中调用batchedUpdates方法修改isBatchingUpdates变量为true,在后置钩子中将变量置为false。原生绑定事件和setTimeout异步的函数没有进入到React的事务当中,或者当他们执行时,刚刚的事务已近结束了,后置钩子触发了,所以此时的setState会直接进入非批量更新模式,表现在我们看来成为了同步SetState。
很好理解的问题 正常情况下 react是会同步更新数据的 也就是说isBatchingUpdate触发变成true 但是 如果你将state的状态值加入到setTimeout的延迟处理队列中 打印出来的值就是setState之后的值,或者你同时调用setState的第二个参数 也会做到异步操作 获取设置之后的state value
题目呢???
做成了 0022 哎。竟然有isBatchingUpdates这种东西 学习了 @yhlben 。
我打印的 0 0 1 1
react-hooks时代貌似改写了这种方式,我用react-hooks写法写出来的结果都是0
@LastStranger
react-hooks时代貌似改写了这种方式,我用react-hooks写法写出来的结果都是0
更新的方式没有更改,首先是因为useEffect函数只运行了一次,其次setTimeout是个闭包,访问到的值一直是0(按照正常的写法setVal(val+1))。以例子来看的话,并没有执行更新的操作。
具体可以参考 issue讨论: https://github.com/facebook/react/issues/14010
恕我直言, 这其实就是react的bug, 整出这么大一堆概念, 就是想让你们接受这个bug, 不要踩这个坑
@LastStranger
react-hooks时代貌似改写了这种方式,我用react-hooks写法写出来的结果都是0
更新的方式没有更改,首先是因为useEffect函数只运行了一次,其次setTimeout是个闭包,访问到的值一直是0(按照正常的写法setVal(val+1))。以例子来看的话,并没有执行更新的操作。
具体可以参考 issue讨论: facebook/react#14010
应该是setVal((val) => val+1)
class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}
componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 1 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 2 次 log
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 3 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 4 次 log
}, 0);
}
render() {
return null;
}
};
为什么不是 0 0 3 4
react的state是为view服务的,state到view是响应式的,这种代码看瞎眼
为什么不是 0 0 3 4
我刚开始得到结果也是0034,看楼主的解答,是前两次setState
触发了批量更新,两次setState
合并成了一次.
在16.8.6源码中调试来看,这题和批量更新是没有关系的,通常批量更新是发生在react事件机制中即触发了事件回调会设置批量更新的标志。从源码中看,该题目的解释是:当前已经处于渲染的阶段,在这个过程中,框架会主动去合并在这期间触发的更新。 react的渲染主要包含两个阶段,render阶段和commit阶段,这两个阶段的包含在performWorkOnRoot方法中,该方法设置isRendering变量,表示当前是否处于渲染阶段。
react的setState可以认为是微任务的一次执行,但是我有点不太明白,setTimeout内的setState为什么没有批量更新?
对于 非concurrent 模式 答案 0023 屏幕显示 0 1 2 3
-
组件创建 屏幕显示 val 为 0
-
componentDidMount 执行 注意:(对于前两次之所以都是0 , 并不是 因为 isBatchingUpdates 为 true, 而是还在处理渲染状态,(只有事件系统等触发的才会isBatchingUpdates设置为true)所谓的渲染状态 也就是源码 isRendering 是 true , 这个变量是 虚拟DOM构建过程以及commit过程中都会维持 true 的状态。) (1)第一个setState触发,创建一个 调度任务,此时 调度链表中有 初始化创建的调度任务和这一次的调度任务。这次调度的过期时间因为是 非concurrent,所以是 sync。将 {val: 1} 对象放入到调度的updateQueue中。此时并不会马上执行,因为 第一个创建的调度还没有结束 isRendering 还是true。所以不会进入渲染阶段,屏幕还是 0. (2)然后打印 0. (3)第二个setState触发,创建一个调度任务, 此时的调度链表中有三个调度任务,分别为 初始化的时候调度、第一次setSate、第二次setSate. 这次调度的过期时间因为是 非concurrent,所以是 sync。将 {val: 1} 对象放入到调度的updateQueue中。同理 isRendering 还是 true,屏幕显示还是0. (4) 然后打印0. (5)初始化结束开始,isRendering 设置成 false。第一个初始化调度从调度链表中清除,开始进入处理剩下调度任务过程, 然后由于两次的调度任务一致, 所以,优先级一样,先处理第一次的调度任务,由于 第一次 setState 与第二次 优先级都一样, 所以在 render 阶段 收集 更新的时候都被处理了。 (6)屏幕显示 1(并不是 两次 setState 都会输出,而是由于过期时间都一样在 render 的收集更新的时候被处理调了)
-
setTime 执行 (1)由于此时 isRendering 是false, 所以不会批量更新,而是第三个 setState 调用的时候,过期时间为 sync。 然后创建调度,然后直接进入更新。 (2) 打印输出2. 屏幕显示 2 (3)同理,第四个setState 同步执行 (4)打印输出 3, 屏幕显示 3
对于 concurrent 模式 正常60hz刷新率情况下 答案 0011 屏幕显示 1 2
-
组件创建,进入调度执行,因为第一个创建的调度任务 肯定是在一帧空闲时间内执行的, 但是可能在 commit 阶段 检测到没有剩余时间了, 所以不会绘制到屏幕上。此时 isRendering 还是true
-
componentDidMount 执行 (1)执行第一个 setState, 此时 isRendering 是 true,仅仅是创建 调度任务 过期时间是一个异步的时间戳。并不会执行。 (2)打印出 0 (3)执行第二个 setState, 此时 isRedndering是true, 也是不会执行, 仅仅市创建调度任务, 注意的是,这里的异步过期时间与第一次的相同(因为 都是在一个 rendering 中)。 (4)打印出 0 (5)此时调度到第二个setState 的 调度任务的时候, 调度时间早已经过期了,所以退化成 同步的方式直接执行,屏幕绘制出 1。
-
setTimeout 执行 (1)第三个setState,进入到下一个时间循环的时候,在这之前可能执行了一次 state 变更,变成了1 所以 第三个创建调度任务的时候 val 可能是 2(只能说是可能, 因为不同的刷新率 和 组件大小都会影响了上一次的setState, 但是在目前实例仅仅有一个组件的情况下,state 肯定变成了 1 的因为必然执行了 render 过程) (2)打印出 1(也是可能值,只不过在 60hz 刷新率下 1 的概率最大) (3)第四个setState,创建调度任务,此调度任务放入队列,该过期时间可能与第三次的过期时间相同,因为异步计算时间都是在50ms 内的都作为一个过期时间。因为 两个过期时间一致, 所有中断了第三次的更新,进而更新第四次的。 (4)打印出 1 (5)屏幕显示 2
我打印的 0 0 1 1
因为你开启了 concurrent 模式 见我的回复 👆
为什么不是 0 0 3 4
虽然说是批量更新,但是两者的值指向同一个元素,因此后面的覆盖前面的
@wangmaoshu 那你的意思是说 如果是事件,比如点击的时候结果会不一样么?我用点击事件,做这个操作和用 didmount执行,都是一样的结果
react v17.0.0 0 0 2 3
import React from 'react';
class Exam19 extends React.Component {
constructor(props: any) {
super(props);
console.log('Exam19-1');
this.state = {
val: 0
};
this.log1 = 0;
this.log2 = 0;
this.log3 = 0;
this.log4 = 0;
}
componentDidMount() {
this.setState({val: this.state.val + 1});
this.log1 = this.state.val;
console.log(this.state.val); // 第 1 次 log
this.setState({val: this.state.val + 1});
this.log2 = this.state.val;
console.log(this.state.val); // 第 2 次 log
setTimeout(() => {
this.setState({val: this.state.val + 1});
this.log3 = this.state.val;
console.log(this.state.val); // 第 3 次 log
this.setState({val: this.state.val + 1});
this.log4 = this.state.val;
console.log(this.state.val); // 第 4 次 log
}, 0);
}
render() {
return <>
<div>{this.log1}</div>
<div>{this.log2}</div>
<div>{this.log3}</div>
<div>{this.log4}</div>
</>;
}
};
export default Exam19;
我在跑代码时发现,命令行打印是 0 0 2 3,然后我给代码加了this.logx
想在页面展示出数据,结果页面展示是0 0 2 0
两个变式
输出:1211
输出:1234
批处理机制:setstate不会立即更新state,而是将更新数据放入队列,在事件循环结束统一更新,提高效率
取决于setstate是否使用批处理机制,在事件监听函数和生命周期函数中异步更新,在setTimeout中同步更新;不是绝对固定,react会调整策略
import React from 'react'; class Exam19 extends React.Component { constructor(props: any) { super(props); console.log('Exam19-1'); this.state = { val: 0 }; this.log1 = 0; this.log2 = 0; this.log3 = 0; this.log4 = 0; } componentDidMount() { this.setState({val: this.state.val + 1}); this.log1 = this.state.val; console.log(this.state.val); // 第 1 次 log this.setState({val: this.state.val + 1}); this.log2 = this.state.val; console.log(this.state.val); // 第 2 次 log setTimeout(() => { this.setState({val: this.state.val + 1}); this.log3 = this.state.val; console.log(this.state.val); // 第 3 次 log this.setState({val: this.state.val + 1}); this.log4 = this.state.val; console.log(this.state.val); // 第 4 次 log }, 0); } render() { return <> <div>{this.log1}</div> <div>{this.log2}</div> <div>{this.log3}</div> <div>{this.log4}</div> </>; } }; export default Exam19;
我在跑代码时发现,命令行打印是 0 0 2 3,然后我给代码加了this.logx 想在页面展示出数据,结果页面展示是0 0 2 0
this.setState({val: this.state.val + 1}); 执行setState方法后会更新视图,所以最新的this.log3 的值会随之更新到页面。 this.log4 = this.state.val; 这种赋值方式并不会更新视图。 若是后面再执行setState方法,页面中的log4的值会随之更新。
现在考这题已经没有意义了。。React 18默认并发就是0011,18以下就是0023。。
现在考这题已经没有意义了。。React 18默认并发就是0011,18以下就是0023。。
当前线上绝大多数还是18以下吧
题
class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}
componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 1 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 2 次 log
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 3 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 4 次 log
}, 0);
}
render() {
return null;
}
};
答
0 // react 的可控范围内 ,批量更新 0 // react 的可控范围内 ,批量更新 // 只更新了 一次 state =1 2 // react 不可控范围,同步更新 state+1 -> 2 3 // react 不可控范围,同步更新 state+1 -> 3
解释:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/17