1. react hooks简介
react hooks 是react 16.8.0 的新增特性,它可以让你在不编写class的情况下使用state以及其他的一些react特性。
在过去的react版本中,如果我们想要使用状态管理或者想要在render之后去做一些事情,我们必须使用class组件才能办到。但是现在hooks的出现,使得函数组件也同样可以做到。
hooks实际上是一些以use开头来明名的函数,它就像钩子一样,把函数组件不具备的特性钩进来,使得函数组件也同样可以使用这些特性。
话不多说,下面我们就开始看一下第一个hook的api。
2.useState 使用规则
function User(props) {
let [count, setCount] = useState(0); // 这里的count,setCount类似于class组件里的state,setState,我们要改变count这个状态的值,只需要调用setCount这个函数就可以了,它接受一个参数,就是你要更改的值。
let [name, setName] = useState('Mary'); // 你可以在组件内部多次调用useState来创建多个状态变量
return <div>
<div>当前计数: count</div>
<button onClick={() => { setCount(count+1); }}>count+1</button>
</div>
}
useState使得我们可以在函数组件里使用状态,它接受一个参数,就是当前状态的初始值。返回两个变量,第一个变量就是我们的状态变量,第二个就是改变这个状态的函数,类似于class组件里的state和setState。
注意,这里useState返回的是一个数组,所以变量的名字是我们自己任意取的。
useState | class state |
---|---|
可以在组件内部多次使用,创建多个状态变量 | 只有一个state对象 |
set函数,传进来的参数完全覆盖该状态值 | 合并state |
3.源码分析
当前react版本为16.9.0。打开源码,我们首先从react.js文件入手,找到useState的源码。
import {
useCallback,
useContext,
useEffect,
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState, // 在这里
useResponder,
} from './ReactHooks'; // 所以我们要找的源码在这个文件里面
我们在进到ReactHooks.js文件里看一下
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
从上述代码可以看出,我们的useState函数是挂到dispatcher对象上面的,那dispatcher对象到底是什么呢,我们再进到resolveDispatcher函数里看一下。
dispatcher对象被赋值为ReactCurrentDispatcher.current,我们在进一步看一下ReactCurrentDispatcher是什么。
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
/**
* @internal
* @type {ReactComponent}
*/
current: (null: null | Dispatcher), // current是Dispatcher类型的
};
export default ReactCurrentDispatcher;
dispatcher我们看到ReactCurrentDispatcher.current被初始化为null,似乎到这里我们什么也没找到。
但我们找到了一条线索,那就是useState其实是挂载ReactCurrentDispatcher.current对象上面的,所以我们只要找到它被赋值的地方就可以了。
但这部分的内容,实际上属于fiber调度的范畴,所以我们就简单提一下,不做过多阐述,实际上真正赋值的地方是在render阶段.
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount // 组件挂载阶段
: HooksDispatcherOnUpdate; // 组件更新阶段
上面代码,当nextCurrentHook为空的时候,被赋值为HooksDispatcherOnMount,不为空的时候被赋值为HooksDispatcherOnUpdate,意思就是说,当组件第一次render,也就是挂载的时候,我们的hook api是在HooksDispatcherOnMount这个对象上的,非首次渲染是在HooksDispatcherOnUpdate对象上的。
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
...
useState: mountState,
...
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
...
useState: updateState,
...
};
所以我们需要分两个分支来看源码。
3.1 mountState
首先我们需要知道,在组件里,多次调用useState,或者其他hook,那react怎么知道我们当前是哪一个hook呢。其实在react内部,所有的hook api第一次被调用的时候都会先创建一个hook对象,来保存相应的hook信息。然后,这个hook对象,会被加到一个链表上,这样我们每次渲染的时候,只要从这个链表上面依次的去取hook对象,就知道了当前是哪一个hook了。
下面我们就看一下这个hook对象的具体格式。
const hook: Hook = {
memoizedState: null, // 缓存当前state的值
baseState: null, // 初始化initState,以及每次dispatch之后的newState
queue: null, // update quene
baseUpdate: null, //基于哪一个hook进行更新,循环update quene的起点
next: null, // 指向下一个hook
};
对于useState来说,memoizedState属性上保存的就是当前hook对应状态变量当前的值,也就是我们获取到的状态变量的值。那这个quene上面保存的是什么呢,我们稍后在解释。
言归正传,我们开始将mountState函数。组件首次渲染的源码,就是mountState这个函数。也就是说首次渲染时useState的源码就是mountState。
那么我们来看看它的实现。
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook(); // 第一步:创建新的hook对象并加到链上,返回workInProgressHook
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState; // 第二步:获取初始值并初始化hook对象
const queue = (hook.queue = { // 第三步:创建更新队列(update quene),并初始化
last: null, // 最后一次的update对象
dispatch: null, // 更新函数
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any), // 前面最后一次更新的state值,更新的值有可能是函数,函数计算需要用到前一个state的值
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind( // 第四步
null,
// Flow doesn't know this is non-null, but we do.
((currentlyRenderingFiber: any): Fiber), // 绑定当前fiber和quene
queue,
): any));
return [hook.memoizedState, dispatch];
}
第一步,创建hook对象,并将该hook对象加到hook链的末尾。
我们来看一下代码。
function mountWorkInProgressHook(): Hook {
const hook: Hook = { // 创建hook对象
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) { // 如果是组件内部的第一个hook
// This is the first hook in the list
firstWorkInProgressHook = workInProgressHook = hook;
} else { // 不是第一个hook对象,就直接把新创建的hook对象加到hook链表的末尾
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
第二步:初始化hook对象的状态值,也就是我们传进来的initState的值。
第三步:创建更新队列,这个队列是更新状态值的时候用的。
第四步:绑定dispatchAction函数。我们可以看到最后一行返回的就是这个函数。也就是说这个函数,其实就是我们改变状态用的函数,就相当于是setState函数。这里它先做了一个绑定当前quene和fiber对象的动作,就是为了在调用setState的时候,知道该更改的是那一个状态的值。
至此,我们就看完了mountState函数。
下面这张图,是我自己画的简易版useState源码的流程图。
那么到这里,我们已经走完了组件首次渲染调用useState时的逻辑。现在,我们已经拿到了我们的状态变量state,那么我们就可以改变这个状态了,也就是调用set函数,这里为了说明方便,我们就直接说setState函数了(实际上你可以随意取名字)。
3.2 dispatchAction
前面已经说过dispatchAction就是我们更改状态值时调用的函数。
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A, // 2
) {
...
if(){
rerender逻辑
}else{
const update: Update<S, A> = { // 第一步
expirationTime,
suspenseConfig,
action, // 2
eagerReducer: null,
eagerState: null,
next: null,
};
// 第二步:将update加到quene上,更新quene的last为当前update,注意quene是一个环形链表
const last = queue.last;
if (last === null) {
// This is the first update. Create a circular list.
update.next = update; // 环形链
} else {
const first = last.next; // 这个last.next是指向第一个update,因为quene是一个环形链表
if (first !== null) {
// Still circular.
update.next = first; // 使quene变成环形链表
}
last.next = update; // 将update加到quene上。
}
queue.last = update; // 更新quene的last为当前update
}
...
省略无关代码,我们可以看到实际上,dispatchAction这个函数主要做了两件事情。
第一件就是创建了一个update对象,这个对象上面保存了本次更新的相关信息,包括新的状态值action。
第二件,就是将所有的update对象串成了一个环形链表,保存在我们hook对象的queue属性上面。所以我们就知道了queue这个属性的意义,它是保存所有更新行为的地方。
在这里我们可以看到,我们要更改的状态值并没有真的改变,只是被缓存起来了。那么真正改变状态值的地方在哪呢?答案就是在下一次render时,函数组件里的useState又一次被调用了,这个时候才是真的更新state的时机。
3.3 updateState
这里就是我们组件更新时,调用useState时真正走的逻辑了。
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
function updateReducer<S, I, A>(
reducer: (S, A) => S, // 对于useState来说就是basicStateReducer
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook(); // 获取当前正在工作的hook,Q1
const queue = hook.queue; // 更新队列
// The last update in the entire queue
const last = queue.last; // 最后一次的update对象
// The last update that is part of the base state.
const baseUpdate = hook.baseUpdate; // 上一轮更新的最后一次更新对象
const baseState = hook.baseState; // 上一次的action,现在是初始值
// Find the first unprocessed update.
let first;
if (baseUpdate !== null) {
if (last !== null) {
// For the first update, the queue is a circular linked list where
// `queue.last.next = queue.first`. Once the first update commits, and
// the `baseUpdate` is no longer empty, we can unravel the list.
last.next = null; // 因为quene是一个环形链表,所以这里要置空
}
first = baseUpdate.next; // 第一次是用的last.next作为第一个需要更新的update,第二次之后就是基于上一次的baseUpdate来开始了(baseUpdate就是上一次的最后一个更新)
} else {
first = last !== null ? last.next : null; // last.next是第一个update
}
if (first !== null) { // 没有更新,则不需要执行,直接返回
let newState = baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
do { // 循环链表,执行每一次更新
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
...
} else { // 正常逻辑
// This update does have sufficient priority.
// Process this update.
if (update.eagerReducer === reducer) { // 如果是useState,他的reducer就是basicStateReducer
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
if (!didSkip) { // 不跳过,就更新baseUpdate和baseState
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
...
hook.memoizedState = newState; // 更新hook对象
hook.baseUpdate = newBaseUpdate;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
updateState做的事情,实际上就是拿到更新队列,循环队列,并根据每一个update对象对当前hook进行状态更新。最后返回最终的结果。
这是我在学习useState源码时的自问自答
1、怎么循环hook对象的,在哪里操作的
(1)从当前fiber对象的memoizedState属性保存着当前组件的第一个hook对象
(2)在每次执行updateState的时候,首先需要获取当前工作中的hook,就是在这里循环的hook
(3)hook链是一个环形链吗?不是,是单向链表
在mount阶段,workInProgressHook.next = null,update阶段最后一个hook的next依然是null
是不是说当前fiber对象的memoizedState一直都是第一个hook (462行)
2.Q:更新函数绑定当前hook的地方在哪
A:在dispatchAcion.bind的地方,绑定了fiber和quene
3.Q:更新state时,怎么定位到第一个需要执行的update的
A:基于baseUpdate来开始更新
4.Q:renderWithHooks为什么第一次没有执行 FunctionComponent这个分支?
A:renderWithHooks是在组件更新阶段执行的FunctionComponent
5.Q:useState可以放对象吗?
A:可以,但是如果setState里的对象还是同一个就不会触发重新渲染
第一次正式的写技术文章,作文水平有限,希望可以帮到大家。
参考
[掘金]» useState源码解析