问一个react更新State的问题?

读react官网:
状态更新可能是动态的

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

官网说这种写法是错误的

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

这种写法是正确的

我实在搞不懂为什么第一个是错误的,第二种写法是正确的,哪位大神能帮忙解释一下?在哪种需求场景下,会出现上述的情况,最好能写点代码解释下,多谢,大神们指导。

阅读 4.5k
3 个回答

因为 this.props 和 this.state 可能是异步更新的,你不能依赖他们的值计算下一个state(状态)。

setState 方法“或许”是异步的。也许你觉得,看上去更新 state 是如此轻而易举的操作,这并没有什么可异步处理的。但是要意识到,因为 state 的更新会触发 re-rendering,而 re-rendering 代价昂贵,短时间内反复进行渲染在性能上肯定是不可取的。所以,React 采用 batching 思想,它会 batches 一系列连续的 state 更新,而只触发一次 re-render。

setState的机制其实跟浏览器的dom更新类似,等到一定数量或者一定时间间隔才一起更新一次,它们都是异步更新的,所以这样做在一定几率是有问题的。

或者,直接看下面的一个小例子。
比如,最简单的一个场景是

function incrementMultiple() {
  this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});
}

直观上来看,当上面的 incrementMultiple 函数被调用时,组件状态的
count 值被增加了3次,每次增加1,那最后 count 被增加了3。但是,实际上的结果只给 state 增加了1。不信你自己试试~

让 setState 连续更新的几个 hack

如果想让 count 一次性加3,应该如何优雅地处理潜在的异步操作,规避上述问题呢?

以下提供几种解决方案:

方法一:常见的一种做法便是将一个回调函数传入 setState 方法中。即 setState 著名的函数式用法。这样能保证即便在更新被 batched 时,也能访问到预期的 state 或 props。(后面会解释这么做的原理)

方法二:另外一个常见的做法是需要在 setState 更新之后进行的逻辑(比如上述的连续第二次 count + 1),封装到一个函数中,并作为第二个参数传给 setState。这段函数逻辑将会在更新后由 React 代理执行。即:

setState(updater, [callback])

方法三:把需要在 setState 更新之后进行的逻辑放在一个合适的生命周期 hook 函数中,比如 componentDidMount 或者 componentDidUpdate 也当然可以解决问题。也就是说 count 第一次 +1 之后,出发 componentDidUpdate 生命周期 hook,第二次 count +1 操作直接放在 componentDidUpdate 函数里面就好啦。

更多详细内容:从 setState promise 化的探讨 体会 React 团队设计思想

React的setState是基于列队的异步实现。

大体上,你可以看一下我的迷你React框架anujs源码,以极简的方式实现官方的各种行为

https://github.com/RubyLouvre/anu/blob/master/src/Component.js
    setState(state, cb) {
        debounceSetState(this.updater, state, cb);
    },
    forceUpdate(cb) {
        debounceSetState(this.updater, true, cb);
    },
    
    
    function debounceSetState(updater, state, cb) {
    if(!updater){
        return;
    }
    if (updater._didUpdate) {
        //如果用户在componentDidUpdate中使用setState,要防止其卡死
        setTimeout(function() {
            updater._didUpdate = false;
            setStateImpl(updater, state, cb);
        }, 300);
        return;
    }
    setStateImpl(updater, state, cb);
}
function setStateImpl(updater, state, cb) {
    if (isFn(cb)) {
        updater._pendingCallbacks.push(cb);
    }
    if (state === true) {
        //forceUpdate
        updater._forceUpdate = true;
    } else {
        //setState
        updater._pendingStates.push(state);
    }
    if (updater._lifeStage == 0) {
        //组件挂载期
        //componentWillUpdate中的setState/forceUpdate应该被忽略
        if (updater._hydrating) {
            //在render方法中调用setState也会被延迟到下一周期更新.这存在两种情况,
            //1. 组件直接调用自己的setState
            //2. 子组件调用父组件的setState,
            updater._renderInNextCycle = true;
        }
    } else {
        //组件更新期
        if (updater._receiving) {
            //componentWillReceiveProps中的setState/forceUpdate应该被忽略
            return;
        }
        updater._renderInNextCycle = true;
        if (options.async) {
            //在事件句柄中执行setState会进行合并
            options.enqueueUpdater(updater);
            return;
        }
        if (updater._hydrating) {
            // 在componentDidMount里调用自己的setState,延迟到下一周期更新
            // 在更新过程中, 子组件在componentWillReceiveProps里调用父组件的setState,延迟到下一周期更新
            return;
        }
        //  不在生命周期钩子内执行setState
        options.flushUpdaters([updater]);
    }
}

    

上面的注释已经讲得一清二楚了,setState在不同阶段(大体分为两个阶段)的不同生命周期内,采用不同的策略,是将用户的state与callback先放入列队,还是立即执行,是在这个周期内执行,还是在下一周期内执行,是瞬间性的异步执行,还是防卡性的更大时差的异步执行。

这只是React15的行为,这点代码,还没有结合其调度器的情况来看,实际更复杂。总的目标是,减少React对DOM树的更新频率,并让更新异步不会让人感惊愕。比如说vue的$nextTick,就是一种不好的补救措施,因为用户知道更新是异步的,但不知准备的更新结束时机,而React总是保证更新完就立即执行你的回调。

是异步不是动态,这是很常见的优化手段,将变化先收集起来按一定周期处理。你的 counter 每次变化都依赖前一次的 counter,如果你在短时间(一个周期)内 set 了多次这样的 state,比如说 this.state.counter0props.increment1,你期待连续 setState 的让 counter 递增两遍达到 2,但实际上只会得到 1,因为第一遍的 setState 被缓存起来还没处理,第二遍引用 this.state.counter 的时候还是旧的值。

改变成回调的方式后,即便缓存起来,回调函数会在更新的时候才调用,这时候第一遍的 setState 已经完成了,新
state 会以参数形式传给回调,所以能引用到正确的值。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题