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

React 中 setState() 为什么是异步的 #12

Open
oliver1204 opened this issue Feb 12, 2019 · 7 comments
Open

React 中 setState() 为什么是异步的 #12

oliver1204 opened this issue Feb 12, 2019 · 7 comments

Comments

@oliver1204
Copy link
Owner

oliver1204 commented Feb 12, 2019

Dan 老哥的回答

1. 保证内部的一致性

即使state是同步更新,props也不是。(你只有在父组件重新渲染时才能知道props)

2. 性能优化

将state的更新延缓到最后批量合并再去渲染对于应用的性能优化是有极大好处的,如果每次的状态改变都去重新渲染真实dom,那么它将带来巨大的性能消耗。

原理解释

面试官:“react中setState是同步的还是异步?”
我:“异步的,setState不能立马拿到结果。”

面试官:“那什么场景下是异步的,可不可能是同步,什么场景下又是同步的?”
我:“......”

setState并不是真正意义上的异步操作,它只是模拟了异步的行为

为什么这么说。可以通过下面的例子
不是真正意义上的异步操作

class App extends Component {
  state = {
    count: 0
  };

  componentDidMount() {
    // 生命周期中调用
    this.setState({ count: this.state.count + 1 });
    console.log("lifecycle: " + this.state.count);
    setTimeout(() => {
      // setTimeout中调用
      this.setState({ count: this.state.count + 1 });
      console.log("setTimeout: " + this.state.count);
    }, 0);
    document.getElementById("div2").addEventListener("click", this.increment2);
  }

  increment = () => {
    // 合成事件中调用
    this.setState({ count: this.state.count + 1 });
    console.log("react event: " + this.state.count);
  };

  increment2 = () => {
    // 原生事件中调用
    this.setState({ count: this.state.count + 1 });
    console.log("dom event: " + this.state.count);
  };

  render() {
    return (
      <div className="App">
        <h2>couont: {this.state.count}</h2>
        <div id="div1" onClick={this.increment}>
          click me and count+1
        </div>
        <div id="div2">click me and count+1</div>
      </div>
    );
  }
}

探讨前,我们先简单了解下react的事件机制:react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClickonChange这些都是合成事件。

那么以上4种方式调用setState(),后面紧接着去取最新的state,按之前讲的异步原理,应该是取不到的。然而,setTimeout中调用以及原生事件中调用的话,是可以立马获取到最新的state的。根本原因在于,setState并不是真正意义上的异步操作,它只是模拟了异步的行为。React中会去维护一个标识(isBatchingUpdates),判断是直接更新还是先暂存state进队列。setTimeout以及原生事件都会直接去更新state,因此可以立即得到最新state。而合成事件和React生命周期函数中,是受React控制的,其会将isBatchingUpdates设置为 true,从而走的是类似异步的那一套。

在 setTimeout 中去 setState 并不算是一个单独的场景,它是随着你外层去决定的,因为你可以在合成事件中 setTimeout ,可以在钩子函数中 setTimeout ,也可以在原生事件setTimeout,但是不管是哪个场景下,基于event loop的模型下, setTimeout 中里去 setState 总能拿到最新的state值。

源码分析

总结

  1. setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
  2. setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  3. setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

对于3 可以结合下面的例子:

class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return <div>{this.state.val}</div>
  }
}

结合上面分析的,钩子函数中的 setState 无法立马拿到更新后的值,所以前两次都是输出0,当执行到 setTimeout 里的时候,前面两个state的值已经被更新,由于 setState 批量更新的策略, this.state.val 只对最后一次的生效,为1,而在 setTimmout 中 setState 是可以同步拿到更新结果,所以 setTimeout 中的两次输出2,3,最终结果就为 0, 0, 2, 3 。

想一下,如何将上面的代码中前两次连续 + 1 都执行呢?

class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState((preState) => {
        console.log(preState.val)   // 0
        return { val: preState + 1 }
    })
     
    this.setState((preState) => {
        console.log(preState.val)   // 1
        return { val: preState.val + 1 }
    })

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);  // 3

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)  // 4
    }, 0)
  }

  render() {
    return <div>{this.state.val}</div>
  }
}

直接赋值原理
返回函数原理

@hehe1111
Copy link

Dan 老哥的回答

1. 保证内部的一致性

即使state是同步更新,props也不是。(你只有在父组件重新渲染时才能知道props)

2. 性能优化

将state的更新延缓到最后批量合并再去渲染对于应用的性能优化是有极大好处的,如果每次的状态改变都去重新渲染真实dom,那么它将带来巨大的性能消耗。

原理解释

面试官:“react中setState是同步的还是异步?”
我:“异步的,setState不能立马拿到结果。”

面试官:“那什么场景下是异步的,可不可能是同步,什么场景下又是同步的?”
我:“......”

setState并不是真正意义上的异步操作,它只是模拟了异步的行为

为什么这么说。可以通过下面的例子
不是真正意义上的异步操作

class App extends Component {
  state = {
    count: 0
  };

  componentDidMount() {
    // 生命周期中调用
    this.setState({ count: this.state.count + 1 });
    console.log("lifecycle: " + this.state.count);
    setTimeout(() => {
      // setTimeout中调用
      this.setState({ count: this.state.count + 1 });
      console.log("setTimeout: " + this.state.count);
    }, 0);
    document.getElementById("div2").addEventListener("click", this.increment2);
  }

  increment = () => {
    // 合成事件中调用
    this.setState({ count: this.state.count + 1 });
    console.log("react event: " + this.state.count);
  };

  increment2 = () => {
    // 原生事件中调用
    this.setState({ count: this.state.count + 1 });
    console.log("dom event: " + this.state.count);
  };

  render() {
    return (
      <div className="App">
        <h2>couont: {this.state.count}</h2>
        <div id="div1" onClick={this.increment}>
          click me and count+1
        </div>
        <div id="div2">click me and count+1</div>
      </div>
    );
  }
}

探讨前,我们先简单了解下react的事件机制:react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClickonChange这些都是合成事件。

那么以上4种方式调用setState(),后面紧接着去取最新的state,按之前讲的异步原理,应该是取不到的。然而,setTimeout中调用以及原生事件中调用的话,是可以立马获取到最新的state的。根本原因在于,setState并不是真正意义上的异步操作,它只是模拟了异步的行为。React中会去维护一个标识(isBatchingUpdates),判断是直接更新还是先暂存state进队列。setTimeout以及原生事件都会直接去更新state,因此可以立即得到最新state。而合成事件和React生命周期函数中,是受React控制的,其会将isBatchingUpdates设置为 true,从而走的是类似异步的那一套。

在 setTimeout 中去 setState 并不算是一个单独的场景,它是随着你外层去决定的,因为你可以在合成事件中 setTimeout ,可以在钩子函数中 setTimeout ,也可以在原生事件setTimeout,但是不管是哪个场景下,基于event loop的模型下, setTimeout 中里去 setState 总能拿到最新的state值。

源码分析

总结

  1. setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
  2. setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  3. setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

对于3 可以结合下面的例子:

class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return <div>{this.state.val}</div>
  }
}

结合上面分析的,钩子函数中的 setState 无法立马拿到更新后的值,所以前两次都是输出0,当执行到 setTimeout 里的时候,前面两个state的值已经被更新,由于 setState 批量更新的策略, this.state.val 只对最后一次的生效,为1,而在 setTimmout 中 setState 是可以同步拿到更新结果,所以 setTimeout 中的两次输出2,3,最终结果就为 0, 0, 2, 3 。

想一下,如何将上面的代码中前两次连续 + 1 都执行呢?

class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState((state) => {
        console.log(this.state.val)   // 0
        return { val: this.state.val + 1 }
    })
     
    this.setState((state) => {
        console.log(this.state.val)   // 1
        return { val: this.state.val + 1 }
    })

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);  // 3

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)  // 4
    }, 0)
  }

  render() {
    return <div>{this.state.val}</div>
  }
}

最后一段,运行结果还是 0 0 2 3,并不是 0 1 3 4

@yibingxiong
Copy link

楼上说的对 , 运行结果还是 0 0 2 3,并不是 0 1 3 4, 楼主大佬解释下呗

@yibingxiong
Copy link

改成这样就对了

componentDidMount() {
      this.setState((state) => {
          console.log(state.val)   // 0
          return { val: state.val + 1 }
      })
       
      this.setState((state) => {
          console.log(state.val)   // 1
          return { val: state.val + 1 }
      })
  
      setTimeout(_ => {
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val);  // 3
  
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val)  // 4
      }, 0)
    }

@DangoSky
Copy link

改成这样就对了

componentDidMount() {
      this.setState((state) => {
          console.log(state.val)   // 0
          return { val: state.val + 1 }
      })
       
      this.setState((state) => {
          console.log(state.val)   // 1
          return { val: state.val + 1 }
      })
  
      setTimeout(_ => {
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val);  // 3
  
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val)  // 4
      }, 0)
    }

@yibingxiong

因为第二个setState里this.state的数据还不是最新的,此时this.state.val还是0。而参数state表示上一次更新后的state,是最新的,state.val是1。

可以参考这篇blog:https://blog.csdn.net/Mr_28/article/details/84778001

@yibingxiong
Copy link

@DangoSky 优秀

@oliver1204
Copy link
Owner Author

oliver1204 commented Mar 23, 2020

componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

   .....
  }

对于上面一段代码为什么会是0,0 ,笔者将源码缩略后,原理如下:

class Component {
  constructor() {
    this.updateQueue = [];
    this.isBatchingUpdates = false;  // 是否需要暂存

    this.state = {
      val: 0
    };
  }
  setState(newState) {
    if(this.isBatchingUpdates) {
      this.updateQueue.push(newState)
    }
  }
  add() {
    this.isBatchingUpdates = true
    this.setState({number: this.state.val + 1}) //   this.state.val = 0
    this.setState({number: this.state.val + 1})  //   this.state.val = 0
    this.setState({number: this.state.val + 1})  //   this.state.val = 0
    this.flushQueue()
  }
  flushQueue() {
    this.updateQueue.map(newState => this.state = newState)
    this.isBatchingUpdates = false;
  }
}

let c = new Component()
c.add()

@oliver1204
Copy link
Owner Author

oliver1204 commented Mar 23, 2020

this.setState((state) => {
   console.log(state.val)   // 0
   return { val: state.val + 1 }
})
       
this.setState((state) => {
   console.log(state.val)   // 1
   return { val: state.val + 1 }
})

上面的代码之所以可以说实现0,1 效果,其原理如下:

class Component {
  constructor() {
    this.updateQueue = [];
    this.callbackQueue = [];
    this.isBatchingUpdates = false;

    this.state = {
      val: 0
    };
  }
  setState(partialState, callback) {
    if(this.isBatchingUpdates) {
      this.updateQueue.push(partialState)
      this.callbackQueue.push(callback)
    }
  }
  add() {
    this.isBatchingUpdates = true
    this.setState((preState) => ({val: preState.val + 1}), () => {
      console.log(this.state.val)
    })
    this.setState((preState) => ({val: preState.val + 1}), () => {
      console.log(this.state.val)
    })
    this.setState((preState) => ({val: preState.val + 1}), () => {
      console.log(this.state.val)
    })
    this.flushQueue()
  }
  flushQueue() {
    let partialState = this.updateQueue.reduce((pre, next) => { 
        return next(pre)
    }, this.state)

    this.state = {...this.state, ...partialState}

    this.callbackQueue.map(callback => callback())
    this.isBatchingUpdates = false;
  }
}

let c = new Component()
c.add()

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

No branches or pull requests

4 participants