hooks

37 阅读7分钟

理念

我们知道,React的架构遵循schedule - render - commit的运行流程,这个流程是React世界最底层的运行规律。 ClassComponent作为React世界的原子,他的生命周期(componentWillXXX/componentDidXXX)是为了介入React的运行流程而实现的更上层抽象,这么做是为了方便框架使用者更容易上手。 相比于ClassComponent的更上层抽象,Hooks则更贴近React内部运行的各种概念(state | context | life-cycle)。 作为使用React技术栈的开发者,当我们初次学习Hooks时,不管是官方文档还是身边有经验的同事,总会拿ClassComponent的生命周期来类比Hooks API的执行时机。 这固然是很好的上手方式,但是当我们熟练运用Hooks时,就会发现,这两者的概念有很多割裂感,并不是同一抽象层次可以互相替代的概念。 比如:替代componentWillReceiveProps的Hooks是什么呢? 可能有些同学会回答,是useEffect,但是componentWillReceiveProps是在render阶段执行,而useEffect是在commit阶段完成渲染后异步执行。 所以,从源码运行规律的角度看待Hooks,可能是更好的角度。这也是为什么上文说Hooks是React世界的电子而不是原子的原因。 Concurrent Mode是React未来的发展方向,而Hooks是能够最大限度发挥Concurrent Mode潜力的Component构建方式。

工作原理

对于useState Hook,考虑如下例子:

function App() {
  const [num, updateNum] = useState(0);

  return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;
}

可以将工作分为两部分:

  1. 通过一些途径产生更新,更新会造成组件render。

  2. 组件render时useState返回的num为更新后的结果。其中步骤1的更新可以分为mount和update:

  3. 调用ReactDOM.render会产生mount的更新,更新内容为useState的initialValue(即0)。

  4. 点击p标签触发updateNum会产生一次update的更新,更新内容为num => num + 1。接下来讲解这两个步骤如何实现。

更新是什么

例子中,更新可以抽象为如下数据结构:

const update = {
  // 更新执行的函数
  action,
  // 与同一个Hook的其他更新形成链表
  next: null
}

对于App来说,点击p标签产生的update的action为num => num + 1。 如果我们改写下App的onClick:

// 之前
return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;

// 之后
return <p onClick={() => {
  updateNum(num => num + 1);
  updateNum(num => num + 1);
  updateNum(num => num + 1);
}}>{num}</p>;

那么点击p标签会产生三个update。 这些update是如何组合在一起呢? 答案是:他们会形成环状单向链表。 调用updateNum实际调用的是dispatchAction.bind(null, hook.queue),我们先来了解下这个函数:

function dispatchAction(queue, action) {
  // 创建update
  const update = {
    action,
    next: null
  }

  // 环状单向链表操作
  if (queue.pending === null) {
    update.next = update;
  } else {
    update.next = queue.pending.next;
    queue.pending.next = update;
  }
  queue.pending = update;

  // 模拟React开始调度更新
  schedule();
}

环状链表操作不太容易理解,这里我们详细讲解下。 当产生第一个update(我们叫他u0),此时queue.pending === null。 update.next = update;即u0.next = u0,他会和自己首尾相连形成单向环状链表。 然后queue.pending = update;即queue.pending = u0 当产生第二个update(我们叫他u1),update.next = queue.pending.next;,此时queue.pending.next === u0, 即u1.next = u0。 queue.pending.next = update;,即u0.next = u1。 然后queue.pending = update;即queue.pending = u1可以照着这个例子模拟插入多个update的情况,会发现queue.pending始终指向最后一个插入的update。 这样做的好处是,当我们要遍历update时,queue.pending.next指向第一个插入的update。

queue.pending = u1 ---> u0   
                ^       |
                |       |
                ---------

如何保持状态

现在我们知道,更新产生的update对象会保存在queue中。 不同于ClassComponent的实例可以存储数据,对于FunctionComponent,queue存储在哪里呢? 答案是:FunctionComponent对应的fiber中。 我们使用如下精简的fiber结构:

// App组件对应的fiber对象
const fiber = {
  // 保存该FunctionComponent对应的Hooks链表
  memoizedState: null,
  // 指向App函数
  stateNode: App
};

Hook数据结构

接下来我们关注fiber.memoizedState中保存的Hook的数据结构。 可以看到,Hook与update类似,都通过链表连接。不过Hook是无环的单向链表。

hook = {
  // 保存update的queue,即上文介绍的queue
  queue: {
    pending: null
  },
  // 保存hook对应的state
  memoizedState: initialState,
  // 与下一个Hook连接形成单向无环链表
  next: null
}

注意 注意区分update与hook的所属关系: 每个useState对应一个hook对象。 调用const [num, updateNum] = useState(0);时updateNum(即上文介绍的dispatchAction)产生的update保存在useState对应的hook.queue中。

模拟React调度更新流程

在上文dispatchAction末尾我们通过schedule方法模拟React调度更新流程

function dispatchAction(queue, action) {
  // ...创建update
  
  // ...环状单向链表操作

  // 模拟React开始调度更新
  schedule();
}

现在我们来实现他。 我们用isMount变量指代是mount还是update。

// 首次render时是mount
isMount = true;

function schedule() {
  // 更新前将workInProgressHook重置为fiber保存的第一个Hook
  workInProgressHook = fiber.memoizedState;
  // 触发组件render
  fiber.stateNode();
  // 组件首次render为mount,以后再触发的更新为update
  isMount = false;
}

通过workInProgressHook变量指向当前正在工作的hook。

workInProgressHook = fiber.memoizedState;

在组件render时,每当遇到下一个useState,我们移动workInProgressHook的指针。

workInProgressHook = workInProgressHook.next;

这样,只要每次组件render时useState的调用顺序及数量保持一致,那么始终可以通过workInProgressHook找到当前useState对应的hook对象。 到此为止,我们已经完成第一步。

  1. 通过一些途径产生更新,更新会造成组件render。接下来实现第二步。

  2. 组件render时useState返回的num为更新后的结果。## 计算state 组件render时会调用useState,他的大体逻辑如下:

function useState(initialState) {
  // 当前useState使用的hook会被赋值该该变量
  let hook;

  if (isMount) {
    // ...mount时需要生成hook对象
  } else {
    // ...update时从workInProgressHook中取出该useState对应的hook
  }

  let baseState = hook.memoizedState;
  if (hook.queue.pending) {
    // ...根据queue.pending中保存的update更新state
  }
  hook.memoizedState = baseState;

  return [baseState, dispatchAction.bind(null, hook.queue)];
}

我们首先关注如何获取hook对象:

if (isMount) {
  // mount时为该useState生成hook
  hook = {
    queue: {
      pending: null
    },
    memoizedState: initialState,
    next: null
  }

  // 将hook插入fiber.memoizedState链表末尾
  if (!fiber.memoizedState) {
    fiber.memoizedState = hook;
  } else {
    workInProgressHook.next = hook;
  }
  // 移动workInProgressHook指针
  workInProgressHook = hook;
} else {
  // update时找到对应hook
  hook = workInProgressHook;
  // 移动workInProgressHook指针
  workInProgressHook = workInProgressHook.next;
}

当找到该useState对应的hook后,如果该hook.queue.pending不为空(即存在update),则更新其state。

// update执行前的初始state
let baseState = hook.memoizedState;

if (hook.queue.pending) {
  // 获取update环状单向链表中第一个update
  let firstUpdate = hook.queue.pending.next;

  do {
    // 执行update action
    const action = firstUpdate.action;
    baseState = action(baseState);
    firstUpdate = firstUpdate.next;

    // 最后一个update执行完后跳出循环
  } while (firstUpdate !== hook.queue.pending.next)

  // 清空queue.pending
  hook.queue.pending = null;
}

// 将update action执行完后的state作为memoizedState
hook.memoizedState = baseState;

这样基本就是useState的一套概念了

hook的数据结构

在上文的极简useState实现中,使用isMount变量区分mount与update。 在真实的Hooks中,组件mount时的hook与update时的hook来源于不同的对象,这类对象在源码中被称为dispatcher。

// mount时的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  // ...省略
};

// update时的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  // ...省略
};

可见,mount时调用的hook和update时调用的hook其实是两个不同的函数。 在FunctionComponent render前,会根据FunctionComponent对应fiber的以下条件区分mount与update。

current === null || current.memoizedState === null// 这个早在之前的章节里提到过

并将不同情况对应的dispatcher赋值给全局变量ReactCurrentDispatcher的current属性。

ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;  

在FunctionComponent render时,会从ReactCurrentDispatcher.current(即当前dispatcher)中寻找需要的hook。 换言之,不同的调用栈上下文为ReactCurrentDispatcher.current赋值不同的dispatcher,则FunctionComponent render时调用的hook也是不同的函数。

一个dispatcher使用场景

当错误的书写了嵌套形式的hook,如:

useEffect(() => {
  useState(0);
})

此时ReactCurrentDispatcher.current已经指向ContextOnlyDispatcher,所以调用useState实际会调用throwInvalidHookError,直接抛出异常。

export const ContextOnlyDispatcher: Dispatcher = {
  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  // ...省略

结构

const hook: Hook = {
  memoizedState: null,
  baseState: null,
  baseQueue: null,
  queue: null,
  next: null,
};

其中除memoizedState以外字段的意义与上一章介绍的updateQueue类似

memoizedState

需要注意的是,hook与FunctionComponent fiber都存在memoizedState属性,不要混淆他们的概念。

  • fiber.memoizedState:FunctionComponent对应fiber保存的Hooks链表。

  • hook.memoizedState:Hooks链表中保存的单一hook对应的数据。不同类型hook的memoizedState保存不同类型数据,具体如下:

  • useState:对于const [state, updateState] = useState(initialState),memoizedState保存state的值

  • useReducer:对于const [state, dispatch] = useReducer(reducer, {});,memoizedState保存state的值

  • useEffect:memoizedState保存包含useEffect回调函数、依赖项等的链表数据结构effect,你可以在这里(opens new window)看到effect的创建过程。effect链表同时会保存在fiber.updateQueue中

  • useRef:对于useRef(1),memoizedState保存{current: 1}

  • useMemo:对于useMemo(callback, [depA]),memoizedState保存[callback(), depA]

  • useCallback:对于useCallback(callback, [depA]),memoizedState保存[callback, depA]。与useMemo的区别是,useCallback保存的是callback函数本身,而useMemo保存的是callback函数的执行结果有些hook是没有memoizedState的,比如:

  • useContext ​ ​ 参考&转载:React技术揭秘