Que's Blog

WebFrontEnd Development

0%

react-17.0.2-hooks 之 useState

全局状况

这一篇其实是react v17.0.2整体结构分析的组成部分。太长就把它抽出来了。
以下实际上都是ReactHooks的组成部分。因为同属hooks范畴就单独归纳一下。

1
2
3
4
5
6
7
8
9
10
11
useCallback,
useContext,
useEffect,
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useMemo,
useMutableSource,
useReducer,
useRef,
useState,

关于这些useXXX系列的函数都在ReactHook.js里面。他们的结构几乎一模一样:

1
2
3
4
5
6
7
8
9
10
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
// useCallback => dispatcher.useCallback(arguments);
// useContext => dispatcher.useContext(arguments);
// useEffect => dispatcher.useEffect(arguments);
// ...

这里dispatcher指向ReactCurrentDispatcher.current

关于这个变量,大致可以认为它是一个指针,会在运行过程中不断变化其指向。在当前分析的版本中,它的定义在ReactFiberHooks.new.js文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// ReactFiberHooks.new.js#line399
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

// ReactFiberHooks.old.js#line1776
// ReactFiberHooks.new.js#line1817
// 这两个文件里面这部分定义是一致的
const HooksDispatcherOnMount: Dispatcher = {
readContext,

useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useOpaqueIdentifier: mountOpaqueIdentifier,

unstable_isNewReconciler: enableNewReconciler,
};

const HooksDispatcherOnUpdate: Dispatcher = {
readContext,

useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useOpaqueIdentifier: updateOpaqueIdentifier,

unstable_isNewReconciler: enableNewReconciler,
};

这里重提一下重点,本篇的重点是React核心的数据结构

useState的细节

我们看一看常见的useState的定义(mountState&&updateState):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 细节忽略
return [hook.memoizedState, dispatch];
}

function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
function rerenderReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 细节忽略
return [newState, dispatch];
}

所以究根结底来说,我们常见const [text, setText] = useState('hello')返回的就是一个state和一个dispatch。

关于这个hooks机制吧,因为在函数式组件里面(每次render必然会重新执行这个render,正常来讲变量赋值会被初始化),所以个人盲猜就是用闭包原理做了一个仅仅对当前函数式组件开放访问的闭包作用域。后面爬源码来验证这个猜想吧。这里目标是mountState函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}

这里hooks赋值的结构是:

1
2
3
4
5
6
7
8
9
10
11
12
const hook: Hook = {
memoizedState: null, // 保存最新的值
baseState: null, // 初始值
baseQueue: null,
queue: {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
},
next: null,
};

这很显然是一个链表的结构,第一次执行之后会返回这样的hooks结构,后续执行的时候就会把相应的hooks数据结构放到这个next属性上并返回,最终所有的hooks组成一个链表。然后它被放在当前渲染的currentlyRenderingFiber节点上(dispatchAction函数内容)。

另外可以看到返回dispatch的核心路径了: dispatchAction.bind()。这个函数如果涉及到fiber要分析还是挺麻烦的,考虑到这里fiber为null,所以我把相关代码删了,贼清晰现在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const eventTime = requestEventTime();
const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};

// Append the update to the end of the list.
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;
}

这里有几个注意点:

  1. bind函数的返回。bind函数是将函数绑定了上下文之后返回。所以dispatch是一个函数。这个函数就是指定了this指向的dispatchAction
  2. dispatch这个定义的时候传入了一些参数,核心就是这个hook.queue。
  3. 闭包的实现:通过bind函数,成功的把状态值存入了dispatchAction函数的闭包里面。

当我们mount渲染完毕之后,dispatch也就被定义完毕,在这个定义过程中,巧妙的利用了bind这个函数将相关的一些值存到了一个基于dispatchAction函数构造出来的新函数里面。这个过程中因为这个闭包的存在,我们的无状态组件在更新的时候,将不会丢失内部状态。

这里我们继续对这个这个更新的流程分析一下,看看后续更新过程之中,它是如何在update流程里面,拿到旧的值,并如何在更新过程中使用setXXX(new)获取新的值,并触发视图更新。

这里主要是两个要点,第一个是视图更新时候如何拿到缓存值,第二个是setXXX(newValue)过程中更新值和View。

这里讨巧一点进行探究,如果先分析第一个那么后续的要重新分析,但是如果直接分析第二个,一方面即可以继续上面的分析思路,另一方面它本身也会触发更新和缓存值读取,这样两个分析就组成了一条线。

所以我们直接看看setXXX(newValue)过程发生了什么。

这里往上进行回顾,首先是setXXX这个函数,它的值的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// useState初始最终导出:[hook.memoizedState, dispatch]
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
// dispatchAction的函数参数定义:
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {};
// 那么此时, 通过dispatchAction.bind这个调用:
setXXX(newValue) === dispatchAction(currentlyRenderingFiber, queue, newValue)

此时我们就可以带着疑问去分析,当执行setXXX(newValue)时候,发生了什么。
此时,我们需要对dispatchAction函数做一些比较细致的分析,之前dispatchAction函数我们把Fiber节点相关东西删掉了方便理解简单的意图,但是这里需要则需要对Fiber处理部分进行细致分析(为了尽量简洁还是删除了dev部分代码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber);

const update: Update<S, A> = {
lane,
action, // <- 这里保存的是setXXX(newValue)里面的newValue值
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// 提到的queue在前文中的定义
// const queue = (hook.queue = {
// pending: null,
// dispatch: dispatchAction.bind(
// null,
// currentlyRenderingFiber,
// queue,
// ): any),
// lastRenderedReducer: basicStateReducer,
// lastRenderedState: (initialState: any),
// });
// Append the update to the end of the list.
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;

const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
// 这是一个渲染阶段的更新, 将其保存在一个延迟创建的队列(更新链表)映射中。
// 在这个更新结束之后,我们将会重新触发顶部的work-in-progress,将保存起来的更新进行应用。
// 这里做一个标记 并在render阶段触发更新 这种对应的是已经有多个update在fiber上的情况
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
} else {
// 主要来说,这里是一个条件赋值行为(针对update对象)
// 这里有个要点在于,当前update队列其实还是空的 所以可以直接计算下一步的state
// 如果: 里面的值和当前一致就直接return退出不走下面的scheduleWork逻辑。
// 反之, 则执行scheduleUpdateOnFiber进行更新

// 以下是16.8版本时候的if判断 17.0.2有更新 但是贴出来感觉也有些参考意义
// currentlyRenderingFiber => workInProgress
// if (
// fiber === currentlyRenderingFiber ||
// (alternate !== null && alternate === currentlyRenderingFiber)
// ) {
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
// fiber.lanes => fiber节点优先级
// fiber.lanes === NoLanes => fiber节点上没有更新需要执行
// 当fiber上不存在别的update,就不需要经过多个update进行合并计算state
// 那么当前的update就是唯一的计算过程,即可以立刻算出结果
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally { }
}
}
// 16.8版本的调用 scheduleWork(fiber, expirationTime);
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}

这里的逻辑在注释里面其实比较清楚了。为了方便阅读部分关键地方我都做了中文注释,并加了一点点个人理解。

这里的逻辑分支大致是:

  • 如果是第一次update更新过程,那么立刻计算出结果,并调用scheduleUpdateOnFiber(scheduleWork)更新

  • 如果之前已经更新过,再次更新,那么此时queue.pending保存的就是上一次缓存的 update 对象(代码块 line36),此时: update.next = pending.next; pending.next = update;queue.pending = update;这三条代码,就比较有趣,它完成了一个环形链表(当然其实首次更新过程也有一个环形链表):

    • 先将 update 下个节点设为pending的下个节点
    • 然后pending下个节点设为 update。通过这两步骤就讲 update 塞进了环形列表
    • 最后将queue.pending指向 update。
    • 完毕后做一个需要批量更新的标记

篇外 || 总结

分析过上文这些数据逻辑变动之后,大致就明白这个这个 useState其实实质上还是对dispatchAction的细节分析。

这里queue的维护是这里核心逻辑。这里主要是维护的pending(当前 update 节点指针),以及update节点所在的环形链表。

至于后续的更新主要是通过标记更新,或者手动执行scheduleUpdateOnFiber|scheduleWork函数来实现的。

关于这个函数,在16.8版本下的scheduleWork函数的分析,其实前面也分析, 具体可以参考看看《react-v16-Update renderPhase篇》

这个函数是在上一篇中是setState函数的下级调用,在这里直接调用它实质上和setState触发更新原理也差不多。
这一篇就不再赘述。

但是新版本这里更新了调用方式scheduleUpdateOnFiber, 但是呢,查看了一下相关函数里面的原有的scheduleWork调用基本都直接替换为scheduleUpdateOnFiber,那么很显然这里scheduleUpdateOnFiber基本等同scheduleWork,只是加入了一个lane优先级参数。

而优先级的整理,则又是一个故事了。