Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

第 19 题:React setState 笔试题,下面的代码输出什么? #18

Open
mengsixing opened this issue Feb 22, 2019 · 34 comments
Open
Labels

Comments

@mengsixing
Copy link

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

@zhanglin-doudou
Copy link

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的判断条件是什么呀?

@mengsixing
Copy link
Author

我理解的是:isBatchingUpdates 默认值为 false,当 react 自身的事件处理函数或 react 生命周期触发时,isBatchingUpdates 会被赋值为 true,当更新完成时又会被复原为 false。 @code-coder

@azl397985856
Copy link

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的判断条件是什么呀?

#17

@xueqingxiao
Copy link

具体可以见 ReactFiberScheduler 中的 performWork 和 performSyncWork。 楼主说的基本是对的,如果有这种 async 的 work 就不执行 batch update 如果没有 async 的就执行 batch update,setTimeout 和 promise 这些要进入 EventLoop 队列的都会被认为是 async work。

@etsvain
Copy link

etsvain commented Feb 27, 2019

哦也我做对了

@mochen666
Copy link

@azl397985856 在React的setState函数实现中,会根据一个变量 isBatchingUpdate 来判断是直接同步更新this.state还是放到队列中异步更新 。React使用了事务的机制,React的每个生命周期和合成事件都处在一个大的事务当中。在事务的前置钩子中调用batchedUpdates方法修改isBatchingUpdates变量为true,在后置钩子中将变量置为false。原生绑定事件和setTimeout异步的函数没有进入到React的事务当中,或者当他们执行时,刚刚的事务已近结束了,后置钩子触发了,所以此时的setState会直接进入非批量更新模式,表现在我们看来成为了同步SetState。

@yygmind yygmind changed the title 关于第19题的见解 第 19 题:React setState 笔试题,下面的代码输出什么? Apr 26, 2019
@AlertTed
Copy link

很好理解的问题 正常情况下 react是会同步更新数据的 也就是说isBatchingUpdate触发变成true 但是 如果你将state的状态值加入到setTimeout的延迟处理队列中 打印出来的值就是setState之后的值,或者你同时调用setState的第二个参数 也会做到异步操作 获取设置之后的state value

@leonwens
Copy link

题目呢???

@ggzcg
Copy link

ggzcg commented Jul 15, 2019

做成了 0022 哎。竟然有isBatchingUpdates这种东西 学习了 @yhlben 。

@zhouyingkai1
Copy link

我打印的 0 0 1 1

@LastStranger
Copy link

react-hooks时代貌似改写了这种方式,我用react-hooks写法写出来的结果都是0

@Bjkb
Copy link

Bjkb commented Aug 23, 2019

@LastStranger

react-hooks时代貌似改写了这种方式,我用react-hooks写法写出来的结果都是0

更新的方式没有更改,首先是因为useEffect函数只运行了一次,其次setTimeout是个闭包,访问到的值一直是0(按照正常的写法setVal(val+1))。以例子来看的话,并没有执行更新的操作。

具体可以参考 issue讨论: facebook/react#14010

@soneway
Copy link

soneway commented Sep 16, 2019

恕我直言, 这其实就是react的bug, 整出这么大一堆概念, 就是想让你们接受这个bug, 不要踩这个坑

@songyutou
Copy link

@LastStranger

react-hooks时代貌似改写了这种方式,我用react-hooks写法写出来的结果都是0

更新的方式没有更改,首先是因为useEffect函数只运行了一次,其次setTimeout是个闭包,访问到的值一直是0(按照正常的写法setVal(val+1))。以例子来看的话,并没有执行更新的操作。

具体可以参考 issue讨论: facebook/react#14010

应该是setVal((val) => val+1)

@yygmind
Copy link
Contributor

yygmind commented Dec 16, 2019

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;
  }
};

@yygmind yygmind added the Vue label Dec 16, 2019
@yygmind yygmind added React and removed Vue labels Jan 2, 2020
@zhaoyinpan2
Copy link

为什么不是 0 0 3 4

@pomelo-chuan
Copy link

react的state是为view服务的,state到view是响应式的,这种代码看瞎眼

@shuch
Copy link

shuch commented Jul 13, 2020

为什么不是 0 0 3 4

我刚开始得到结果也是0034,看楼主的解答,是前两次setState触发了批量更新,两次setState合并成了一次.

@yusongjohn
Copy link

在16.8.6源码中调试来看,这题和批量更新是没有关系的,通常批量更新是发生在react事件机制中即触发了事件回调会设置批量更新的标志。从源码中看,该题目的解释是:当前已经处于渲染的阶段,在这个过程中,框架会主动去合并在这期间触发的更新。
react的渲染主要包含两个阶段,render阶段和commit阶段,这两个阶段的包含在performWorkOnRoot方法中,该方法设置isRendering变量,表示当前是否处于渲染阶段。
image

@Code-Dog-1024
Copy link

react的setState可以认为是微任务的一次执行,但是我有点不太明白,setTimeout内的setState为什么没有批量更新?

@wangmaoshu
Copy link

wangmaoshu commented Oct 28, 2020

对于 非concurrent 模式 答案 0023 屏幕显示 0 1 2 3

  1. 组件创建 屏幕显示 val 为 0

  2. 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 的收集更新的时候被处理调了)

  3. setTime 执行
    (1)由于此时 isRendering 是false, 所以不会批量更新,而是第三个 setState 调用的时候,过期时间为 sync。 然后创建调度,然后直接进入更新。
    (2) 打印输出2. 屏幕显示 2
    (3)同理,第四个setState 同步执行
    (4)打印输出 3, 屏幕显示 3

对于 concurrent 模式 正常60hz刷新率情况下 答案 0011 屏幕显示 1 2

  1. 组件创建,进入调度执行,因为第一个创建的调度任务 肯定是在一帧空闲时间内执行的, 但是可能在 commit 阶段 检测到没有剩余时间了, 所以不会绘制到屏幕上。此时 isRendering 还是true

  2. componentDidMount 执行
    (1)执行第一个 setState, 此时 isRendering 是 true,仅仅是创建 调度任务 过期时间是一个异步的时间戳。并不会执行。
    (2)打印出 0
    (3)执行第二个 setState, 此时 isRedndering是true, 也是不会执行, 仅仅市创建调度任务, 注意的是,这里的异步过期时间与第一次的相同(因为 都是在一个 rendering 中)。
    (4)打印出 0
    (5)此时调度到第二个setState 的 调度任务的时候, 调度时间早已经过期了,所以退化成 同步的方式直接执行,屏幕绘制出 1。

  3. setTimeout 执行
    (1)第三个setState,进入到下一个时间循环的时候,在这之前可能执行了一次 state 变更,变成了1 所以 第三个创建调度任务的时候 val 可能是 2(只能说是可能, 因为不同的刷新率 和 组件大小都会影响了上一次的setState, 但是在目前实例仅仅有一个组件的情况下,state 肯定变成了 1 的因为必然执行了 render 过程)
    (2)打印出 1(也是可能值,只不过在 60hz 刷新率下 1 的概率最大)
    (3)第四个setState,创建调度任务,此调度任务放入队列,该过期时间可能与第三次的过期时间相同,因为异步计算时间都是在50ms 内的都作为一个过期时间。因为 两个过期时间一致, 所有中断了第三次的更新,进而更新第四次的。
    (4)打印出 1
    (5)屏幕显示 2

@wangmaoshu
Copy link

wangmaoshu commented Oct 28, 2020

我打印的 0 0 1 1

因为你开启了 concurrent 模式 见我的回复 👆

@youngs-github
Copy link

为什么不是 0 0 3 4

虽然说是批量更新,但是两者的值指向同一个元素,因此后面的覆盖前面的

@flftfqwxf
Copy link

@wangmaoshu 那你的意思是说 如果是事件,比如点击的时候结果会不一样么?我用点击事件,做这个操作和用 didmount执行,都是一样的结果

@miyukoarc
Copy link

react v17.0.0 0 0 2 3

@Yolo-0317
Copy link

Yolo-0317 commented Jun 1, 2021

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
image

@sxyuanfeng
Copy link

两个变式

image

输出:1211
image

输出:1234

@shifengdiy
Copy link

shifengdiy commented Aug 22, 2021

批处理机制:setstate不会立即更新state,而是将更新数据放入队列,在事件循环结束统一更新,提高效率

取决于setstate是否使用批处理机制,在事件监听函数和生命周期函数中异步更新,在setTimeout中同步更新;不是绝对固定,react会调整策略

@fangxiaochao
Copy link

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 image

this.setState({val: this.state.val + 1}); 执行setState方法后会更新视图,所以最新的this.log3 的值会随之更新到页面。
this.log4 = this.state.val; 这种赋值方式并不会更新视图。
若是后面再执行setState方法,页面中的log4的值会随之更新。

@Emiya0306
Copy link

现在考这题已经没有意义了。。React 18默认并发就是0011,18以下就是0023。。

@Yuweiai
Copy link

Yuweiai commented Jul 4, 2022

现在考这题已经没有意义了。。React 18默认并发就是0011,18以下就是0023。。

当前线上绝大多数还是18以下吧

@Yangfan2016
Copy link

Yangfan2016 commented Aug 18, 2022

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

解释:#17

@SceneryCN
Copy link

SceneryCN commented Feb 17, 2023

当前函数式组件当中通过useState去定义state然后通过setState的回调函数的方式+1,react
输出为0,0,0,0。
是否跟当前的Concurrent mode 有关联,或者说0,0,2,3这种情况只会在生命周期当中实现并且react v<18

@pipihua666
Copy link

这个在react的Concurrent mode下打印的是0,0,1,1,在Sync mode下打印的是0,0,2,3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests