class组件的状态
针对react
中对于FunctionComponet
,ClassComponent
,DOM
节点的基本处理和挂载已经告一段落了。
jsx
原理可以查看这篇文章~,接下来我们来讨论讨论React
中class
组件中对于sate
的使用,我们会来先讲讲。
state
的基础使用。state
遇到的一些"坑"。state
基础原理讨论。- 我们跟随上一节的
jsx
原理的代码来手把手实现一套state
机制。
state
基础使用
我们都清楚在react
中组件的数据来源两个部分,一个是组件自身的state
,一个是接受父组件传入的props
。这两种状态的改变都会造成视图层面的更新。
当然我们需要注意的是,改变组件内部状态一定是要通过**setState
**进行更新组件内部数据的,直接赋值的话并不会触发页面的更新的。
state
的基础使用这里就不讨论使用代码展开了,基础使用官网有一个Clock例子。
state
遇到的一些"坑"
在react
中我们都明白关于setState
是用于异步批量更新,可是你真的明白这里的"异步"所谓的是什么意思吗,以及他所谓的批量什么时候才会批量,什么时候又会依次更新呢?接下来我们来看看。
需要注意的是这里的"异步更新",所谓的异步和
Promise
以及setTimeout
这些微/宏任务是无关的。这点我们在后续会讲到,这也是Vue
中异步更新策略不同之处。 处于性能的考虑,React
可能会将多次setState
的更新合并到一个。接下来我们深入去探讨react
什么时候会合并多次更新,什么时候并不会合并多次更新。
"问题"分析
基础用法
接下来我们来看这样一个代码:
代码语言:javascript复制interface ICountState {
number: number;
}
class Counter extends React.Component<any, ICountState> {
constructor(props: any) {
super(props);
this.state = {
number: 0,
};
}
// 在事件处理函数中setState的调用会批量异步执行
handleClick = (event: React.MouseEvent) => {
// 第一次增加
this.setState({
number: this.state.number 1,
});
console.log(this.state.number); // 0
// 第二次增加
this.setState({
number: this.state.number 1,
});
console.log(this.state.number); // 0
};
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.handleClick}> </button>
</div>
);
}
}
const element = <Counter></Counter>;
ReactDOM.render(element, document.getElementById('root'));
复制代码
这段代码中,我们定义了一个handleClick
点击函数,当我们点击按钮的时候。在事件处理函数中执行了两次**setState
**,并且每次**setState
**值都依赖于上一次的**state
**。
不难想象,我们最终页面上会渲染出1
,因为react
是基于异步批量更新原则。当我们点击执行setState
时,组件内部的state
并没有及时更新,此时this.state.number
仍然为0
,所以第二次在执行setState(this.state.number 1)
就相当于setState(0 1)
.
最终react
将这两次更新合并为一次执行并且刷新页面,state
更新为1
,并且页面渲染为1
。
我们可以看到在事件处理函数中**
setState
**方法并不会立即更新**state
**的值,而是会等到事件处理函数结束之后。批量执行**setState
**统一更新**state
**进行页面渲染。
如果我们要在setState
中依赖上一次调用setState
的值,那么react
官方支持传入一个callback
,它接受一个参数就是上一次传入的值:
handleClick = (event: React.MouseEvent) => {
this.setState((state) => {
console.log(state.number, 'number'); // 上一次是0
return { number: state.number 1 };
});
console.log(this.state.number); // 0
// 第二次增加
this.setState((state) => {
console.log(state.number, 'number'); // 上一次是1
return { number: state.number 1 };
});
console.log(this.state.number); // 0
};
复制代码
打开控制台我们可以发现控制台打印0 0 0 1
。 前两个是0
是两次setState({...})
执行完毕之后都是0
,而后边打印的0 1
是两次callback
执行内部打印出来的。第一修改我们发现之前是0
,将number=0 1
,第二个修改依赖了之前的值,打印1
。
同样的道理,这段代码打印0 0 1 2
,相信你也能很好的理解
handleClick = (event: React.MouseEvent) => {
this.setState({ number: 1 });
this.setState((state) => {
console.log(state.number, 'number'); // 上一次是1
return { number: state.number 1 };
});
console.log(this.state.number); // 0
// 第二次增加
this.setState((state) => {
console.log(state.number, 'number'); // 上一次是2
return { number: state.number 1 };
});
console.log(this.state.number); // 0
};
// 依次打印 0 0 1 2
复制代码
由此可见当
setState
传入callback
形式的时,内部callback
的参数是上一次state
修改后的参数。所以我们可以在这里依赖上一次的state
变化作出修改。callback
的执行时机你可以将它看成为一种异步--当handleClick
同步代码执行完毕后callback
依次执行。但是实际他并不是传统意义上的异步。
浅谈原理
当然提到callback
形式的setState
,那么我们就来简单谈谈他内部的实现机制:
const state = { number: 0 };
const queue = [];
// 当所有同步代码执行完毕
// 同时当所有setState({...})执行完毕 会执行setState(() => {})
// 我们每次调用setState(() => {}) 其实会将callback推入react一个队列中
queue.push((state) => ({ number: state.number 1 }));
queue.push((state) => ({ number: state.number 1 }));
// 最终清空这个队列
const result = queue.reduce((state, action) => {
return action(state);
}, state);
console.log(result,'result')
复制代码
简单来说他的原理就是react
内部通过一个queue
的队列进行控制,在事件处理函数的结尾去依次清空队列传入上一个值。
"同步更新"
当然上边我们讲到了setState
是异步更新,但是我们想要setState
实现同步更新,这个时候应该怎么办呢?
我们来看看这段代码:
代码语言:javascript复制handleClick = (event: React.MouseEvent) => {
setTimeout(() => {
this.setState({ number: this.state.number 1 });
console.log(this.state); // 1
this.setState({ number: this.state.number 1 });
console.log(this.state); // 2
});
};
复制代码
当我们在setTimeout 下一个宏任务中去执行setState的时候,惊奇的发现 setState是同步执行的。
其实
setTimeout
函数中并不属于handleClick
事件中。它是下一次宏任务,在handleClick
事件函数中它是批量的,但是在setTimeout
下一个宏任务中他是同步更新的。
setState
执行机制
对于setState
的更新机制,究竟是同步还是异步。也就是所谓是否是批量更新,可以把握这个原则:
- 凡是**
React
**可以管控的地方,他就是异步批量更新。比如事件函数,生命周期函数中,组件内部同步代码。 - 凡是**
React
**不能管控的地方,就是同步批量更新。比如setTimeout
,setInterval
,源生DOM
事件中,包括**Promise
**中都是同步批量更新。
handleClick = (event: React.MouseEvent) => {
// 同样是同步更新 微任务同样不属于React管理的范围
Promise.resolve().then(() => {
this.setState({ number: this.state.number 1 });
console.log(this.state); // 1
this.setState({ number: this.state.number 1 });
console.log(this.state); // 2
});
};
复制代码
简单来聊聊setState
的同步异步
上边我们讲到的setState
的同步和异步本质上就是批量执行,和js
中的异步是完全没有关系的。(这点和Vue大相庭径
,vue
中是通过nextTick - promise - settimeout
)。
react
中的异步其实是内部通过一个变量来控制是否是同步或者异步,从而进行批量/单个更新。
之后我们会详细说到这里的更新机制,同时会尝试自己来实现一个setState
机制。
先来简单看看他的原理吧。
同步更新:
代码语言:javascript复制// 标记位
let isBatchingUpdate = false;
let state = { number: 0 };
function setState(newState) {
return { ...state, ...newState };
}
// 这样的话 内部就是同步的了 每次调用setState
// 就会及时更新State的值
setState({number:1})
setState({number:2})
复制代码
异步更新:
代码语言:javascript复制let isBatchingUpdate = false;
let queue = [];
let state = { number: 0 };
function setState(newState) {
if (isBatchingUpdate) {
// 批量更新 进入队列
queue.push(newState);
} else {
// 否则直接更新
return { ...state, ...newState };
}
}
// 这样的话 内部就是同步的了 每次调用setState
// 就会及时更新State的值
setState({ number: 1 });
setState({ number: 2 });
// 在事件函数处理结尾 批量执行queue中的setState
const result = queue.reduce((preState,newState) => {
return { ...preState,...newState }
},state)
复制代码
实质上你可以理解成为每次handleClick执行前, react
会重置标记位isBatchingUpdate
为true
,表示可控,进行异步批量更新。 结束之后再给他关闭isBatchingUpdate
变为false
进行异步更新。
// 标记位
let isBatchingUpdate = false;
let queue = [];
let state = { number: 0 };
function setState(newState) {
if (isBatchingUpdate) {
// 批量更新 进入队列
queue.push(newState);
} else {
// 否则直接更新
return { ...state, ...newState };
}
}
function handleClick() {
...
// React会在每次函数执行前进行一次封装调用
isBatchingUpdate = true
// 我们在React中书写的业务逻辑函数
setState({ number: 1 }); // 批量打印 0
setState({ number: 2 }); // 批量打印 0
// 我们自己书写逻辑结束
// 同样React也会在我们逻辑结尾进行一次封装调用
isBatchingUpdate = false
... // 比如清空队列
// 在事件函数处理结尾 批量晴空queue中的setState更新
state = queue.reduce((preState,newState) => {
return { ...preState,...newState }
},state)
}
handleClick()
复制代码
我们可以清楚的看到内部react
是给予isBatchingUpdate
这个变量去控制是否是"异步"批量更新。
当然他们的执行机制在17
之间react
中所有的事件都是委托到body
上去处理,所以它会每次都给我们的逻辑添加一些额外的处理(比如我们业务逻辑之中上边的代码和下边的代码)。17
之后是所有的事件委托到了root
上进行执行的事件,其实是一样的道理。
需要主要的是
react
中可控的setState
无论setState({})
或者setState(() => {})
都是批量更新的,而不可控的就是非批量更新的。
原理讨论&手动实现
上边我们来讲过了setState/state
的用法,当然除了上边的用法setState
还支持额外传入一个callback
。
setState({xxx:1},() => {
// do some thing
})
复制代码
这样的写法你可以理解为在所在setState
执行完毕后,页面渲染完成之后再去执行callback
。(它会在上边说到的两种setState
执行完毕后->渲染页面->执行之后的callback
)。
原理实现
之后我们会讨论关于react
中setState
的处理以及setState/state
手动实现。~