React Version | v16.8.6 组件类型 | Hooks Components
一、序言
在开发中,我们只知道useState返回的set,执行后值发生变化的话,就会触发组件重新渲染。但为什么呢?当时我脑海浮现了几个疑问:
- React为何使用了useState后就可以保存value,不管函数执行了多少次。
- setState后,value怎么被更新的,形象点就是setState(prev ⇒ return prev + 1)时,prev参数到底从哪里来的?
- setState后,React从哪里开始更新的,为什么React能知道要从这里开始更新?
带着这几个疑问,开始翻阅源码跟资料,希望下文能有所启发。
二、useState源码分析
React源码 | 源码地址 React项目本地配置 | vscode 配置调试react源码 - 掘金
代码定位
在React中搜useState
可以看到在最顶层的const React = { ..., useState}
,这里便是我们平常使用时 import { useState } from 'react';
所引入的函数,顺着这个代码,可以发现最终指向三个Dispatcher
:ContextOnlyDispatcher
、HooksDispatcherOnMount
、HooksDispatcherOnUpdate
,忽略用于边界情况异常上下文的Dispatcher
,最终分为两类:HooksDispatcherOnMount
和HooksDispatcherOnUpdate
。
可以看到不同状态下Dispatcher
的useState
是不同的,分为mountState
和updateState
。
源码分析
组件初始化时调用的useState -> mountState
,会初始化该hooks的上下文。而React的hooks,就是将一个存储了 值+更新回调 的链表。具体看Hooks的结构:
typescript
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
};
分析源码可以看到,memoizedState
便是存储的hooks对应的值,这也是函数组件执行多次,state仍可以保存状态的原因。
但这里会有一个新的疑问,既然Hook节点是一个链表的全局变量,那函数组件在运行时怎么知道当前useState
对应的Hook节点是谁呢?以及最初问题2?
关于这些,需要分别看state跟set如何获取和更新的,这里简化下useState的执行逻辑:
-
初始化Hook节点:
- 创建Hook节点,挂载到
workInProgressHook
- 初始化state值
- 包装set并挂载到对应Fiber节点上:这里会初始化UpdateQueue,这是当前Hook节点的更新链
- 创建Hook节点,挂载到
-
set state:上面初始化的时候提到包装set,具体代码看
mountState → dispatchAction
,包装的set后,每次set(action)
执行时,都会将action包装成一个Update节点推入Hook节点的UpdateQueue中,然后调用scheduleWork
触发更新渲染。所以可以看到,在set时,正常来讲其实是不会实时更新state值的。 -
get/update state:可以在
updateState-> updateReducer
里看到,这里有比较复杂的更新逻辑,简化下逻辑:- 取出对应Hook节点
- 不断应用上次
set(action)
:将上次通过set(action)
包装到update里的更新链全都取出,然后执行一遍,用于更新state(代码)
三、运行机制
对整个useState运行机制进行归纳:
- 组件初始化,通过
mountState
初始化Hook节点并挂载到Hooks链表上,这也是为何所有的hook api不允许在return 之后定义。 - 组件执行
set(action)
,初始化时对dispatchAction
柯里化的set,将action操作封装成Update节点挂载到Hook.update这个UpdateQueue
上,并通过scheduleWork
触发新一轮更新渲染。 - 组件重新渲染,也就是第二次执行useState时,会将Hook.update的所有更新操作出栈并执行,这将得到新的
memoizedState
,而这时state就是最新值了,所以setState的回调是在下次渲染时生效的。
四、题外话
了解了useState的机制,其实不难发现其跟useReducer基本没啥差别,甚至可以看到useState基本都是基于useReducer封装的,那我们如何通过useReducer也来封装一个useState呢?
typescript
type FCAction<S> = (newState: S) => S;
type SetState<S> = (newState: S | FCAction<S>) => void;
function useState<S>(initialState: (() => S) | S): [S, SetState<S>] {
const [state, dispatch] = useReducer<(state: S, newState: S) => S>(
(state, newState) => newState,
typeof initialState === "function"
? (initialState as () => S)()
: initialState
);
const setState: SetState<S> = (newState) => {
if (typeof newState === "function") {
dispatch((newState as FCAction<S>)(state));
} else {
dispatch(newState);
}
};
return [state, setState];
}