Open libin1991 opened 6 years ago
本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出 来我的 GitHub repo 阅读完整的专题文章 来我的 个人博客 获得无与伦比的阅读体验
本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出
来我的 GitHub repo 阅读完整的专题文章
来我的 个人博客 获得无与伦比的阅读体验
React使用一个特殊的对象this.state来管理组件内部的状态。
this.state
然后开发者就可以通过描述状态来控制UI的表达。
如何描述状态呢?
一般我们会在constructor生命周期钩子初始化状态。
constructor
import React, { Component } from 'react'; class App extends Component { constructor(props) { super(props); this.state = { name: '', star: 0 }; } } export default App; 复制代码
也可以直接用属性初始化器的写法,看起来更加简洁。
然后通过this.setState()来改变状态。
this.setState()
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.setState方法。
this.setState
为什么要这样设计?
可能是为了更加语义化吧,开发者清楚自己在更新状态,而不是像Vue那样改变于无形。
不过别急,我为正在阅读的你准备了一个炸弹:
猜猜下面例子最终渲染出来的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上。
render
不过,既然你确实改变了它的值,如果之后调用了this.setState()的话,它会在你直接改变的值的基础上再做更新。
所以呀少年,要想不懵逼,得靠我们自己的代码规范。
至于注释的部分,只是为了说明顺序问题。
第一部分注释渲染出来的star是1001。因为回调会首先计算star的值,而这时候star的值是1000。
第二部分注释渲染出来的star是1001。这很好理解。
第三部分注释渲染出来的star是1。这也好理解,这个时候star的值还是0。
大家也看到了,我们可以每次更新部分状态。
新状态并不会覆盖旧状态,而是将已有的属性进行合并操作。如果旧状态没有该属性,则新建。
这类似于Object.assign操作。
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。
this.state.star
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.setState()会触发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合成事件回调和生命周期钩子。
event handlers
我的理解,因为只有这些方法才能回应事件,所以它们之中的状态更新是批量的。但是它们之中的异步代码里有状态更新操作,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.setState()的设计不符合直觉,React早就为开发者提供了解决方案。
this.setState()的参数既可以是一个对象,也可以是一个回调函数。函数返回的对象就是要更新的状态。
回调函数提供了两个参数,第一个参数就是计算过的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; 复制代码
总之呢,React更新状态的设计到处都是坑。
大家对React吐槽最多的点是什么呢?
圈外人吐槽JSX。
圈内人吐槽this.setState。
期盼React给开发者一个不令人困惑的状态更新API吧。
React专题一览 什么是UI JSX 可变状态 不可变属性 生命周期 组件 事件 操作DOM 抽象UI
什么是UI
JSX
可变状态
不可变属性
生命周期
组件
事件
操作DOM
抽象UI
React使用一个特殊的对象
this.state
来管理组件内部的状态。然后开发者就可以通过描述状态来控制UI的表达。
如何描述状态呢?
一般我们会在
constructor
生命周期钩子初始化状态。import React, { Component } from 'react'; class App extends Component { constructor(props) { super(props); this.state = { name: '', star: 0 }; } } export default App; 复制代码
也可以直接用属性初始化器的写法,看起来更加简洁。
然后通过
this.setState()
来改变状态。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.setState
方法。为什么要这样设计?
可能是为了更加语义化吧,开发者清楚自己在更新状态,而不是像Vue那样改变于无形。
不过别急,我为正在阅读的你准备了一个炸弹:
猜猜下面例子最终渲染出来的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.setState()
的话,它会在你直接改变的值的基础上再做更新。所以呀少年,要想不懵逼,得靠我们自己的代码规范。
至于注释的部分,只是为了说明顺序问题。
第一部分注释渲染出来的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.setState()
会触发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的回答:
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.setState()
的设计不符合直觉,React早就为开发者提供了解决方案。this.setState()
的参数既可以是一个对象,也可以是一个回调函数。函数返回的对象就是要更新的状态。回调函数提供了两个参数,第一个参数就是计算过的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吧。