使用 react hooks 很久了,对于 useState 的理解一直都是模糊不清,终于下定决心把它理清楚,看似简单的 useState 暗藏玄机,先来看一段代码
import { useState } from 'react';
console.log('函数外');
const HomePage = () => {
console.log('函数内 顶部');
const [num, setNumber] = useState(0);
const [count, setCount] = useState(0);
const handerNum = () => {
for (let i = 0; i < 5; i++) {
console.warn(`第${i}次执行`);
setTimeout(() => {
console.log('set number 前', num);
setNumber((state) => {
console.log('set number 内', state, state + 1);
return state + 1;
});
console.log('set number 后', num);
console.log('---------------------------->');
}, 1000);
console.log('timer 后输出');
}
};
const handerCount = () => {
for (let i = 0; i < 5; i++) {
console.warn(`第${i}次执行`);
setTimeout(() => {
console.log('set count 前', count);
setCount(count + 1);
console.log('set number 后', count);
console.log('---------------------------->');
}, 1000);
console.log('timer 后输出');
}
};
console.log(`return前, num: ${num}, count: ${count}`);
return (
<div>
<Button onClick={handerNum}>number: {num}</Button>
<hr />
<Button onClick={handerCount}>count: {count}</Button>
</div>
);
};
export default HomePage;
这就是一个普通的 jsx 文件,思考下面两个问题
- 当点击number按钮时输出什么
- 当点击count按钮时输出什么
下面我们揭晓答案
点击 number 按钮时输出如下
点击 count 按钮时输出如下
我们在 setNumber 中传入的是一个回调,在 setCount 中传入的是一个 表达式,同样是 setState 为什么传入表达式与传入函数的执行结果会有这么大差异(这里面的setState指的是setNumber 和 setCount)下面我们逐一解释handerNum
的执行:
const handerNum = () => {
for (let i = 0; i < 5; i++) {
console.warn(`第${i}次执行`);
setTimeout(() => {
console.log('set number 前', num);
setNumber((state) => {
console.log('set number 内', state, state + 1);
return state + 1;
});
console.log('set number 后', num);
console.log('---------------------------->');
}, 1000);
console.log('timer 后输出');
}
};
点击 number 按钮如何执行的
我们知道 js 是顺序执行,当点击 number 按钮时,会调用 handerNum
方法,执行 for 循环,第一次for循环 执行,遇到console.warn(
第${i}执行);
直接输出 i
的值, 遇到setTimeout
会将 setTimeout
放入定时器线程,并记录延迟时间,当延迟时间结束(1秒后),会把 setTimeout
的回调函数放入事件触发线程,主线程空闲时会调用事件触发线程中的回调函数,接着向下执行遇到console.log('timer 后输出');
直接输出,第一次for循环结束,以此类推,for循环执行五次以后,定时器线程中就有5个 setTimeout
,定时器线程会根据延迟时间长短确定优先级,相同时间遵循先进先出原则,依次放入事件触发线程,等待主线程空闲调用。至此这个方法执行完毕,这里面有个疑点:为什么console.log('timer 后输出');
的执行会在 setTimeout
之前,因为 setTimeout
是异步任务,总结下: 同步优先,异步靠边,回调垫底,这就是为什么下面红框输出会在一起
在说 setTimeout
回调之前我们先来说说 setState
的执行顺序
当 react 工作流执行的时候(我们把react api 的执行叫react工作流),setState
会判断传入的参数是函数还是值(值其实也是表达式,因为表达式最终会计算出具体值)参数类型不同执行的机制也有所差异,setState
方法本身就是一个异步方法,分为两种 mountState 和 updateState, 如果首次执行setState
的时候,先执行 setState
回调函数计算出值,然后立马调用组件方法(上面代码里组件方法为HomePage)在组件更新完成后,再执行 setState
后面的代码,这部分属于 mountState 再次执行setState
时,react 会先调用组件方法(引起组件更新),接着执行setState
的回调计算出更新值,执行渲染输出也就是return的reactDom,最后执行setState
后面的代码
下面我们看控制台输出和上面描述是不是一样
接下来我们再看 setTimeout
回调的执行
() => {
console.log('set number 前', num);
setNumber((state) => {
console.log('set number 内', state, state + 1);
return state + 1;
});
console.log('set number 后', num);
console.log('---------------------------->');
}
第一个 setTimeout
回调的执行,console.log('set number 前', num);
控制台直接输出,往下遇到 setNumber
方法,(因为首次执行先执行setState)先执行传入的是回调,console.log直接输出set number 内 0 1
return 计算出值,调用组件函数(也就是HomePage)整个函数组件会重新执行,所以输出
函数内 顶部
return前, num: 1, count: 0
最后执行 setNumber
后面的代码 所以输出
set number 后 0
---------------------------->
第一个 定时器回调执行完成,也就是 mountState,完成
第二个 setTimeout
回调执行,console.log('set count 前', count);
控制台输出
set number 前 0
setNumber
的回调会在调用组件方法之后,return
之前执行,也就是在遇到 setNumber
会先重新调用下函数组件所以会输出
函数内 顶部
紧接着执行 setNumber
的回调,输出如下,return 最新state
set number 内 1 2
函数组件会使用最新的state做render输出,也就是执行函数组件中的 return
关键字,在执行 return
先执行 return
上的 console.log(
return前, num: ${num}, count: ${count});
输出如下
return前, num: 2, count: 0
最后执行 setNumber
后面的代码输出
set number 后 0
index.tsx:35 ---------------------------->
剩下定时器回调执行同上
接下来我们整理下 setState
回调作为参数的执行顺序
mountState:
先调用回调方法计算出state ➞ 调用组件方法 ➞ return ➞ setState后面代码
updateState:
先调用组件方法 ➞ setState回调计算出新的state ➞ return ➞ setState后面代码
接下来我们再看 setState 传入表达式的执行