众所周知,很多人都知道react的this.setState()异步的,并且只能通过这个函数来改变状态。但是,都是知其然,不知其所以然。
其实不是不能通过this.state来改变状态,只是这样的改变并 不会引起组件的更新,因为react不像vue那样监听属性的改变来更新视图,而是通过this.setState()函数的调用来更新视图,前面我们写了一个简单的实现。
import { renderComponent } from '../react-dom'
class Component {
constructor(props = {}){
this.props = props;
this.state={}
}
setState(stateChange) {
Object.assign(this.state, stateChange);
// 更改参数以后需要重新渲染组件
renderComponent(this,)
}
}
export default Component;
就是调用函数后直接更新视图,这是没有做任何的性能优化。如果说遇到下面的情况:
componentDidMount() {
console.log('组件加载完成')
for(let i = 0; i < 10; i++) {
console.log(i)
this.setSate({
num: i
})
}
}
循环更新状态,在很短的时间内,this.setState()调用的10次,可能也有百次千次的情况,这个时候如果我们不做优化,直接渲染组件,this.setState()调用了多少次,组件就更新了多少次,这样就会很消耗性能。还有可能导致内存溢出。
而真实的this.setState函数内部做了相应的性能优化,使用react框架来测试一下,例如:
import React from 'react'
import ReactDOM from 'react-dom'
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
num: 1
}
}
componentWillMount() {
console.log('组件将要加载')
}
componentDidMount() {
console.log('组件加载完成')
for(let i = 0; i < 10; i++) {
console.log(i)
this.setSate({
num: i
})
}
}
componentWillReceiveProps() {
console.log('组件将要接受新的参数')
}
componentWillUpdate() {
console.log('组件将要更新')
}
componentDidUpdate() {
console.log('组件更新完成')
}
componentWillUnmount() {
console.log('组件卸载')
}
handlerClick() {
this.setSate({
num: this.state.num + 1,
})
}
render() {
return (
<div className='active'>
{this.state.num}
<h1 >react</h1>
<button key='9' onClick={this.handlerClick.bind(this)}>改变状态</button>
</div>
)
}
}
ReactDOM.render(<Home title='home' />, document.getElementById('app'));
我们在组件加载完成的生命周期中,调用了10次this.setState(),可以看到打印了10次,但是组件将要更新和组件更新完成只执行了一次。
这是因为react短时间内会将多个 setState() 调用合并为一次更新。batch update 批量更新,这就是react做的性能优化,下面我们自己来实现一下。
先来分析一下 this.setState()函数的参数,上面提到可一种,对象。
另一种 setState() 的形式,接受一个函数。这个函数将接收前一个状态作为第一个参数,应用更新时的 props 作为第二个参数,代码如下:
this.setState((prevState, props) => ({
num: prevState.num + 1
}));
先来分析一次,this.setDtate()需要做些什么?
首先需要存储上一次的状态,然后延迟更改状态并更新了组件。我们怎么去找这个点呢?刚好最后一个状态的时候去更新组件呢?
react利用了队列的先进先出特性,来实现了这个功能。
我们在react文件夹下面创建一个set-state-queue.js。这个文件负责实现以上的功能。
我们需要两个队列,一个存储当前的状态和相应的组件实例,一个只存储组件实例。
首先,是需要存储传递进来的状态值。然后再去存储当前的组件实例,只存一次当前的组件实例。
我们可以直接把组件的状态挂在组件实例上,便于后面使用。
import { renderComponent } from "../react-dom";
const setSateQueue = []; //存储组件实例及对应的状态
const renderQueue = []; // 存放组件实例
export function enqueueSetState(stateChange, component) {
// 在合适的时候调用flush();
if(setSateQueue.length === 0) {
setTimeout(() => {
flush();
},0)
}
// 短时间内合并多个setSate
setSateQueue.push({
stateChange,
component
})
// 看下renderQueue中有没有组件
let r = renderQueue.some((item)=>item === component);
if(!r) {
renderQueue.push(component)
}
}
flush函数负责更新状态,并在最后一个状态更新完以后,渲染组件。因为flush是异步调用的,所以当开始调用的时候,setSateQueue已经被加入了很多内容。
stateChange就是当前this.setState()的第一个参数,component是当前组件实例。
stateChange有两种情况,一种是对象,一种是函数。
setSateQueue队列遵循先进先出的原则,直到最后进去的一位取出,这是队列为空,就可以更新改变状态然后渲染组件了。
renderQueue组件队列也是先进先出。
function flush() {
let item, components;
while(item = setSateQueue.shift()) {
const { stateChange,component } = item;
// 保存之前状态
if(!(component && component.prevState)) {
component.prevState = Object.assign({},component.state)
}
if(typeof stateChange === 'function') {
Object.assign(component.state, stateChange(component.prevState, component.props))
console.log(component.state)
} else {
Object.assign(component.state, stateChange)
}
component.prevState = component.state;
}
while(components = renderQueue.shift()) {
renderComponent(components)
}
}
调整下react/component.js
import { renderComponent } from '../react-dom'
import { enqueueSetState } from './set-state-queue'
class Component {
constructor(props = {}){
this.props = props;
this.state={}
}
setSate(stateChange) {
enqueueSetState(stateChange, this);
}
}
export default Component;
因为所有的组件都继承Componnet。所有组件都可以直接调用this.setState()。this就是当前组件。
import React from './react'
import ReactDOM from './react-dom'
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
num: 1
}
}
componentWillMount() {
console.log('组件将要加载')
}
componentDidMount() {
console.log('组件加载完成')
for(let i = 0; i < 10; i++) {
console.log(i)
this.setSate({
num: i
})
}
}
componentWillReceiveProps() {
console.log('组件将要接受新的参数')
}
componentWillUpdate() {
console.log('组件将要更新')
}
componentDidUpdate() {
console.log('组件更新完成')
}
componentWillUnmount() {
console.log('组件卸载')
}
handlerClick() {
this.setSate({
num: this.state.num + 1,
})
}
render() {
return (
<div className='active'>
{this.state.num}
<h1 >react</h1>
<button key='9' onClick={this.handlerClick.bind(this)}>改变状态</button>
</div>
)
}
}
ReactDOM.render(<Home title='home' />, document.getElementById('app'));
实现了跟真实react一样的效果。用react测试一下,setState
import React, {Component, PureComponent } from 'react';
class App extends Component {
state = {
count: 0
};
componentDidMount() {
// 生命周期中调用
this.setState({ count: this.state.count + 1 });
console.log("lifecycle: " + this.state.count);
setTimeout(() => {
// setTimeout中调用
this.setState({ count: this.state.count + 1 });
console.log("setTimeout: " + this.state.count);
}, 0);
document.getElementById("div2").addEventListener("click", this.increment2);
}
increment = () => {
// 合成事件中调用
this.setState({ count: this.state.count + 1 });
console.log("react event: " + this.state.count);
};
increment2 = () => {
// 原生事件中调用
this.setState({ count: this.state.count + 1 });
console.log("dom event: " + this.state.count);
};
render() {
return (
<div className="App">
<h2>couont: {this.state.count}</h2>
<div id="div1" onClick={this.increment}>
click me and count+1
</div>
<div id="div2">click me and count+1</div>
</div>
);
}
}
export default App;
探讨前,我们先简单了解下react的事件机制:react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx
中常见的onClick
、onChange
这些都是合成事件。
那么以上4种方式调用setState()
,后面紧接着去取最新的state,按之前讲的异步原理,应该是取不到的。然而,setTimeout
中调用以及原生事件中调用的话,是可以立马获取到最新的state的。根本原因在于,setState并不是真正意义上的异步操作,它只是模拟了异步的行为。React中会去维护一个标识(isBatchingUpdates
),判断是直接更新还是先暂存state进队列。setTimeout
以及原生事件都会直接去更新state,因此可以立即得到最新state。而合成事件和React生命周期函数中,是受React控制的,其会将isBatchingUpdates
设置为 true
,从而走的是类似异步的那一套。
多次请求合并,提高性能:
increment = () => {
// 合成事件中调用
this.setState({ count: this.state.count + 1 });
this.setState({ count: 3});
this.setState({ count: 4 });
};
异步里面多次调用不会合并:
increment = () => {
// 合成事件中调用
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: 3});
this.setState({ count: 4 });
})
};
总结:
这里只是介绍了react的一些思想,真正的源码远不是这么简单,想了解更多的,可以直接去解读源码,大致思想是一致的。
setState
只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout
中都是同步的。setState
的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。setState
的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState
,setState
的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState
多个不同的值,在更新时会对其进行合并批量更新。