React中的setState到底是同步还是异步??

2,292 阅读5分钟

例子快照

class App extends Component{
    state = {
        name:'rose',
        age: 0,
    };

    click = () => {
        this.setState((state)=>({name:'name1',age: state.age+1}))
        this.setState((state)=>({name:'name2',age: state.age+1}))
        this.setState((state)=>({name:'name3',age: state.age+1}))
        this.setState({name:'name4', age: this.state.age+1})
        this.setState((state)=>({name:'name5',age: state.age+1}))
    };

    render() {
        console.log('render...');
        const { name, age } = this.state;
        return (
            <div>
                <span>{name + '---' +  age}</span>
                <button id='btn' onClick={this.click}>click me!</button>
            </div>
        )
    }
}

提问:

  1. render打印了几次?
  2. name 和 age最终是多少?

setState之后立马渲染?

触发了setState之后,会立马触发render!

诶!不对!

来自官方的一段话:

setState() 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式

将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

setState() 并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。如需基于之前的 state 来设置当前的 state,请阅读下述关于参数 updater 的内容。

除非 shouldComponentUpdate() 返回 false,否则 setState() 将始终执行重新渲染操作。如果可变对象被使用,且无法在 shouldComponentUpdate() 中实现条件渲染,那么仅在新旧状态不一时调用 setState()可以避免不必要的重新渲染

大概就是,setState不是立马更新的,而是把更改之后的state排入一个队列,在要渲染的时候,批量处理,也就是拿队列里面最后进来的那个state...

take is cheap...

例子🌰:

 click=()=>{
        console.log('start!')
        this.setState((state)=>({name:'name1',age: state.age+1}))
        this.setState((state)=>({name:'name2',age: state.age+1}))
        this.setState((state)=>({name:'name3',age: state.age+1}))
        console.log('end!')
};

render(){
    console.log('render!', this.state)
}

最后打印:

start!
end!
render! {name: "name3", age: 3}

虽然触发setState了三次,但是render只执行了一次,结果为最后setState的为准!也就是上面说的【修改之后的state】队列的最新的一条数据!

总结

不是!

setState不是立马更新的,而是把更改之后的state排入一个队列,在要渲染的时候,批量处理,也就是拿队列里面最后进来的那个state


多次setState,函数方式不会合并,对象方式会合并?

我一直觉得合并这个词用来形容setState的行为不太好, 合并给人的感觉是,我写了三行setState的代码,最终只执行了最后一行setState的代码!

结果是最终只执行了最后一行setState的代码?

take is cheap...

例子🌰【对象方式】

click=()=>{
    console.log('start!')
        
        this.setState({name:'name1',age: this.state.age+1})

        this.setState({name:'name2',age: this.state.age+1})

        this.setState({name:'name3',age: this.state.age+1})

    console.log('end!')
};
    
render(){
    console.log('render!', this.state)
}

最后打印:

start!
end!
render! {name: "name3", age: 1}

最后打印的结果,是最后一个this.setState({name:'name3',age: this.state.age+1})的结果!因为age最终就是从0到了1,前面的两个似乎丝毫没有起作用!

在看例子🌰:

newState = (name) => {
        console.log('pre:', this.state);
        return {name,age: this.state.age+1};
    }

click=()=>{
        console.log('start!')

        this.setState(this.newState('name1'))

        this.setState(this.newState('name2'))

        this.setState(this.newState('name3'))


        console.log('end!')
};

最终打印:

start!
pre: {name: "rose", age: 0}
pre: {name: "rose", age: 0}
pre: {name: "rose", age: 0}
end!
render! {name: "name3", age: 1}

每次setState都执行了!

至于为什么造成:

最后打印的结果,是最后一个this.setState({name:'name3',age: this.state.age+1})的结果!因为age最终就是从0到了1

的错觉,是因为 this.state.age 一直都是0。因为我们前面的setState没有立马更新state,所以age的结果也没有改变。

总结

每次setState都会执行,执行之后的值没有立马更新this.state,所以每次在setState里面取到的this.state的值都是最初的值,造成了合并执行最后一条setState的错觉。

例子🌰【函数方式】

click=()=>{
    console.log('start!')
        
        this.setState((preState)=>({name:'name1',age: preState.age+1}))
        
        this.setState((preState)=>({name:'name2',age: preState.age+1}))
        
        this.setState((preState)=>({name:'name3',age: preState.age+1}))


    console.log('end!')
};
    
render(){
    console.log('render!', this.state)
}

最后打印:

start!
end!
render! {name: "name3", age: 3}

最后打印的结果age ===3, 看来是三个setState共同作用后的结果,所以不是合并,看起来是这样的。

在看例子🌰:

newState = (name) => {
        console.log('pre:', this.state);
        return {name,age: this.state.age+1};
    }

click=()=>{
        console.log('start!')

        this.setState((preState)=>{
            console.log('pre1:', preState)
            return  {name:'name1',age: preState.age+1}
        })

        this.setState((preState)=>{
            console.log('pre2:', preState)
            return  {name:'name2',age: preState.age+1}
        })

        this.setState((preState)=>{
            console.log('pre3:', preState)
            return  {name:'name3',age: preState.age+1}
        })


        console.log('end!')
};

最终打印:

start!
end!
pre1: {name: "rose", age: 0}
pre2: {name: "name1", age: 1}
pre3: {name: "name2", age: 2}
render! {name: "name3", age: 3}
总结

每次setState都执行了,preState拿到了上次setState之后的结果,作为参数传递给下一个setState,从感觉上像是立马更新了state。

其实还是:

把更改之后的state排入一个队列,在要渲染的时候,批量处理,也就是拿队列里面最后进来的那个state...


setState是异步的?

为什么说setState是异步的?

看个例子🌰【异步】:

this.setState({name:'jack', age:18})
console.log(this.state)

打印出来居然还是setState之前的值!

{name: "rose", age: 0}

其实是设置成功了的!只是因为异步,我们可以在setState的回调函数里面看到最新的值:

this.setState({name:'jack', age:18}, ()=>{
    console.log(this.state)
})

但是setState就真的是异步了的吗?

不全是!

再看个例子🌰【同步】:

componentDidMount() {
     document.getElementById('btn').addEventListener('click',this.click)
}

click=()=>{
    console.log('start')
    
    this.setState({name:'jack', age:18})
    console.log('Hello:', this.state)
    
    console.log('end!')
}

render() {
    console.log('render!', this.state)
    
    return (
        <button id='btn'>click me!</button>
    )
}

打印结果:

start!
render! {name: "jack", age: 18}
Hello: {name: "jack", age: 18}
end!

执行了setState之后立马render,再执行下一句代码console.log('Hello:', this.state),所以,是同步的!

总结

setState在react自己的机制下是异步的,在原始机制下比如addEventListener原始事件机制下,setTimeout,promise等是同步的!


例子快照答案

  1. render打印了一次
  2. 最终结果是
{name: "name5", age: 2}