HuangHongRui / Notebook

:pencil2: Yeah.. This's My NoteBook...:closed_book:
0 stars 0 forks source link

setState[解读] #36

Open HuangHongRui opened 7 years ago

HuangHongRui commented 7 years ago

React抽象来说,就是一个公式

UI=f(state) 把最终绘制出来的UI当做一个函数f运行的结果, f就是React和我们基于React写得代码, 而f的输入参数就是state。

理解setState的关键点:

setState不会立刻改变React组件中state的值

在React中,一个组件中要读取当前状态用是访问this.state, 但是更新状态却是用this.setState,不是直接在this.state上修改,为什么呢?

//读取状态
const count = this.state.count;

//更新状态
this.setState({count: count + 1});

//无意义
this.state.count = count + 1;

因为this.state说到底只是一个对象,单纯去修改一个对象的值是没有意义的,去驱动UI的更新才是有意义的. [想想看,如果只是改了this.state这个对象,但是没有让React组件重新绘制一遍,那有什么用?你可以尝试在代码中直接修改this.state的值,会发现的确能够改变状态,但是却不会引发重新渲染。]

所以,需要用一个函数去更改状态,这个函数就是setState,当setState被调用时,能驱动组件的更新过程,引发componentDidUpdate、render等一系列函数的调用。

这么看来,React提供setState这个API是一个挺合理的决定。

因为setState并不会立刻修改this.state的值,所以下面的code可能产生很不直观的结果。

function handelIncrementThreeTimes() {
    this.setState({value: this.state.value + 1});
    this.setState({value: this.state.value + 1});
    this.setState({value: this.state.value + 1});
};

直观上来看,当上面的handelIncrementThreeTimes函数被调用时,组件状态的value值被增加了3次,每次增加1,那最后value被增加了3,但是,实际上的结果只给state增加了1。

原因并不复杂,就是因为调用this.setState时,并没有立即更改this.state,所以this.setState只是在反复设置同一个值而已,上面的code等同下面这样。

function handelIncrementThreeTimes() {
    const currentValue = this.state.value;
    this.setState({value: this.currentValue + 1});
    this.setState({value: this.currentValue + 1});
    this.setState({value: this.currentValue + 1});
}

currentValue就是一个快照结果,重复地给count设置同一个值,不要说重复3次,哪怕重复一万次,得到的结果也只是增加1而已。

既然this.setState不会立即修改this.state的值,那在什么时候修改this.state的值呢?这就要说一下React的更新生命周期。

setState通过引发一次组件的更新过程来引发重新绘制

setState调用引起的React的更新生命周期函数4个函数【比修改prop引发的生命周期少一个componentWillReceiveProps函数】,这4个函数依次被调用。

shouldComponentUpdate函数被调用的时候,this.state没有得到更新。 当componentWillUpdate函数被调用的时候,this.state依然没有得到更新。 直到render函数被调用的时候,this.state才得到更新。

【或者,当shouldComponentUpdate函数返回false,这时候更新过程就被中断了,render函数也不会被调用了,这时候React不会放弃掉对this.state的更新的,所以虽然不调用render,依然会更新this.state。】

如果你没兴趣去记住React的生命周期(虽然你应该记住),那就可以简单认为,直到下一次render函数调用时(或者下一次shouldComponentUpdate返回false时)才得到更新的this.state。

不管你喜欢不喜欢,反正this.state就是不会再this.setState调用之后立刻更新。

多次setState函数调用产生的效果会合并

比如下面的代码。

function updateName() {
  this.setState({FirstName: 'Morgan'});
  this.setState({LastName: 'Cheng'});
}

连续调用了两次this.setState,但是只会引发一次更新生命周期,不是两次,因为React会将多个this.setState产生的修改放在一个队列里,缓一缓,攒在一起,觉得差不多了再引发一次更新过程。

在每次更新过程中,会把积攒的setState结果合并,做一个merge的动作,所以上面的代码相当于这样。

function updateName() {
  this.setState({FirstName: 'Morgan', LastName: 'Cheng'});
}

如果每一个this.setState都引发一个更新过程的话,那就太浪费了! 对于开发者而言,也可以放心多次调用this.setState,每一次只要关注当前修改的那一个字段就行,反正其他字段会合并保留,丢不掉。

所以,合并多次this.setState调用更改的状态这个API设计决定也不错。

最近一个this.setState函数的隐藏功能进入了大家的视野,那就是:原来this.setState可以接受一个函数作为参数啊!

函数式的setState用法

如果传递给this.setState的参数不是一个对象而是一个函数,那游戏规则就变了。

这个函数会接收到两个参数

  1. 第一个是当前的state值,
  2. 第二个是当前的props,

这个函数应该返回一个对象,这个对象代表想要对this.state的更改

换句话说,之前你想给this.setState传递什么对象参数,在这种函数里就返回什么对象, 不过,计算这个对象的方法有些改变,不再依赖于this.state,而是依赖于输入参数state

比如,对于上面增加state上count的例子,可以这么写一个函数。

increment(state, props) {
    return {value: state.value + 1}
}

可以看到,同样是把状态中的count加1,但是状态的来源不是this.state,而是输入参数state。

对应incrementMultiple的函数就是这么写。

handelIncrementThreeTimes = () => {
    this.setState(this.increment);
    this.setState(this.increment);
    this.setState(this.increment);
};
class Welcome extends React.Component {

    state = {value: 0};
    increment(state, props) {
        return {value: state.value + 1}
    }
    handelIncrementThreeTimes = () => {
        this.setState(this.increment);
        this.setState(this.increment);
        this.setState(this.increment);
    };

    render() {
        return (
            <div>
                <button onClick={this.handelIncrementThreeTimes}>+++</button>
                <h1>{this.state.value}</h1>
            </div>
        )
    }
}

对于多次调用函数式setState的情况,React会保证调用每次increment时,state都已经合并了之前的状态修改结果。

简单说,加入当前this.state.value的值是0, 第一次调用this.setState(increment),传给increment的state参数是0, 第二调用时,state参数是1, 第三次调用是,参数是2, 最终incrementMultiple的效果,真的就是让this.state.count变成了3,这个函数handelIncrementThreeTimes终于实至名归。

值得一提的是,在increment函数被调用时,this.state并没有被改变,依然,要等到render函数被重新执行时(或者shouldComponentUpdate函数返回false之后)才被改变。

让setState接受一个函数的API设计很棒! 因为这符合函数式编程的思想,让开发者写出没有副作用的函数,

increment函数并不去修改组件状态,只是把“希望的状态改变”返回给React,维护状态这些苦力活完全交给React去做。

正因为流程的控制权交给了React,所以React才能协调多个setState调用的关系。

让我们再往前推进一步,试着如果把两种setState的用法混用,那会有什么效果?

handelIncrementThreeTimes = () => {
    this.setState(this.increment);
    this.setState(this.increment);
    this.setState({value: this.state.value + 1});
    this.setState(this.increment);
};

在几个函数式setState调用中插入一个传统式setState调用(嗯,我们姑且这么称呼以前的setState使用方式),最后得到的结果是让this.state.count增加了2,而不是增加4。

原因也很简单,因为React会依次合并所有setState产生的效果,虽然前两个函数式setState调用产生的效果是count加2,但是半路杀出一个传统式setState调用,一下子强行把积攒的效果清空,用count加1取代。

这么看来,传统式setState的存在,会把函数式setState拖下水啊!只要有一个传统式的setState调用,就把其他函数式setState调用给害了。

如果说setState这儿API将来如何改进,也许就该完全采用函数为参数的调用方法,废止对象为参数的调用方法。

当然,React近期肯定不会有这样的惊世骇俗的改变,但是大家可以先尝试函数式setState用法,这才是setState的未来。