Skip to main content

Hook

前置知识

闭包

在 Hook 的应用比如 dispatch 函数,也就是 useState 返回的第二个参数

闭包是指有权访问另一个函数作用域中变量或方法的函数,创建闭包的方式就是在一个函数内创建闭包函数,通过闭包函数访问这个函数的局部变量, 利用闭包可以突破作用链域的特性,将函数内部的变量和方法传递到外部。

单链表

A-->B-->C。Fiber 架构通过使用这种数据结构,实现了 render 过程的可中断性。比如更新了 B 节点,突然有高优先级插入,这个时候只需要记住当前更新到的节点是 B 即可,然后去执行高优任务,之后再从 B 节点继续更新

Fiber & Hook

Fiber 对象和 Hook 对象的关系如下图: Fiber & Hook

Fiber 架构

React 16 引入了 Fiber 架构,会基于 ReactElement 生成唯一的 Fiber 对象。在更新阶段,会基于旧的 Fiber 链表创建一个新的 Fiber 链表(如下图),可以复用旧的对象,并且获取旧 Hook 的状态

fiber双链表

两个单链表

Hook 链表

/*
Hooks are stored as a linked list on the fiber's memoizedState field.
hooks 以链表的形式存储在fiber节点的memoizedState属性上
The current hook list is the list that belongs to the current fiber.
当前的hook链表就是当前正在遍历的fiber节点上的
The work-in-progress hook list is a new list that will be added to the work-in-progress fiber.
work-in-progress hook 就是即将被添加到正在遍历fiber节点的hooks新链表
*/
let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;

无论是初次挂载还是更新,每调用一次 Hook 函数,都会产生一个 Hook 对象与之对应,Hook 对象结构如下

{
baseQueue: null, // 当前 update
baseState: 'hook1', // 初始值,即 useState 入参
memoizedState: null, // 当前状态(更新时表示上一次的状态)
queue: null, // 待执行的更新队列(queue.pending)
next: { // 下一个 hook
baseQueue: null,
baseState: null,
memoizedState: 'hook2',
next: null
queue: null
}
}

产生的 Hook 对象依次排列,形成链表存储到函数组件 Fiber.memoizedState 上。在这个过程中,有一个十分重要的指针:workInProgressHook,它通过记录当前生成(更新)的 Hook 对象,可以间接反映在组件中当前调用到哪个 Hook 函数了。每调用一次 Hook 函数,就将这个指针的指向移到该 Hook 函数产生的 Hook 对象上

比如先调用 hookA

fiber.memoizedState: hookA
^
workInProgressHook

调用 hookB

fiber.memoizedState: hookA -> hookB
^
workInProgressHook

Hook 的更新队列

这其实是个单链表环,存放某个 hook 的更新队列,如下图,是一个 useState 的更新队列:

任务待执行链表.png

其实还有一个副作用链表,跟 useEffect 有关,最终会挂到 root 节点上

issues

为什么不能在 if 语句中使用 Hook?

原因是使用了单向链表来关联某个 Fiber 节点上的所有 Hook,在更新的时候会重置链表指针,按顺序执行 Hook 的更新任务来获取新值(useState 为例),因此需要保证 Hook 的顺序。实现上其实也可以用数组模拟,但链表的好处是更灵活地插入或删除中间节点等等

自定义 Hook 是如何工作的?