“ 很早之前看了React中封装setState方法的源码,趁着最近来写一下,帮助大家能更进一步的了解setState方法,并且也能避免在开发过程中出现无形的bug。”
1. setState的使用
在讲解源码之前,我们首先需要先知道它的用法(如果你知道它的用法的话,那么是可以跳过这一部分的,直接到第二段中看源码的实现)。关于setState的用法可以分为三个部分讲解:
(1)setState改变值之后会替换掉初始化的内容吗?
答案是不会的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React, {PureComponent} from 'react'; export default class SetStateTest extends PureComponent { constructor (props) { super(props); this.state = { count: 0, content: 'hello world', }; } // 点击button按钮之后count值被更新了,但是content内容还存在,证明state不是整个替换 handleClick = () => { this.setState({count: this.state.count + 1}); }; render() { return ( <div> <p>count: {this.state.count}</p> <p>content: {this.state.content}</p> <button onClick={this.handleClick}>改变count</button> </div> ); } }
|
(2)为什么页面中state的值改变了,我打印出来的值却还是上一次的?
举一个例子,比如我定义了一个count状态,初始值为0,定义一个button按钮用于改变count的值,这时当我点击button之后页面中count确实改变了,但是在控制台中打印时发现拿到的却是上一次的值。代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import React, {PureComponent} from 'react'; export default class SetStateTest extends PureComponent { constructor (props) { super(props); this.state = { count: 0, }; } handleClick = () => { this.setState({count: this.state.count + 1}); console.log('count', this.state.count); // 点击1次按钮,页面显示1,但是在这里控制台打印出的却是上一次的count值0 }; render() { return ( <div> <p>count: {this.state.count}</p> <button onClick={this.handleClick}>改变count</button> </div> ); } }
|
对于上面,我们如何才能拿到最新更新的值呢?
(1)setState第二个参数是一个回调函数,可以在回调函数中获取;
(2)componentDidUpdate生命周期中也是可以获取到的。
对上面的代码我们做如何的处理即可获取最新的count值,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import React, {PureComponent} from 'react'; export default class SetStateTest extends PureComponent { constructor (props) { super(props); this.state = { count: 0, }; } // 2. 在componentDidUpdate生命周期中可以获取到最新更新的值 componentDidUpdate = () => { console.log('update count', this.state.count); }; handleClick = () => { // 1. 在setState第二个回调函数中可以获取到最新更新值 this.setState({count: this.state.count + 1}, () => { console.log('callback count', this.state.count); }); }; render() { return ( <div> <p>count: {this.state.count}</p> <button onClick={this.handleClick}>改变count</button> </div> ); } }
|
(3)如果有多个setState同时更新count值,那么它会叠加更新吗?
答案是不会的。
比如我在button按钮中写三个setState来更新count,这时我点击button一次,在我们想象中count应该会变为3,但是事实上却是1,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React, {PureComponent} from 'react'; export default class SetStateTest extends PureComponent { constructor (props) { super(props); this.state = { count: 0, }; } // 点击1次button按钮最后页面显示count为1,而不是3 handleClick = () => { this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); }; render() { return ( <div> <p>count: {this.state.count}</p> <button onClick={this.handleClick}>改变count</button> </div> ); } }
|
解决办法:这个时候就会考虑到setState第一个值不为对象,而是函数,函数接收两个参数分别是第一个参数prevState用于获取上一次state的值,以及第二个参数props获取到父组件传递的值,改造过后的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React, {PureComponent} from 'react'; export default class SetStateTest extends PureComponent { constructor (props) { super(props); this.state = { count: 0, }; } // 改造之后的setState,点击1次button按钮最后页面显示的count3 handleClick = () => { this.setState((prevState, props) => ({count: prevState.count + 1})); this.setState((prevState, props) => ({count: prevState.count + 1})); this.setState((prevState, props) => ({count: prevState.count + 1})); }; render() { return ( <div> <p>count: {this.state.count}</p> <button onClick={this.handleClick}>改变count</button> </div> ); } }
|
2. setState的源码解析
提示:如果你在官网下载源码之后,核心的包都在packages文件夹中。这里我只会把关键的代码展示出来,当然如果你感兴趣的话可以找到这几个关键文件夹然后去看看。
首先来解答一下setState改变值之后并不会替换掉初始化的内容,其实原因是因为在源码中使用到了Object.assign()
方法,具体源码在react-reconciler--->ReactUpdateQueue.new.js
文件中的processUpdateQueue函数下找到getStateFromUpdate函数,它的作用就是拿到state值,然后返回新的state值。重点就在getStateFromUpdate函数中,我们会找到最关键的代码,所以你根本不用担心值会被覆盖的问题。
return Object.assign({}, prevState, partialState);
为什么多个setState同时更新count值,并不会叠加更新。这是因为在源码中我们先找到getStateFromUpdate函数,找到switch中case为UpdateState,此时我们会看到const payload = update.payload
;代码,payload其实就是setState传进来要更新的值,当我们在setState中传入对象时,partialState会指向payload,最后会去执行return Object.assign({}, prevState, partialState);其实在这里我们就会发现无论你执行多少次setState,它的partialState都是最初的那个值,并不会根据setState执行多少次而改变。解决:是将setState第一个参数改为函数就可以,在源码中会使用typeof payload判断payload是否是函数,如果是函数会执行如下代码,在这里就可以看到是函数时会执行函数并更新partialState的值,这样就保证了它每一次都是更新后的值,那么多个setState时就可以进行一个叠加更新。
partialState = payload.call(instance, prevState, nextProps)
;
为什么页面中state的值改变了,我打印出来的值却还是上一次的。这里其实是涉及到了setState异步更新问题。在这篇文章中我先不说明,下一章的时候我会单独写一篇关于setState同步和异步的处理文章,在这里你先记住是如何使用的,下一章节我们继续来探讨setState的用法。