开始之前,先讲一下该文章能帮你解决哪些问题?
-
React Hooks是什么?
-
React Hooks是怎么实现的?(包含部分React源码)
- react hooks和class component的差异
- react hooks是怎么保存状态的
- 为什么setState是”异步”的
关于Fiber如果有问题,可以阅读之前的文章
前言
该文章涉及的源码部分基于React v17.0.2
PS.文章以useState为例介绍React Hooks的流程,其他的hook也属于这个流程之内,但是会有自己的逻辑处理
React Hooks是什么?
在class组件中,每次都只会生成一个class实例,组件状态都被存储在实例中,之后的每次更新,只是调用了class的render方法,在class中的状态都不会丢失。但是如果是函数组件,每次render更新都需要重新执行这个函数,函数组件也就没有保存状态的能力。所以在之前我们都会把函数组件当做纯展示组件,通过props来更新页面。所以Hooks就是赋予函数组件保存数据状态和执行side effects的能力。
既然函数每次render都是从新开始,React Hooks是怎么把上次的数据状态保存下来的呢?
React Hooks是怎么实现的?
虚拟dom自从react出现已经被提烂了,都知道,虚拟dom就是使用js数据来存储dom结构,每次的数据更新都是更新js的对象,通过把对象映射到dom上,来进行页面更新。在这里提出这个是因为需要做一个思维转换,所有的dom都是js中的一个数据结构,那所有的组件其实也是一个数据结构——在react中叫Fiber,函数组件也是一样,对应一个Fiber节点,如果把函数组件的状态存储到Fiber之上,每次执行函数时都从Fiber中取数据不就可以保存数据状态了么。其实所有组件本质上都是一个Fiber节点的数据结构,所有的数据都可以存储在fiber中。
hooks长什么样子?
我们直接快进到Hook的数据结构
reactFiberHooks.new.js
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
// 可以看到state存储在Fiber的memoizedState上
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
// 下次进入直接给next赋值
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
复制代码
在react源码中可以看到,每次useState,都新建一个hook,并把它挂载到当前的Hook.next,所以函数组件的所有的state都是以单链表的形式存储在fiber的memoizedState中,这也解释了为什么react官方有警告不能把hooks写在条件判断中。
hooks是怎么更新数据的?
还记得吗?react每次更新都会执行function。但是对于我们使用者来说,无论是初始化,还是update,在使用中都是执行的useState(),就像下面这样:
const [num,setNum] = use(0)
复制代码
其实在react内部,所有的初始化和更新都是两套代码
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
复制代码
分别对应:mountState和updateState,初始化阶段,初始化hook,新建上述的hook单链表,更新阶段更新state数据
初始化阶段
function mountState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === "function") {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch =
(dispatchAction.bind(null, currentlyRenderingFiber, queue): any));
return [hook.memoizedState, dispatch];
}
复制代码
调用useState的初始化会执行mountState,可以看到返回值是[hook.memoizedState, dispatch],我们可以看到dispatch就是我们调用setNum更新数据的函数,可以看看他执行了什么(下方代码删除了很多无关代码)
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A
) {
const update: Update<S, A> = {
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
const alternate = fiber.alternate;
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
// fiber调度更新机制
scheduleUpdateOnFiber(fiber);
}
}
复制代码
可以看到dispatchAction接收3个参数,fiber,queue和action,但是为什么我们setNum时候只传入了一个action,这是因为react已经通过dispatchAction.bind(null, currentlyRenderingFiber, queue)把当前的fiber和queue传入进去,所以当我们执行setNum时候传入action,会执行dispatchACtion,构建一个update队列,然后进行fiber的调度机制(和class 的setState使用的同一套机制)来触发更新,如果更新开始就会进入到下一次render,下一次render重新执行function,又会走到useState,这时候就会进入updateState的更新阶段。
更新阶段
function updateState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
复制代码
从updateState可以看到,updateState本质上走的也是updateReducer(所以updateState只是updateReducer的一个简易版,无论从使用上来看,还是实现上来看,都是这样)。所以直接跳转到updateReducer(删除了调度等其他一些不相关的代码)
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);
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
//调用传入的reducer更新state
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
复制代码
可以看到在更新阶段,是把update的队列循环merge,计算新的state数据,这也是setNum为什么是”异步”的原因
总结
-
一句话讲清楚Hooks实现原理:Hooks通过单链表形式将state存储在对应的Fiber结构中,每次更新按顺序读取执行。
-
setState是”异步”的,是因为react把状态存储在queue队列中,等待调度完成一起更新
-
几乎所有的Hooks使用相关的问题及解答都可以在React官网FAQ 中看到,强烈建议阅读(可能会遇到面试中的问题,譬如usePrevious怎么实现,useMemo, useRef , etc…)
-
感觉贴了太多的源码(尽管已经是简化了的),会影响阅读体验
-
文章涉及的源码部分均可以在这里找到