React@16.8.6原理浅析(hooks)

2,400 阅读10分钟

本系列文章总共三篇:

课前小问题

  1. hooks 是如何存储状态的
  2. 有多个相同的 hooks 时 react 是如何区分的

定义

React hooks api 是在 react 这个库里面定义的,我们以 useState 为例:

export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

我们可以发现 hooks 的定义非常简单,只是获取了 dispatch 然后调用 dispatcher 对应的 useState 属性,其它 hooks 也是类似,比如 useEffect 是调用 dispatcher 的 useEffect 属性。

接着我们就需要看看 dispatcher 到底是什么,通过查看 resolveDispatcher 我们发现 dispatcher 指向的是 ReactCurrentDispatcher.current

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  invariant(
    dispatcher !== null,
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
    ' one of the following reasons:\n' +
    '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
    '2. You might be breaking the Rules of Hooks\n' +
    '3. You might have more than one copy of React in the same app\n' +
    'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
  );
  return dispatcher;
}

通过全局搜索我们发现 ReactCurrentDispatcher.current 在 ReactFiberHooks.js 这个文件中被赋值,接下来我们就来看看这个文件。

renderWithHooks

前言

经过搜索我们发现 ReactCurrentDispatcher.current在 ReactFiberHooks.js 文件中被频繁赋值,其中最主要被赋值的地方就在 renderWithHooks 方法中,经过搜索我发现 renderWithHooks 在 ReactFiberBeginWork.js 这个文件中被多次调用,如果你之前看过上一篇文档或是对 react 的更新流程的源码比较熟悉的话,你应该知道 ReactFiberBeginWork.js 文件对应着 beginWork 这个方法,在这个方法中会找出要更新的 fiber 对象并执行对应的更新方法。
经过搜索我找到了和 function component 相关的几个方法:updateFunctionComponent 和 mountIndeterminateComponent,这两个都是更新 function component,区别是第一次渲染的时候会调用 mountIndeterminateComponent,因为第一次还无法确定是 function component 还是 class component。

mountIndeterminateComponent:

image.png

updateFunctionComponent:

image.png

接下来我们就来看看 renderWithHooks 到底做了什么。

流程图

在线地址:www.processon.com/view/link/5…

具体逻辑

通过上面的流程图,我们发现 renderWithHooks 做了如下几件事:

  1. 通过判断 nextCurrentHook 是否为 null 来判断是否是初次渲染,如果是初次渲染就将 ReactCurrentDispatcher.current 赋值为 HooksDispatcherOnMount 否则赋值为 HooksDispatcherOnUpdate
  2. 然后调用 function component 得到 children
  3. 判断是否存在嵌套更新(didScheduleRenderPhaseUpdate),如果存在就继续执行第二步,直到嵌套更新结束或是超过最大嵌套更新层数
  4. 设置当前 fiber 对象上的 memoizedState 为当前的 hook 对象,以及设置其它属性,并将 effectTag 标记为 sideEffectTag
  5. 重置全局变量
  6. 返回 children

HooksDispatcherOnMount

简介

HooksDispatcherOnMount 对象中定义了各个 hooks api 在初次渲染中的实现

image.png

流程图

在线地址:www.processon.com/view/link/5…

HooksDispatcherOnUpdate

简介

HooksDispatcherOnUpdate 对象中定义了各个 hooks api 在再次渲染中的实现

image.png

流程图

在线地址:www.processon.com/view/link/5…

useState

经过前面的讲述此时你应该知道 useState 最终调用的是 ReactCurrentDispatcher.current.useState 而 ReactCurrentDispatcher.current 又是在 renderWithHooks 中被赋值为 HooksDispatcherOnMount 或 HooksDispatcherOnUpdate,那我们先来看一下 HooksDispatcherOnMount 中的实现。

mountState

  1. 首先调用 mountWorkInProgressHook 方法创建 hook 对象
  2. 判断传入的 initialState 也就是 useState 传入的参数是否是函数,如果是就执行它得到初始 state
  3. 设置 hook.memoizedState 和 hook.baseState 为 initialState,这里你就可以知道为什么 function component 使用了 hook 之后就可以保存状态了,因为状态保存在 hook 对象上了,而 hook 对象又保存在 fiber 对象的 memoizedState 属性上
  4. 创建 queue 对象并赋值给 hook.queue,queue 类似于 fiber 对象上面的 updateQueue
  5. 为将当前 fiber(workInProgress)和 queue 绑定为 dispatchAction 的前两个参数,并赋值给 dispatch
  6. 返回 [hook.memoizedState, dispatch]

updateState

updateState 内部调用了 updateReducer,updateRecucer 内部做了以下事情:

  1. 首先调用 mountWorkInProgressHook 方法创建 hook 对象
  2. 赋值 queue.lastRenderedReducer 为 basicStateReducer
  3. 如果出现重复渲染(即在一次渲染中又调用了一次渲染),我们去 renderPhaseUpdates 中根据 queue 获取 update 然后遍历执行 update 链表获取 newState,然后判断 newState 和 oldState 是否相等,如果不相等就标记更新,最后返回 [newState, dispatch]
  4. 如果没有出现重复渲染就从 queue 找到最后一个 update,进而找到第一个 udpate,因为是循环链表所以可以通过 last.next 找到 first,然后和第四步一样循环执行 update 链表获取 newState,然后判断 newState 和 oldState 是否相等,如果不相等就标记更新,最后返回 [newState, dispatch]

dispatchAction

dispatchAction 就是 useState 返回的第二个参数

流程图

在线地址:www.processon.com/view/link/5…

具体逻辑

  1. 首先判断一下是否是处于一个渲染阶段的更新,如果是将 didScheduleRenderPhaseUpdate 设置为 true,这个标志位在 renderWithHooks 中被用于判断是否处于嵌套更新,接着创建一个 update 对象,再创建一个 renderPhaseUpdates Map 对象,并以 queue 为 key update 为 value 存储到 renderPhaseUpdate 中,renderPhaseUpdate 在 updateState 方法中会调用
  2. 如果不是处于一个渲染阶段的更新,则先计算出 expirationTime 然后创建一个 update 对象,接着将 update 放到 queue.last 这个循环链表中,接着判断一下如果当前 fiber.expirationTime = NoWork,并且 queue.lastRenderedReducer 不为空,我们就可以通过 lastRenderedReducer 计算出新的 state(eagerState),lastRenderedReducer 接受之前的 state(currentState)和 action(就是传入 useState 返回的第二个方法的参数),接着将 lastRenderedReducer 和 eagerState 赋值给 update 的 eagerReducer 和 eagerState,接着判断新的 state (eagerState)和老的 state(currentState)是否相等,如果相等就直接 return 因为没有更新产生,如果不相等那就调用 scheduleWork 进入调度阶段,这个就和上一篇讲的流程连接起来了。

本章解决的问题

  1. hooks 是如何存储状态的

UseEffect

经过前面的讲述此时你应该知道 useEffect 和 useState 一样,最终调用的是 ReactCurrentDispatcher.current.useEffect 而 ReactCurrentDispatcher.current 又是在 renderWithHooks 中被赋值为 HooksDispatcherOnMount 或 HooksDispatcherOnUpdate,那我们先来看一下 HooksDispatcherOnMount 中的实现。


mountEffect

HooksDispatcherOnMount 中 useEffect 指向的是 mountEffect,它又调用了 mountEffectImpl

function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    deps,
  );
}

mountEffectImpl

mountEffectImpl 做了以下事情:

  1. 通过 mountWorkInProgressHook 创建一个 hook 对象
  2. 将传入的 fiberEffectTag 设置到 sideEffectTag 上,对应到 mountEffect 就是 UpdateEffect | PassiveEffect,最终 sideEffectTag 会被设置到当前 fiber 对象的 effectTag 上(参见 renderWithHooks
  3. 最后调用 pushEffect,传入 hookEffectTag(UnmountPassive | MountPassive),create,nextDeps
  4. 将 pushEffect 的结果赋值给 hook.memoizedState

updateEffect

在更新阶段会将 dispatcher 指向 HooksDispatcherOnUpdate,在 HooksDispatcherOnUpdate 中 useEffect 指向的是 updateEffect,它又调用了 updateEffectImpl。

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    deps,
  );
}

updateEffectImpl

updateEffectImpl 做了以下事情:

  1. 通过 updateWorkInProgressHook 创建一个 hook 对象
  2. 判断 currentHook 是否为 null,currentHook 不为 null 说明不是初次渲染,获取 currentHook.memoizedState,也就是上一个 effect 对象,找到该对象的 destory 属性和 deps 属性,判断新的 deps 和老的 deps 是否相等,如果相等就调用 pushEffect 传入 NoHookEffect,表示没有 effect 需要执行,也就不会在 commit 阶段执行 unmount 和 mount,也就是调用 destroy 和 create 方法,然后 return
  3. 如果 currentHook 等于 null 或是新的 deps 和老的 deps 不相等,将传入的 fiberEffectTag 设置到 sideEffectTag 上(UpdateEffect | PassiveEffect),最终 sideEffectTag 会被设置到当前 fiber 对象的 effectTag 上(参见 renderWithHooks),最后调用 pushEffect,传入 hookEffectTag(UnmountPassive | MountPassive),create,nextDeps,将 pushEffect 的结果赋值给 hook.memoizedState

pushEffect

  1. 创建一个 effect 对象
  2. 将 effect 添加到 componentUpdateQueue.lastEffect 上,形成一个循环链表,componentUpdateQueue 会被添加到当前 fiber 对象的 updateQueue 上(参见 renderWithHooks
  3. 返回 effect

effect

const effect: Effect = {
    tag, // hookEffectTag
    create, // useEffect 接收的第一个参数
    destroy, // 在 mountEffect 中是 undefined
    deps, // useEffect 接收的第二个参数
    // Circular
    next: (null: any), // 指向下一个 effect
  };

commitLayoutEffects

最终生成的 updateQueue 会在 commit 阶段的 commitLayoutEffects 中执行
详情可以看上一篇

commitLayoutEffectOnFiber(commitLifeCycles)

还记得上面 mountEffectImpl 方法会将 UpdateEffect | PassiveEffect 设置到 fiber.effectTag 上,对于有 UpdateEffect 的 fiber 对象在 commitLayoutEffects 中会执行 commitLayoutEffectOnFiber 方法,它对应的就是  commitLifeCycles 方法,在该方法中对于 FunctionComponent 会执行 commitHookEffectList方法,传入 UnmountLayout, MountLayout, finishedWork

commitHookEffectList

在该方法中会对传入的 finishedWork.updateQueue 上面的 effect 对象执行 unmount 和 mount,也就是调用 effect 对象上的 destroy 方法和 create 方法,对应于 useEffect 返回的方法和传入的方法,第一次渲染设置的 destroy 为 undefined 所以第一次渲染 destroy 不会执行

useRef

useRef 和其它 hooks 一样最终调用的是 ReactCurrentDispatcher.current.useRef 而 ReactCurrentDispatcher.current 又是在 renderWithHooks 中被赋值为 HooksDispatcherOnMount 或 HooksDispatcherOnUpdate,那我们先来看一下 HooksDispatcherOnMount 中的实现。


mountRef

在 HooksDispatcherOnMount 中 useRef 指向的是 mountRef 方法,我们来看一下它做了什么:

  1. 通过 mountWorkInProgressHook 方法创建了 hook 对象
  2. 创建 ref 对象 const ref = { current: initialValue }; 初始值就是传入 useRef 的第一个参数
  3. 设置 hook.memoizedState = ref;
  4. 返回 ref

updateRef

在 HooksDispatcherOnUpdate 中 useRef 指向的是 updateRef 方法,我们来看一下它做了什么:

  1. 通过 updateWorkInProgressHook 获取到 hook
  2. 返回 hook 对象上的 memoizedState

创建 hook

经过上面的几个 hook api 的实现我们发现每个 hook api 都需要先创建一个 hook 对象,而创建 hook 对象针对初次渲染和再次渲染这两个阶段调用的方法有所不同,我们先来看初次渲染。

mountWorkInProgressHook

初次渲染调用的是 mountWorkInProgressHook 方法,我们来看一下它做了什么:

  1. 创建一个 hook 对象
  2. 判断 workInProgressHook 是否为空,如果为空就将 workInProgressHook 和 firstWorkInProgressHook 指向新的 hook
  3. 如果不为空就插入其后(next),然后将 workInProgress 指向新的 hook
  4. 返回 workInProgress

updateWorkInProgressHook

接下来我们看看再次渲染时调用的 updateWorkInProgressHook 方法:

  1. 首先判断一下 nextWorkInProgressHook 是否为空,如果不为空说明当前处于渲染阶段触发的重新渲染,因为只有在重新渲染时 renderWithHooks 才会将其设置为 firstWorkInProgressHook,如果为空就将 workInProgressHook 设置为 nextWorkInProgressHook,然后将 nextWorkInProgressHook 设置为 workInProgressHook.next,然后设置 nextCurrentHook
  2. 如果 nextWorkInProgressHook 为空,我们将 currentHook 设置为 nextCurrentHook,也就是找到上一次渲染的 hook 对象(类似于 fiber里面的 current),然后根据 currentHook 复制一个 newHook,执行 mountWorkInProgressHook 中的第二三步,然后将 nextCurrentHook 指向 currentHook 的 next,这里我们就可以知道为什么多个 hook api 执行的时候 react 是如何一一对应的了,就是通过初次渲染形成的链表去对应的,所以千万要注意前后两次的渲染中 hook 的顺序不能有改变
  3. 返回 workInProgress

hook

我们来看看 hook 对象到底是个什么东西

const hook: Hook = {
    memoizedState: null,

    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };

memoizedState

存储 hook 对象的数据,useState 对应的就是 state,useEffect 对应的就是 effect 对象,useRef 对应的就是 ref 对象

baseState

和 useState 相关,在初次渲染时等于传入的初始 state,后续是每次计算出的新的 state

queue

类似于 fiber 对象的 updateQueue,每次调用 useState 返回的 setSomeState 方法就会创建一个 update 对象放到 queue 中,然后在 render 阶段再遍历 queue 计算出新的 state

const queue = (hook.queue = {
    last: null, // 指向最后一个 update,它的 next 指向第一个 update,这是一个循环链表
    dispatch: null, // dispatch 方法,用于计算出新的 state
    lastRenderedReducer: reducer, // 最后一个 update 的 reducer
    lastRenderedState: (initialState: any), // 指向最后一个 update 产生的 state
  });

本节解决的问题

  1. 有多个相同的 hooks 时 react 是如何区分的

Github

包含带注释的源码、demos和流程图
github.com/kwzm/learn-…