state基础使用
在react
中组件的数据来源两个部分,一个是组件自身的state
,一个是接受父组件传入的props
。这两种状态的改变都会造成视图层面的更新。
当然我们需要注意的是,改变组件内部状态一定是要通过setState
进行更新组件内部数据的,直接赋值的话并不会触发页面的更新的。(state是只读的)
每次渲染state
和prop
都是相互独立的(因为是各自函数作用域内的变量),每次独立渲染函数中的state
和prop
都是保持不变的常量。
每次改变state/props
造成函数组件重新执行,从而每次渲染函数中的state/props
都是独立的,固定的。
注意这里的固定和独立这两个关键字。
- 固定表示函数在开始执行时已经确定了本次
state/props
的值。- 独立表示每次运行函数的
state/props
都是各自独立作用域中的。
函数组件中当使用setState
时只有以下两种情况函数组件才会重新执行:
- 对于值类型,state 的值改变
- 对于引用类型,state 的引用改变
既然state是只读的,当state是一个具有深嵌套层次的对象时,要避免对其进行浅拷贝(比如展开运算符)再更改数据,这会造成一些意料之外的问题。
关于setState是“同步”
还是“异步”
我在这里可是加了双引号的,需要注意的是,在JavaScript中也有同步和异步,它们可是不同的概念。
React中的 “异步更新”,处于性能的考虑,setState会放入到一个更新队列中,会对 state 更新进行批处理。所谓的异步和Promise
以及setTimeout
这些微/宏任务是无关的。
这也是和Vue
中异步更新策略不同之处,在vue3
中是通过proxy
来代理目标对象,当修改代理对象值的时候会触发对应的set
函数从而触发更新运行对应收集的effect
进行模版更新。
JavaScript同步
同步操作是指按顺序执行的任务。在一个同步操作中,当前任务必须完成,才能开始下一个任务。这意味着在一个任务完成之前,程序会被阻塞,无法执行其他任务。
JavaScript异步
异步操作允许在等待某个任务完成的同时继续执行其他任务。这样可以避免长时间的阻塞,提高应用程序的响应速度和性能。
细说 setState
代码示例:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0,
};
}
// 在事件处理函数中setState的调用会批量异步执行
handleClick = () => {
// 第一次增加
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>
);
}
}
这段代码中,定义了一个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
的值,那么setState
支持传入一个callback
,它接受一个参数就是上一次传入的值:
handleClick = () => {
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 0 1 2
,相信你也能很好的理解
handleClick = () => {
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
};
由此可见当
setState
传入callback
形式时,callback
的参数是上一次state
修改后的参数。所以可以在这里依赖上一次的state
变化作出修改。callback
的执行时机你可以将它看成为一种异步,当handleClick
同步代码执行完毕后callback
依次执行。但是实际他并不是传统意义上的异步。
然后看以下示例代码:
const state = { number: 0 };
const queue = [];
// 我们每次调用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);
简单来说就是react
内部通过一个queue
的队列进行控制,在事件处理函数的结尾去依次清空队列传入上一个值。
但是我们想要setState
实现同步更新,这个时候应该怎么办呢?
我们来看看这段代码:
handleClick = () => {
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
下一个宏任务中他是同步更新的。
所以(仅限于React18版本之前,React18版本之后统一都是批量更新):
凡是React
可以管控的地方,他就是批量更新。比如事件函数,生命周期函数中,组件内部同步代码。」
凡是React
不能管控的地方,就是单个状态同步更新。比如setTimeout
,setInterval
,源生DOM
事件中,包括Promise
中都是单个状态同步更新。
浅谈 setState 实现机制
setState
的同步和异步本质上就是批量执行,和js
中的异步是完全没有关系。(这点和Vue
不一样,Vue
中是通过nextTick - promise - setTimeout
)。
React
中的异步其实是内部通过一个变量来控制是否是同步或者异步,从而进行批量/单个更新。
先来简单看看他的原理吧。
同步单个状态更新:
// 标记位
let isBatchingUpdate = false;
let state = { number: 0 };
function setState(newState) {
return { ...state, ...newState };
}
// 这样的话 内部就是同步的了 每次调用setState
// 就会及时更新State的值
setState({ number:1 });
setState({ number:2 });
批量更新:
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)
queue = [];
}
handleClick()
所以React
内部是通过isBatchingUpdate
这个变量去控制是否是"异步"批量更新。
总结
React中setState
的"同步和异步"本质上就是状态单个/批量执行,和js
中的同步异步是完全没有关系的。