React专题:可变状态

本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出
来个人 GitHub repo 阅读完整的专题文章
来个人 我的博客 得到无与伦比的阅读体验

React使用一个特殊的对象this.state来管理组件内部的状态。javascript

而后开发者就能够经过描述状态来控制UI的表达。java

如何描述状态呢?react

通常咱们会在constructor生命周期钩子初始化状态。git

import React, { Component } from 'react';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = { name: '', star: 0 };
    }
}

export default App;

也能够直接用属性初始化器的写法,看起来更加简洁。github

而后经过this.setSatate()来改变状态。算法

import React, { Component } from 'react';

class App extends Component {
    state = { name: '', star: 0 };
    
    componentDidMount() {
        this.setState({ name: 'react', star: 1 });
    }
}

export default App;

this.state

首先,改变状态有特殊的门路

开发者不能直接改变this.state的属性,而是要经过this.setSatate方法。json

为何要这样设计?api

多是为了更加语义化吧,开发者清楚本身在更新状态,而不是像Vue那样改变于无形。异步

不过别急,我为正在阅读的你准备了一个炸弹:ide

猜猜下面例子最终渲染出来的star是多少?

import React, { Component } from 'react';

class App extends Component {
    state = { star: 0 };
    
    componentDidMount() {
        this.state.star = 1000;
        this.setState(prevState => ({ star: prevState.star + 1 }));
    }
    
    // componentDidMount() {
        // this.setState(prevState => ({ star: prevState.star + 1 }));
        // this.state.star = 1000;
    // }
    
    // componentDidMount() {
        // this.state.star = 1000;
        // this.setState({ star: this.state.star + 1 });
    // }
    
    // componentDidMount() {
        // this.setState({ star: this.state.star + 1 });
        // this.state.star = 1000;
    // }
}

export default App;

答案是1001。

诶,不是说不能直接改变this.state的属性么?

听我讲,首先,this.state并非一个不可变对象,你(非得较劲的话)是能够直接改变它的属性的。可是它不会触发render生命周期钩子,也就不会渲染到UI上。

不过,既然你确实改变了它的值,若是以后调用了this.setSatate()的话,它会在你直接改变的值的基础上再作更新。

因此呀少年,要想不懵逼,得靠咱们本身的代码规范。

至于注释的部分,只是为了说明顺序问题。

第一部分注释渲染出来的star是1001。由于回调会首先计算star的值,而这时候star的值是1000。

第二部分注释渲染出来的star是1001。这很好理解。

第三部分注释渲染出来的star是1。这也好理解,这个时候star的值仍是0。

其次,状态更新会合并处理

你们也看到了,咱们能够每次更新部分状态。

新状态并不会覆盖旧状态,而是将已有的属性进行合并操做。若是旧状态没有该属性,则新建。

这相似于Object.assign操做。

并且合并是浅合并。

只有第一层的属性才会合并,更深层的属性都会覆盖。

import React, { Component } from 'react';

class App extends Component {
    state = { userInfo: { name: '', age: 0 } };
    
    componentDidMount() {
        this.setState({ userInfo: { age: 13 } });
    }
}

export default App;

最后,能够有不是状态的状态

若是你须要存储某种状态,可是不但愿在状态更新的时候触发render生命周期钩子,那么彻底能够直接存储到实例的属性上,只要不是this.state的属性。使用起来仍是很自由的。

异步更新

什么叫异步更新?

异步更新说的直白点就是批量更新。

它不是真正的异步,只是React有意识的将状态攒在一块儿批量更新。

React组件有本身的生命周期,在某两个生命周期节点之间作的全部的状态更新,React会将它们合并,而不是当即触发UI渲染,直到某个节点才会将它们合并的值批量更新。

如下,组件更新以后this.state.star的值是1。

import React, { Component } from 'react';

class App extends Component {
    state = { star: 0 };
    
    componentDidMount() {
        this.setState({ star: this.state.star + 1 });
        this.setState({ star: this.state.star + 1 });
        this.setState({ star: this.state.star + 1 });
    }
}

export default App;

由于这些状态改变的操做都是在组件挂载以后、组件更新以前,因此实际上它们并无当即生效。

this.state.star的值一直是0,尽管状态被屡次操做,它获得的值一直是1,所以合并以后this.state.star的仍是1,并非咱们直觉觉得的3。

为何要异步更新?

由于this.setSatate()会触发render生命周期钩子,也就会运行组件的diff算法。若是每次setState都要走这一套流程,不只浪费性能,并且是彻底没有必要的。

因此React选择了在必定阶段内批量更新。

仍是以生命周期为界,挂载以前的全部setState批量更新,挂载以后到更新以前的全部setState批量更新,每次更新间隙的全部setState批量更新。

非异步状况

再来看一种状况:

猜猜最终渲染出来的star是多少?

import React, { Component } from 'react';

class App extends Component {
    state = { star: 0 };
    timer = null;
    
    componentDidMount() {
        this.timer = setTimeout(() => {
            this.setState({ num: this.state.star + 1 });
            this.setState({ num: this.state.star + 1 });
            this.setState({ num: this.state.star + 1 });
        }, 5000);
    }
    
    componentWillUnmount() {
        clearTimeout(this.timer);
    }
}

export default App;

答案是3。

卧槽!

说实话,这里我也没想明白。

我在React仓库的Issues里提过这个状况,这是React主创之一Dan Abramov的回答:

setState is currently synchronous outside of event handlers. That will likely change in the future.

Dan Abramov所说的event handlers应该指的是React合成事件回调和生命周期钩子。

个人理解,由于只有这些方法才能回应事件,因此它们之中的状态更新是批量的。可是它们之中的异步代码里有状态更新操做,React就不会批量更新,而是符合直觉的样子。

咱们看下面的例子,正常的重复setState只会触发一次更新,可是http请求回调中的重复setState却会屡次触发更新,看来异步的setState不在React掌控以内。

import React, { Component } from 'react';

class App extends Component {
    state = { star: 0 };
    
    componentDidMount() {
        fetch('https://api.github.com/users/veedrin/repos')
            .then(res => res.json())
            .then(res => {
                console.log(res);
                this.setState({ star: this.state.star + 1 });
                this.setState({ star: this.state.star + 1 });
                this.setState({ star: this.state.star + 1 });
            });
    }
}

export default App;

还有一种状况就是原生的事件回调,好比document上的事件回调,也不是异步的。

总结一下:所谓的异步只是批量更新而已。真正异步回调和原生事件回调中的setState不是批量更新的。

不过,Dan Abramov早就提到过,会在未来的某个版本(多是17大版本)管理全部的setState,不论是不是在所谓的event handlers以内。

React的设计有一种简洁之美,从这种对待开发者反馈的态度可见一斑。

回调

既然this.setSatate()的设计不符合直觉,React早就为开发者提供了解决方案。

this.setSatate()的参数既能够是一个对象,也能够是一个回调函数。函数返回的对象就是要更新的状态。

回调函数提供了两个参数,第一个参数就是计算过的state对象,即使这时尚未渲染,获得的依然是符合直觉的计算过的值。同时,贴心的React还为开发者提供了第二个参数,虽然并无什么卵用。

如下,组件更新以后this.state.star的值是3。

有一个小细节:箭头函数若是直接返回一个对象,要包裹一层小括号,以区别块级做用域。

import React, { Component } from 'react';

class App extends Component {
    state = { star: 0 };
    
    componentDidMount() {
        this.setState((prevState, prevProps) => ({ star: prevState.star + 1 }));
        this.setState((prevState, prevProps) => ({ star: prevState.star + 1 }));
        this.setState((prevState, prevProps) => ({ star: prevState.star + 1 }));
    }
}

export default App;

chaos

总之呢,React更新状态的设计处处都是坑。

你们对React吐槽最多的点是什么呢?

圈外人吐槽JSX。

圈内人吐槽this.setState

期盼React给开发者一个不使人困惑的状态更新API吧。

React专题一览

什么是UI
JSX
可变状态
不可变属性
生命周期
组件
事件
操做DOM
抽象UI

相关文章
相关标签/搜索