您现在的位置是:首页 >学无止境 >React 的源码与原理解读(十四):Hooks解读之三 useState&useReducer网站首页学无止境
React 的源码与原理解读(十四):Hooks解读之三 useState&useReducer
写在专栏开头(叠甲)
-
作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。
-
本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。
-
本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。
本一节的内容
这个章节主要讲解React 的 useState 和 useReducer 这两个 api,这是我们使用最多的 hook 了,他们允许我们在函数组件中也持有状态 state,然后对这个状态进行操作,这一章我们会从使用和源码两个角度深入讲解这两个 hooks
useState 的定义
useState()
是我们最常见的几个 hooks 之一,它运行我们传入一个初始值来初始化一个 state,之后返回给我们这个 state 和改变它的方法 setState:
const [state, setState] = useState(initialstate)
这里我们来看一个面试题:我们使用一个数组来接收 useState
的赋值,这是因为其使用的是 es6 的解构赋值,数组解构时变量的取值由数组元素的位置决定,变量名可以任意命名,这样的设计使得我们可以自定义 useState
的返回值,比如:
const [num, setNum] = useState(0)
useState 的使用
基本用法
对于 useState
的使用相信大部分的 React 使用者都了然于心,这里就不多介绍了,一个很简单的计时器的例子带过:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1)
}
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
</>
);
}
函数式更新
useState
也提供了函数式的更新,我们可以将之前的数值作为参数传入,得到更新后的结果:
function Counter() {
const [count, setCount] = useState(0);
function handleClickFn() {
setCount((prevCount) => {
return prevCount + 1
})
}
return (
<>
Count: {count}
<button onClick={handleClickFn}>+</button>
</>
);
}
两者的差异
这两种更新方法的区别是:函数式更新传入的值是当前的最新值,而前者则不然,因为 setState
是异步的,这是一个例子:
我们连续做三次 setCount
操作,前者只有一次生效,对同一次的渲染来说,count
是一个固定值,无论在哪里使用这个值,都是固定的。setCount(count+1)
的作用仅仅是把要更新的最新数据记录在了 React 内部,然后等待下次的渲染更新。
当使用函数式更新 state 的时候,这种问题就没有了,因为它可以获取之前的 state 值,也就是每次都是最新的值。
稍后我们会结合源码来深入理解这个特性
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
// 1
function handleClickFn() {
setCount(count => count + 1);
setCount(count => count + 1);
setCount(count => count + 1);
}
// 3
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>+</button>
</>
);
}
同步获取更新后的值
根据上面是描述,setState 只有在下次的渲染更新才会更新,所以它是更新是异步的,而我们知道,在 class 组件中,我们可以通过回调函数来获取最新的 state 的值,但是在 function 组件中呢?我们可以通过两种方法来实现
- 直接计算出我们需要的值
我们可以直接计算出我们需要的结果,更新这个状态,之后将我们计算出的结果提供给我们需要的函数即可
const handleClick = () => {
const newCount = count + 1;
setCount(newCount);
getList(newCount);
};
- 使用 useEffect 来监听
我们知道 useEffect
这个钩子可以监控一个依赖项的变化,因为我们不确定 state 什么时候发生改变,所以我们可以通过监听它的变化来,直到它发生变化才进行后续的操作
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
getList();
}, [count]);
const handleClick = () => {
setCount(count + 1);
};
}
覆盖对象而不是合并
useState
返回的 setState
和 class 组件的 setState
的区别是对于 object 的操作:
- class 组件中会合并你两次传入的对象
- 但是
useState
返回的setState
是直接覆盖原来的元素的
产生这个问题的主要原因是,hooks 内部使用了 object.is 对两次的数值进行比较,不清楚的可以查看上一篇的教程,它只会比较两个对象元素引用的地址是不是一致,不一致则直接替换
class App {
state = {
key1: "value1",
ey1: "value1",,
};
handleClick() {
this.setState({ key2: "value3" });
}
// key1: "value1", key2: "value3",
}
function App() {
const [Info, setInfo] = useState({ ey1: "value1", key2: "value2", });
// 手动展开再赋值
setInfo({ ...Info, key2: "value3" });
}
useState 的源码
mount 阶段
接下来我们来看 useState
的源码,它的部分逻辑和我们之前在 Lane
这一章中的是一致的,大家阅读起来应该会比较轻松,我们还是先从 mount 开始看,这里我们需要回顾一下一些概念,可以跟着我一行一行来回顾:
- 首先是我们获取传入的初始值,如果是函数的话,我们执行它获取结果作为我们的初始值
- 之后我们将我们的初始化放到 hook 节点的
memoizedState
和baseState
属性上,这部分在 hook 的原理这章提到了,memoizedState
代表上一次处理完成的 state,因为我们的初次挂载,所以它就是初始值,这个属性也是我们返回给用户的 state 属性。而baseState
的当前已经处理完的更新产生的 state,他是为了在一连串的更新执行过程中记录中间状态设定的,因为是初次挂载,已经处理完的就是当前的 state。 - 之后我们创建一个更新队列,我们将他们放在 hook 的 queue 属性中,其中
lastRenderedReducer
属性绑定我们上一次在setState
函数传入的内容,可能是数值或者一个函数;而lastRenderedState
属性存放我们上次render后的state - 最后我们使用
dispatchSetState
生成一个更新函数,返回给用户,这个函数被绑定在了 hook 更新的 dispatch 属性上,值得注意都是dispatchSetState
函数本来需要传入三个参数,在使用 bind 将其中两个参数传入之后,我们只需要提供一个参数即可,而这个新生成的函数就是我们的setState
更新函数
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
// 如果传入的初始值是一个函数,直接执行获得结果
if (typeof initialState === 'function') {
initialState = initialState();
}
// 更新 hook 缓存的数据
hook.memoizedState = hook.baseState = initialState;
// 创建一个更新队列
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null, // 依然是环状链表,这个属性指向链表的最后一个节点
interleaved: null,
lanes: NoLanes,
dispatch: null, // setState
lastRenderedReducer: basicStateReducer, // 上次render传入的操作
lastRenderedState: (initialState: any), // 上次render后的state
};
hook.queue = queue;
// 生成更新函数
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
之后我们来看 dispatchSetState
这个函数做了什么:
- 首先它获取了本次更新的优先级 ,
requestUpdateLane
我们在 lane 这章着重讲过,不清楚的可以回去查看 - 之后我们将这个更新封装成一个 Update 节点,要注意,这个 Update 节点是为了我们 hooks 服务的,和之前提到的
updateQueue
那节的不一样,但是处理方式基本一致,它会被挂载在 hook 的 queue 的 pending 属性上 - 之后判定这个更新是不是在渲染阶段发生,如果是把它拼接到 queue 的 pending 上,并且设定一个标识
- 如果是正常更新,我们还是首先把它拼接到 queue 的 pending 上,如果当前节点没有更新任务,直接计算出新的 state,然后与之前的 state 对比,若没有更新,则直接退出;
- 若有更新,我们调用
scheduleUpdateOnFiber
函数,这个函数在 Lane 这章节已经详细讲过了,可以回去看这个函数
总结来说就是,把我们的 setState
的更新内容(数据或者函数)放到了 hook 的queue 的pending 属性中,如果组件之前没有更新事件,直接计算出 state 的新值,值不变则不触发更新,否则新建一个更新事件开始调度
function dispatchSetState<S, A>(fiber: Fiber, queue: UpdateQueue<S, A>, action: A) {
// 获取lane
const lane = requestUpdateLane(fiber);
// 将 action 操作封装成一个 update节点,用于后续构建链表使用
const update: Update<S, A> = {
lane, // 优先级
action, // setState 传入的内容,可能是操作或者数值
hasEagerState: false, // 紧急状态
eagerState: null, // 紧急状态下提前计算出的结果
next: (null: any), // 指向到下一个节点的指针
};
// 在渲染阶段的更新,拼接到 queue.pending 的后面
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
// 把 update 更新到 queue.pending 指向的环形链表里
enqueueUpdate(fiber, queue, update, lane);
const alternate = fiber.alternate;
// 如果当前节点没有更新任务
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
const lastRenderedReducer = queue.lastRenderedReducer; // 上次render后的reducer,在mount时即 basicStateReducer
if (lastRenderedReducer !== null) {
let prevDispatcher;
const currentState: S = (queue.lastRenderedState: any); // 上次render后的state,mount时为传入的initialState
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true; // 表示该节点的数据已计算过了
update.eagerState = eagerState; // 存储计算出来后的数据
if (is(eagerState, currentState)) {
// 若这次得到的state与上次的一样,则不再重新渲染
return;
}
}
}
// 有更新任务
const eventTime = requestEventTime();
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane, action);
}
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
) {
// 标识render阶段的更新产生了
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
const pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
function enqueueUpdate<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
update: Update<S, A>,
lane: Lane,
) {
// 交错更新
if (isInterleavedUpdate(fiber, lane)) {
const interleaved = queue.interleaved;
if (interleaved === null) {
update.next = update;
pushInterleavedQueue(queue);
} else {
update.next = interleaved.next;
interleaved.next = update;
}
queue.interleaved = update;
} else {
const pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
}
update 阶段
之后我们来看看 update 阶段发生了什么,这里先要提一句,我们在 mount 阶段仅仅是初始化了 hooks,返回一个 setState 函数,而这个函数的作用是将传入的 action 放到 hook 的更新列表中,但是实际上的更新操作是在 update 阶段才会去执行的,这也是我们上面提到的函数式更新和数值更新产生差异的原因,我们具体来说:
我们承接上文,scheduleUpdateOnFiber
函数开始了一个调度,当它执行到了 renderWithHooks
的时候,此时因为以及渲染出了我们的 Fiber,所以会进入更新的钩子,就是 updateState
函数,而它直接调用了 updateReducer
函数,这个其实是 useReducer
这个钩子的方法,我们马上会讲到
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
这里我们来看一下 updateReducer
这个函数,它是我们传入 updateReducer
的逻辑,也就是我们 setState 的更新逻辑,如果我们传入一个值,那么我们直接返回这个值,如果我们传入一个函数,我们返回计算后的结果,而这个函数需要传入一个参数,也就是我们函数式更新传入的上一次的渲染值:
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
我们直接来看这个函数updateReducer
,他的逻辑和我们之前的 fiber 上的 Update 的更新逻辑类似,大家应该很熟悉,我们简单来概括一下:
-
首先获取当前的 pending 队列和上次遗留下来的队列,把他们合并到一起
-
之后遍历队列中所有的 hook 更新,根据当前的优先级判定它能不能执行
-
若符合当前优先级的,则执行该 update 节点的 action,计算出新的 state,它将作为我们的返回值,可以看到,这里我们传入的当前更新前计算出的最新的值,也就说,我们每次传入的值都是到这个更新触发前最新的 state,这样保证上文中的函数式更新可以每次都能正常更新,而如果我们不使用函数式更新,那么我们传入的值即使包含了 state,那这个 state 也不过是我们调用那个时刻的 state ,但是如果它之前有其他的更新就不能得到响应了;若优先级不符合的,则将此节点到最后的所有节点都存储起来,便于下次渲染遍历,并将到此刻计算出的 state 作为下次更新时的基准 state。
**注意:**这个逻辑我们需要注意,对于我们遇到的所有可执行的任务,我们都需要执行其 action,然后更新我们执行它之后的 state;但是如果我们已经遇到一个不可执行的任务了,即使当前任务可以执行,我们也要把可执行任务放到
BaseQueue
中,那么在我们遍历了全部的任务之后,我们的BaseQueue
中存放的是从第一个不可执行任务之后的全部任务,他们会在下一次渲染的时候执行,这个渲染需要一个baseState
,也就是我们初始化的newBaseState
这个变量,它的值会在第一次出现不可知性的任务的时候,把执行到目前的 state 保存下来,也就是说,BaseState
是和BaseQueue
配合使用的, 一次渲染后,BaseState
的值和渲染得到的结果不一定是相同的!只有在全部任务执行完成的情况下,他们才可能完全相同 -
遍历完所有可以执行的任务后,得到一个新的 newState,然后判断与之前的 state 是否一样,若不一样,则标记该 fiber 节点需要更新,并返回新的 newState 和 dispatch 方法。
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
//上次遗留下来的优先级不够的任务
let baseQueue = current.baseQueue;
// 获取 queue 队列
const pendingQueue = queue.pending;
// 拼接链表
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState; // 上次的渲染的结果值,每次循环时都计算得到该值,然后供下次循环时使用
// 新的 BaseState,用于下次渲染的
let newBaseState = null;
// 新的 basequeue
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 优先级不足,跳过此更新,放到暂存执行的队列中
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
// 如果第一次出现了不能执行的了,我们要把当前的计算结果保存下来,因为我们会将此节点到最后的所有节点都存储起来,所以此时我们的 newBaseState 就是当前得到的值,这样下次渲染的时候,我们获得的就是这个不能执行的节点前的执行结果
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// 更新优先级
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
// 标识跳过的任务的优先级
markSkippedUpdateLanes(updateLane);
} else {
// 之前的节点有不能执行的,那么后面的节点都要缓存
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
// 该update需要执行,所以我们永远不能跳过他,使用NoLane优先级,可以避免上面的判断会跳过该步骤
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// 已经执行过了(mount中)
if (update.hasEagerState) {
newState = ((update.eagerState: any): S);
} else {
// 计算出当前位置的新的state,注意,这个 newState 是作为这一步的返回结果的,不一定我们的 newBaseState,所以就算要被缓存的节点,只要能执行就需要处理
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
// 所有的update都执行了,那么没有下一次渲染了,所以下一次需要的 baseState就是计算结果
newBaseState = newState;
} else {
// 有低优先级的update任务,则next指针指向到第1个,形成单向环形链表,
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// 若newState和之前的state不一样,标记该fiber需要更新
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState; // 整个update链表执行完,得到的newState,用于本次渲染时使用
hook.baseState = newBaseState; // 下次执行链表时的初始值
hook.baseQueue = newBaseQueueLast; // 新的update链表,可能为空
queue.lastRenderedState = newState; // 将本次的state存储为上次rendered后的值
}
// 交错更新,省略.....
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
useReducer 的定义和使用
useReducer
和 useState
类似,但是它接收一个形如 (state, action) => newState 的 reducer
,并返回当前的 state 以及与其配套的 dispatch
方法。
const [state, dispatch] = useReducer(reducer, initState,init);
useReducer接收两个参数:
-
第一个参数:reducer函数,它告诉我们根据传入的
state
和action
怎么样计算返回一个最新的newState
-
第二个参数:初始化的hook 传入的 state
-
第三个参数(可选):用于懒创建state的函数,如果我们使用了第三个参数,那么第二个参数会作为第三个参数的参数传入懒创建函数,第三个参数的返回值是将作为我们的 state
const initFunc = (initialCount) => { if (initialCount !== 0) { initialCount=+0 } return {count: initialCount}; } const [state, dispatch] = useReducer(reducer, initialCount, initFunc);
-
返回值为最新的 state 和 dispatch 函数,其用来触发 reducer 函数
在我们初始化的时候,我们的 state 也就绑定在了 reducer 函数中,此时,我们只需要在调用返回的 dispatch
时传入我们的 action,就可以触发对应的操作,下面是一个简单的例子,我们使用 useReducer
编写了一个具有加减功能的计数器:
const initialState = {count: 0};
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
};
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
同样的,如果 useReducer
返回的值和当前的一样,React不会更新组件,因为React内部使用了Object.is 的语法
useReducer 源码
根据上面的描述我们知道了,useReducer
其实就是一个简单的可以自定义更新方法的 useState
,那么他们的源码也应该极为相似,所以这里我们就直接给出源码和注释:
mountReducer
方法和mountState
的差距仅仅是我们需要通过 第二个第三个参数来初始化我们的 state,最后同样是返回我们的 state 和 dispatch触发器- 而
updateReducer
方法已经给出了,只不过我们在updateReducer
方法中我们传入了 basicStateReducer ,而此处,我们使用了自定义的reducer
函数,所以此处我们需要一个(state, action) => newState
的函数作为参数
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
// 如果是懒创建的 initState,我们调用函数得到其值,否则直接获取其值
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
// 初始化 memoizedState 和 baseState
hook.memoizedState = hook.baseState = initialState;
// 初始化更新队列
const queue: UpdateQueue<S, A> = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
// 创建 dispatch
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
拓展&面试题
解读完了源码,我们来结合源码讲几个面试题:
- useState 是异步还是同步
这题相信大家都已经明白了,之前我们在 setState 中也讲过,useState
的更新是跟我们的 Lane 优先级联动了,它每次调用给出的 setState
的时候,并没有立即执行这个更新返回新的值,而是创建一个更新并且保存在 hook 的数据结构中,之后触发更新,只有满足当前 Fiber 优先级的的更新会被执行,所以显然它是异步的,如果你想马上获得更细完毕的 state,我们也给出了解决方案
- 一并多个调用 dispatch 会触发多次更新吗
这题我们也应该很快得出答案,如下的多次触发 dispatch ,他们会生成多个更新任务,显然这些任务的优先级是相同的,那么在处理这些更新的时候,也会在一次渲染中一并处理,所以不会触发多次更新
const handleClick = () => {
setCount(count => count + 1);
setCount(count => count + 1);
};
- 使用 props 作为 useState 的值会动态改变吗
这题也很简单,形如下的赋值,只有在第一次调用初始化的时候使用的 mount 逻辑,此时会把 props.count 的值赋给我们的 hook,此后不论 props.count 如何变化,都只会触发 update 逻辑,那么 props.count 的值就不会产生影响了
const [count, setCount] = useState(props.count);
如果要实现题目要求的效果,我们应该使用 useEffect
和 useState
配合使用
function App(props) {
const [count, setCount] = useState(props.count);
useEffect(() => {
setCount(props.count);
}, [props.count]);
}
总结
最近在忙 OSPP。也是抽空完成了 useReducer
和 useState
两个为赋值相关 hooks 的源码解读,他们的逻辑类似:
- mount 的时候,传入一个初始值(或者函数),然后建立一个更新队列,最后返回一个 dispatch 作为我们的修改 state 的触发器
- dispatch 在我们调用的时候创建了一个更新,放到了我们的更新队列中,之后开始一次更新的调度
- 当调度到更新组件时,会 update 逻辑,这个逻辑会根据当前 fiber 的优先级来判定更新队列中哪些更新可以触发,然后返回这次渲染更新后的新 state,对于暂时不能触发的更新,我们将他们按照逻辑缓存起来
- 触发更新时,useState 使用一个默认的函数来触发更新,可以选择传入一个数值或者使用一个函数接收上一次更新后产生的 state 来触发更新;而 useReducer 则允许我们自定义更新函数的逻辑
那么接下来离预定的 hook 部分还差一个 useEffect,我会尽快完成这个 React hook 最后的讲解,敬请期待!