React@16.8.6原理浅析(hooks)

本系列文章总共三篇:javascript

课前小问题

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

定义

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

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

咱们能够发现 hooks 的定义很是简单,只是获取了 dispatch 而后调用 dispatcher 对应的 useState 属性,其它 hooks 也是相似,好比 useEffect 是调用 dispatcher 的 useEffect 属性。react

接着咱们就须要看看 dispatcher 究竟是什么,经过查看 resolveDispatcher 咱们发现 dispatcher 指向的是 ReactCurrentDispatcher.currentgit

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 这个文件中被赋值,接下来咱们就来看看这个文件。github

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。api

mountIndeterminateComponent:
app

image.png

updateFunctionComponent:
ide

image.png

接下来咱们就来看看 renderWithHooks 到底作了什么。函数

流程图

具体逻辑

经过上面的流程图,咱们发现 renderWithHooks 作了以下几件事:post

  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

流程图

HooksDispatcherOnUpdate

简介

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

image.png

流程图

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 返回的第二个参数

流程图

具体逻辑

  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-…