首页 ›  文章

React setState 这样用,开发直呼内行!

2019-12-03

{ // callback里可以获取到更新后的newState })

// 第二种:updater可以是函数,返回一个对象值 setState((state, props)=>{ return { key: val } }), newState=>{ })

其中`updater`表示新的`state`值可以是返回一个对象的函数,也可以直接是一个对象。这部分的内容会通过**浅比较**被合并到`state`中去。
官方文档很明确的告诉我们:

> setState 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

因此这个api的第二个参数`callback`,允许我们在`setState`执行完成后做一些更新的操作。

以上稍微回顾下基础知识部分,接下来我们正式开始详细探讨。

# 关于第一个函数参数

为了避免枯燥,我们带着问题来继续研究:

*   **问题1:setState使用函数参数和对象参数有何区别?**
    在回答这个问题之前,请先看这个很常用的计时器的例子:

class Demo extends Component { constructor(props) { super(props); this.state = { count: 0 //初始值 }; }

increaseCount = () => {
    this.setState({count: this.state.count + 1});
}

handleClick = ()=>{
    this.increaseCount();
}

render() {
    return (
        <div className="App-header">
            <button onClick={this.handleClick}>点击自增</button>      
            count:{this.state.count}
        </div>
    );
}

}

这个代码看起来没有什么问题, 每次点击的时候也能自增1,完全符合预期效果。但是接下来! **我们希望通过改动handleClick,使得每次点击时,count自增2次**,即:

handleClick = ()=>{ this.increaseCount() this.increaseCount() }

这时候就会惊奇地发现,**每次点击后,count还是自增1!**问题出在哪里呢?

其实就是`this.setState({count: this.state.count + 1});`这种写法,由于前面提到了`setState`并非同步方法,所以这里的`this.state.count`并不能保证取到最新的值,这时候我们可以采用第二种写法:

// 这里我们用的函数参数的方式 increaseCount = () => { this.setState((state)=>{return{count: state.count + 1}}); }

这时候再试试,发现计时器可以按照预期效果执行,此时可以回答问题1了:**如果setState时需要根据现有的state来更新新的state,那么应该使用函数参数来保证取到最新的state值。**

**答案1: 如果需要依赖当前state的值来更新下一个值的情况,需要使用函数作为参数,因为函数才能保证取到最新的state**

# 关于批量更新

接下来要研究的,就是重头戏--**setState更新过程**,可能你看过的文档都告诉你,`setState`不会保证立即执行,而是会在某个时机批量更新所有的`component`。

那么问题来了: 为什么要设定这个批量机制,这个批量更新的过程到底又是如何执行的呢?

**问题2: setStates为什么要设定批量更新机制?**

这一点其实是处于大型应用的性能考虑,首先我们都知道,`component`的`render`是很耗时的。想象这种场景:

如果在某个复合组件由一个`Parent`和一个`Child`组成,在一个`click`事件中,`Parent`组件和`Child`组件都需要执行`setState`,如果没有批量更新的机制,那么首先父组件的`setState`会触发父组件的`re-render` 并且也会触发一次子组件的render,而子组件自己的`setState`还要触发一次它自身的`re-render`,这样会导致`Child` rerender两次,批量更新机制就是为了应对这种情况而产生的。

所以紧接着问题来了:

**问题3:批量更新的过程是怎么执行的**

为了回答这个问题,我整理了一下`react`中`setState`相关的源码(源码学习的步骤放在最后,有兴趣的小伙伴可以阅读,想直接看结论也可以略过),抛开一些对主流程影响不大的细节(去掉了一些错误抛出之类的代码,提高阅读效率),梳理出这样的一个大概的流程:(如果图被压缩看不清请点击[https://www.processon.com/view/link/5de4a992e4b0df12b4afbc2a](https://www.processon.com/view/link/5de4a992e4b0df12b4afbc2a))

![流程图.png](https://img.javascriptcn.com/a0584528455efd53c737c87bd29a6d6b "流程图.png")

大致分为以下阶段:

1.  首先判断执行`setState`的上下文环境,是否处于事件系统或者Mount周期中(这一点很重要!!!,后面会详细说明)
2.  某个`component`执行`setState`
3.  将新的`state`值`partialState`放入`component`对应的`instance`变量,这里简单介绍下,react在内存中为每个`component`创建了一个对应的`instance`对象,用来保存对应的一些属性,方便在更新以及其他过程时清晰的使用`component`对应的属性。
4.  把缓存完变量的`component`,放入全局变`dirtyComponents`数组中,根据第一步的判断,判断目前是否要立即批量更新(如果是则直接更新,如果正处于`handle event`或者`mount`阶段,则等到阶段末尾再执行更新)

_PS:这个过程中的关键步骤,react15.6的源码是使用了事务`transition`的写法来实现的,但是我认为对于解释`setState`的内容并非必要的,所以在本文不深入说明,剥离出来说明是为了让读者更容易理解。关于`transition`如果有必要,之后另外写文章说明_

从以上的描述可以看到,我把判断执行`setState`的上下文环境放在开头,为什么要这样呢,接下来我们看另外一个有趣的例子,把最前面的计时器例子,稍微改变一下:

class Main extends Component { constructor(props) { super(props); this.state = { count: 0 //初始值 }; }

// 注意这个函数的改动
increaseCount = () => {
    this.setState({count: this.state.count + 1});
    console.log("第1次输出", this.state.count) 
    setTimeout(()=>{
        this.setState({count: this.state.count + 1});
        console.log("第2次输出", this.state.count)
        this.setState({count: this.state.count + 1});
        console.log("第3次输出", this.state.count)
    },0)
}

handleClick = ()=>{
    this.increaseCount();
}

render() {
    return (
        <div className="App-header">
            <button onClick={this.handleClick}>点击自增</button>      
            count:{this.state.count}
原文链接:segmentfault.com

上一篇:React生命周期更新阶段
下一篇:国际化站点的字体设置
相关文章

首次访问,人机识别验证

扫描下方二维码回复 1024 获取验证码,验证完毕后 永久 无须验证

操作步骤:[打开微信]->[扫描上侧二维码]->[关注 FedJavaScript 的微信] 输入 1024 获取验证码

验证码有误,请重新输入