View on GitHub

Cycle263 Blog

Stay hungry, stay foolish.

setState

在React中,要改变组件的状态,不能直接用 this.state = xxx 这种方式来修改,如果这样做 React 就没办法知道你修改了组件的状态,也就没办法更新页面,只能使用setstate方法,而且setState不会立即更新组件。为了性能,React会延迟更新,会把多个组件的更新放在一次操作里。React中setState并不总是立即更新组件,它可能会推后至批量更新。

setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程(isBatchingUpdates=true),当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。

setState 只在合成事件和钩子函数中是“异步”的(非实时的),因为这些函数里,react正处于更新中,需要等待上次更新完后才能更新当前的;而在原生事件和setTimeout 中都是同步的,因为这次回调函数中,上次早就更新完成了。

setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。

在 React 15 中, 执行生命周期函数或事件处理函数时会默认创建一个事务(transaction),在其内部同步调用的 setState 都将被缓存入栈,待同步函数执行结束后开始批量更新(batch update),多个 setState 会被 merge 并得到终状态,之后真正的组件更新才会开始。值得一提的是,在事务之外执行的多个 setState 是不会被合并的,比如在 setTimeout 中调用的 setState。

在 React 16之后,React开发团队调整了渲染机制,新的调和算法fiber,将原有的同步渲染组件方式,改成可异步渲染且可中断渲染的机制。

在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列(dirtyComponents)中回头再说;而 isBatchingUpdates 默认是 false,也就表示 setState 会同步更新 this.state。但是,有一个函数 batchedUpdates,这个函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会调用这个 batchedUpdates,造成的后果,就是由 React 控制的事件处理过程 setState 不会同步更新 this.state。

也就是说,整个将 React 组件渲染到 DOM 中的过程就处于一个大的 transaction 中,有前置的 batchedUpdate 调用,isBatchingUpdates已经为true,生命周期函数或事件处理函数处于一个小的transaction中,也会受此影响而不进行立即更新。

那么调用this.setState()后什么时候this.state才会更新?答案是即将要执行下一次的render函数时。

diff

// Wrong
this.setState({
    counter: this.state.counter + this.props.increment,
});

// Correct
this.setState((prevState, props) => ({
    counter: prevState.counter + props.increment
}));

/* 由React控制的事件处理过程setState不会同步更新this.state;在React控制之外的情况,setState会同步更新this.state,如:addEventListener,setTimeout/setInterval等, 这类情况下的setState没走react的事物机制transaction,执行时批量更新(isBatchingUpdates)没设置为true,因此每次都直接render了。 */

componentDidMount() {
    setTimeout(() => {
        this.setState({
            index: this.state.index + 1
        });
        console.log('state', this.state.index);   // 0
        this.setState({
            index: this.state.index + 1
        });
        console.log('state', this.state.index);   // 1
    }, 0);
}
class Child extends Component {
    constructor(props) {
        super(props);
        this.state = {
            someThings: props.someThings
        };
    }
    componentWillReceiveProps(nextProps) {
        this.setState({someThings: nextProps.someThings});
    }
    render() {
        return <div>{this.state.someThings}</div>
    }
}