react setState 原理
- 组件的数据来源有两个地方,分别是属性对象和状态对象
- 属性是父组件传递过来的,不可更改
- 状态是自己内部的,改变状态的唯一方式就是setState
- 属性和状态的变化都会引起视图更新
import React from "react";
import ReactDOM from "react-dom";
/**
* 属性是由父组件传递过来的,不能改变
* 状态是组件内部,由自己维护,外界无法访问 改变状态的唯一方式就是setState
*/
class Counter extends React.Component{
constructor(props) { //构造函数是唯一给状态赋值的地方
super(props)
//定义状态的地方
this.state = {number: 0}
}
render(){
//当我们调用setState的时候会引起状态的改变和组件的更新
console.log('render')
return (
<div>
<p>{this.state.number}</p>
<button onClick={() => this.setState({number: this.state.number + 1})}>+</button>
</div>
)
}
}
// let counter = new Counter()
ReactDOM.render(<Counter></Counter>,document.getElementById('root'))
构造函数是唯一定义状态并且赋值的地方,当我们要改变状态的值的时候需要通过setState方法,而不是直接修改state的值,并且每次调用setState的时候会引起状态的改变和组件的更新。
1.不能直接修改state的值
import React from "react";
import ReactDOM from "react-dom";
/**
* 属性是由父组件传递过来的,不能改变
* 状态是组件内部,由自己维护,外界无法访问 改变状态的唯一方式就是setState
*/
class Counter extends React.Component{
constructor(props) { //构造函数是唯一给状态赋值的地方
super(props)
//定义状态的地方
this.state = {number: 0}
}
add = () => {
this.state.number += 1
}
render(){
//当我们调用setState的时候会引起状态的改变和组件的更新
console.log('render')
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.add}>+</button>
</div>
)
}
}
// let counter = new Counter()
ReactDOM.render(<Counter></Counter>,document.getElementById('root'))
像这种直接修改state值得方法并不会生效。
2.state的更新可能是异步
我们知道调用setState会触发更新操作,这个过程包括更新state,创建新的VNode,在经过diff算法对比差异,决定需要渲染那一部分,假如他是同步更新的话,每次调用都要执行一次前面的流程,这样会造成很大的性能问题,所以需要将多个setState放进一个队列里面,、然后再一个一个执行,最后再一次性更新视图,这样会提高性能。举个例子:
let state = {number: 0}
function setState(newState) {
state = newState
console.log(state)
}
setState({number: state.number + 1})
setState({number: state.number + 2})
setState({number: state.number + 3})
这段代码会通过 setState 方法改变state值,我们看看打印结果:
可以看到每次调用setState都会改变state的值并且进行渲染,这将是一个非常消耗性能的问题。
所以React针对setState做了一些特别的优化:将多个setState的调用放进了一个队列,合并成一个来执行,这意味着当调用setState时,state并不会立即更新,看下面这个例子:
let state = {number: 0}
let updataQueue = []
function setState(newState) {
updataQueue.push(newState)
}
setState({number: state.number + 1})
setState({number: state.number + 2})
setState({number: state.number + 3})
updataQueue.forEach(item =>{
state = item
})
console.log(state) // 3
我们预想的是结果等于6.但是输出的却是3,这是因为变成异步更新之后state的值并不会立即更新,所以每次拿到的state都是 0 ,如果我们想要让结果等于 6,也就是每次都能拿到最新值,那就需要给setState()传递一个函数作为参数,在这个函数中可以拿到每次改变后的值,并通过这个函数的返回值得到下一个状态。
let state = { number: 0 }
let updataQueue = [] //更新函数队列
let callbackQueue = [] //回调函数队列
function setState(updataState,callback) {
//入队
updataQueue.push(updataState)
callbackQueue.push(callback)
}
//清空队列
function flushUpdata () {
for(let i = 0; i < updataQueue.length; i++) {
state = updataQueue[i](state) //拿到每次改变后的值作为下一个的状态
}
state = state
callbackQueue.forEach(callbackItem => callbackItem())
}
function add(){
setState(preState => ({ number: preState.number + 1}),() => {
console.log(state)
})
setState(preState => ({ number: preState.number + 2}),() => {
console.log(state)
})
setState(preState => ({ number: preState.number + 3}),() => {
console.log(state)
})
//批量更新
flushUpdata()
}
add()
console.log(state) // 6
由于回调函数也是异步执行的,所以最后一次性输出的都是6.
改写成class类的形式如下:
class Component {
constructor() {
this.state = {
number: 0
}
this.batchUpdata = false
this.updataQueue = [] //更新队列
this.callbackQueue = [] //回调函数队列
}
setState(updataState, callback) {
if (this.batchUpdata) {
this.updataQueue.push(updataState) //放入队列
this.callbackQueue.push(callback)
}
}
flushUpdata() {
let state = this.state
// this.updataQueue.forEach(newStateitem => this.state = newStateitem)
for(let i = 0; i < this.updataQueue.length; i++) {
state = this.updataQueue[i](state)
}
this.state = state
this.callbackQueue.forEach(callback => callback())
}
add() {
this.batchUpdata = true //开启合并模式
this.setState(preState => ({ number: preState.number + 1}),() => {
console.log(this.state)
})
this.setState(preState => ({ number: preState.number + 2}),() => {
console.log(this.state)
})
this.setState(preState => ({ number: preState.number + 3}),() => {
console.log(this.state)
})
//批量更新
this.flushUpdata()
}
}
let c = new Component()
c.add()
console.log(c.state)
现在这个逻辑对于setState传入的参数是函数很适合,但是有时候我们希望传入的是对象,且希望利用setState执行完之后做一些操作,比如在请求到数据之后隐藏进度条等,这个时候就需要setState能变为同步执行,这个时候我们会借助promise、setTimeout等方法来改变setState让它变为同步的。也就是不用放入队列,而是立即执行,但是以上逻辑不支持同步的情况,我们需要修改:
class Component {
constructor() {
this.state = {
number: 0
}
this.batchUpdata = false
this.updataQueue = [] //更新队列
this.callbackQueue = [] //回调函数队列
}
setState(updataState, callback) {
if (this.batchUpdata) { //批量更新
this.updataQueue.push(updataState) //放入队列
this.callbackQueue.push(callback)
}else { //直接更新
console.log('直接更新')
//如果是函数需要把老值传进去
if(typeof updataState === 'function') {
this.state = updataState(this.state)
}else {
this.state = updataState
}
}
}
flushUpdata() {
let state = this.state
// console.log(this.updataQueue)
for(let i = 0; i < this.updataQueue.length; i++) {
//为了兼容参数为函数和对象的情况需要判断一下 参数为对象的时候不用传上一个的状态值,参数为函数的时候需要传上一个的状态给下一个状态
if(typeof this.updataQueue[i] === 'function') {
state = this.updataQueue[i](state)
}else {
state = this.updataQueue[i]
}
}
this.state = state
this.callbackQueue.forEach(callback => {
if(callback) callback() //为了兼容参数为函数和对象的情况需要判断一下,参数为对象的时候没有回调函数就不执行
})
this.batchUpdata = false //更新完毕置为false
}
add() {
this.batchUpdata = true //开启合并模式
//不会放进更新队列
setTimeout(() => {
this.setState({number: this.state.number + 4})
console.log(this.state)
},1000)
this.setState({number: this.state.number + 1})
// this.setState(preState => ({ number: preState.number + 1}),() => {
// console.log(this.state)
// })
// this.setState({number: this.state.number + 1})
//批量更新
this.flushUpdata()
}
}
let c = new Component()
c.add()
console.log('end'+ JSON.stringify(c.state))
批量处理机制就是为了减少setState刷新页面的次数,setTimeout,promise等异步方法可以直接跳过批量处理机制,setState调几次就改几次。
https://www.cnblogs.com/jiuyi/p/9263114.html这篇文章对于同步更新讲的比较好
3.seState的更新会被合并
当调用setState的时候,React会把你要修改的那一部分的对象合并到当前的state上面,举个栗子:
class Counter extends React.Component{
constructor(props) { //构造函数是唯一给状态赋值的地方
super(props)
this.add = this.add.bind(this)
//定义状态的地方
this.state = {name: 'leah' ,number: 0}
}
add (event) {
console.log(event)
// this.state.number += 1 不能直接修改state的值
this.setState({number: this.state.number + 1})
}
render(){
console.log(this)
//当我们调用setState的时候会引起状态的改变和组件的更新
console.log('render')
return (
<div>
<p>{this.state.name}</p>
<p>{this.state.number}</p>
<button onClick={this.add}>+</button>
</div>
)
}
}
当前我们只修改了state.number这个时候,name还是会渲染,我们需要对这部分进行合并
class Component {
constructor() {
this.state = {
name: 'leah',
number: 0
}
this.batchUpdata = false
this.updataQueue = [] //更新队列
this.callbackQueue = [] //回调函数队列
}
setState(updataState, callback) {
if (this.batchUpdata) { //批量更新
this.updataQueue.push(updataState) //放入队列
this.callbackQueue.push(callback)
}else { //直接更新
console.log('直接更新')
//如果是函数需要把老值传进去
if(typeof updataState === 'function') {
this.state = updataState(this.state)
}else {
this.state = updataState
}
}
}
flushUpdata() {
let state = this.state
// console.log(this.updataQueue)
for(let i = 0; i < this.updataQueue.length; i++) {
//为了兼容参数为函数和对象的情况需要判断一下 参数为对象的时候不用传上一个的状态值,参数为函数的时候需要传上一个的状态给下一个状态
let partialState = typeof this.updataQueue[i] === 'function' ? this.updataQueue[i](this.state) : this.updataQueue[i]
state = {...state, ...partialState}
}
this.state = state
this.callbackQueue.forEach(callback => {
if(callback) callback() //为了兼容参数为函数和对象的情况需要判断一下,参数为对象的时候没有回调函数就不执行
})
this.batchUpdata = false //更新完毕置为false
}
add() {
this.batchUpdata = true //开启合并模式
//不会放进更新队列
setTimeout(() => {
this.setState({number: this.state.number + 4})
console.log(this.state)
},1000)
this.setState({number: this.state.number + 1})
// this.setState(preState => ({ number: preState.number + 1}),() => {
// console.log(this.state)
// })
// this.setState({number: this.state.number + 1})
//批量更新
this.flushUpdata()
}
}
let c = new Component()
c.add()
console.log('end'+ JSON.stringify(c.state))
4.在组件实例中this的指向问题:
一般来说,类的方法里this是undefined,那如何让普通方法的this指向组件实例呢?
4.1.箭头函数
class Counter extends React.Component{
constructor(props) { //构造函数是唯一给状态赋值的地方
super(props)
//定义状态的地方
this.state = {number: 0}
}
add = (event) => {
console.log(event)
// this.state.number += 1 不能直接修改state的值
this.setState({number: this.state.number + 1})
this.setState({number: this.state.number + 2})
this.setState({number: this.state.number + 3})
}
render(){
console.log(this)
//当我们调用setState的时候会引起状态的改变和组件的更新
console.log('render')
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.add}>+</button>
</div>
)
}
}
4.2.匿名函数
class Counter extends React.Component{
constructor(props) { //构造函数是唯一给状态赋值的地方
super(props)
//定义状态的地方
this.state = {number: 0}
}
add (event) {
console.log(event)
// this.state.number += 1 不能直接修改state的值
this.setState({number: this.state.number + 1})
this.setState({number: this.state.number + 2})
this.setState({number: this.state.number + 3})
}
render(){
console.log(this)
//当我们调用setState的时候会引起状态的改变和组件的更新
console.log('render')
return (
<div>
<p>{this.state.number}</p>
<button onClick={() => this.add()}>+</button>
</div>
)
}
}
4.3.bind绑定
class Counter extends React.Component{
constructor(props) { //构造函数是唯一给状态赋值的地方
super(props)
//定义状态的地方
this.state = {number: 0}
}
/**
* 合成事件 react合成事件
* 事件代理
* event 并不是原始的dom对象 而是react二次封装的事件对象 可以复用
*/
add (event) {
console.log(event)
// this.state.number += 1 不能直接修改state的值
this.setState({number: this.state.number + 1})
this.setState({number: this.state.number + 2})
this.setState({number: this.state.number + 3})
}
render(){
console.log(this)
//当我们调用setState的时候会引起状态的改变和组件的更新
console.log('render')
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.add.bind(this)}>+</button>
</div>
)
}
}
但是这样绑定有个问题,就是每次渲染的时候都需要绑定一次,所以可以在构造函数里面一次性绑定
class Counter extends React.Component{
constructor(props) { //构造函数是唯一给状态赋值的地方
super(props)
this.add = this.add.bind(this)
//定义状态的地方
this.state = {number: 0}
}
/**
* 合成事件 react合成事件
* 事件代理
* event 并不是原始的dom对象 而是react二次封装的事件对象 可以复用
*/
add (event) {
console.log(event)
// this.state.number += 1 不能直接修改state的值
this.setState({number: this.state.number + 1})
this.setState({number: this.state.number + 2})
this.setState({number: this.state.number + 3})
}
render(){
console.log(this)
//当我们调用setState的时候会引起状态的改变和组件的更新
console.log('render')
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.add}>+</button>
</div>
)
}
}
5.合成事件
合成事件原理:利用事件冒泡机制
如果react事件绑定在了真实DOM节点上,一个节点同事有多个事件时,页面的响应和内存的占用会受到很大的影响。因此SyntheticEvent作为中间层出现了。
事件没有在目标对象上绑定,而是在document上监听所支持的所有事件,当事件发生并冒泡至document时,react将事件内容封装并叫由真正的处理函数运行。
这篇主要讲了一下setState的异步更新处理过程。
我们的更新其实并不是真正的异步处理,而是更新的时候把更新内容放到了更新队列中,最后批次更新,这样才表现出异步更新的状态。setTimeout,promise等异步方法可以直接跳过批量处理机制,setState调几次就改几次。