React Hooks

这两天在公司写需求的时候,有一个项目react版本在 16.8,所以想顺便学习一下 react hooks

react-hooks是 react 加在 函数式组件里的一些增强方法,简单说来,就是让 函数式组件,也具备了类组件维持自己内部状态的能力

为什么要设计 hooks

而为什么需要用hook来代替类组件的这些功能呢,按照 Dan 的说法,设计 hooks 主要是为了解决 classComponent 的以下几个问题:

  • 很难复用逻辑,只能用 HOC,或者 renderProps,导致组件层级嵌套很深
  • 会产生巨大的组件(指很多方法会被写到类里)
  • 类组件不好理解,比如方法需要bind,this指向不明确

HOC嵌套确实是一个问题,如果是多层嵌套的话,组件层级嵌套深,也会不好理解

然后还有在componentDidMount和componentDidUpdate中订阅内容,还需要在componentWillUnmount中取消订阅的代码,里面会存在很多重复性工作。最重要的是,在一个ClassComponent中的生命周期方法中的代码,是很难在其他组件中复用的,这就导致了代码复用率低的问题。

而我们都很清楚,react 中原有的 函数式组件,只能根据prop进行render,是没有办法保存自己的状态的,所以,hooks的出现,就是为了让我们在避免 class component 冗杂的代码的同时,还可以在 function component 里维护并使用自己的状态

那么,react 的hooks 是怎么在函数式组件内部存储自己的状态的呢

hooks实现原理

react 16 提出了 Fiber 架构的概念,通过把之前无法被打断的循环diff过程拆分成一个一个的小任务,以划分的时间片为执行时间单位,在这个时间片单位里,要执行以下的几个任务

这些任务执行完毕之后,如果还有空余时间,就会执行 diff 过程划分的小任务,而这些小任务,就是以 fiber node 为基本单位的

fiber node 基本结构如下:

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
{
...
// 浏览器环境下指 DOM 节点
stateNode: any,

// 形成列表结构
return: Fiber | null,
child: Fiber | null,
sibling: Fiber | null,

// 更新相关
pendingProps: any, // 新的 props
memoizedProps: any, // 旧的 props
// 存储 setState 中的第一个参数
updateQueue: UpdateQueue<any> | null,
memoizedState: any, // 旧的 state

// 调度相关
expirationTime: ExpirationTime, // 任务过期时间

// 大部分情况下每个 fiber 都有一个替身 fiber
// 在更新过程中,所有的操作都会在替身上完成,当渲染完成后,
// 替身会代替本身
alternate: Fiber | null,

// 先简单认为是更新 DOM 相关的内容
effectTag: SideEffectTag, // 指这个节点需要进行的 DOM 操作
// 以下三个属性也会形成一个链表
nextEffect: Fiber | null, // 下一个需要进行 DOM 操作的节点
firstEffect: Fiber | null, // 第一个需要进行 DOM 操作的节点
lastEffect: Fiber | null, // 最后一个需要进行 DOM 操作的节点,同时也可用于恢复任务
....
}

这些fiber node,就是通过 return、child、和 sibling 串成一个链表结构的。

这些属性中,hook里需要特别关注的,就是 memoizedState

在普通的class component中,这里的memoizedState就是一个存储在上次渲染过程中最终获得的节点的 state,每次执行render方法之前,React会计算出当前组件最新的state然后赋值给class的实例,再调用render

而在 function component 中其实也是这个字段来存储 组件状态的,但是它的机制又有所不同,hook 组件的 memoizedState,不是一个直接代表state整体的对象,而是一个特殊的数据结构,我们称其为一个 hook 对象

1
2
3
4
5
6
7
{
baseState,
next,
baseUpdate,
queue,
memoizedState
}

重点关注 memoizedState、 next 和 queue

为什么要用这种数据格式呢?主要是因为 function component 中,React 不知道我们调用了几次 useState,所以就索性使用这种类似链表的结构, memoizedState 代表 这次useState 设置的state,next指向下一个 useState代表的hook对象,这是一个顺序结构

在首次 render 的时候,function component 就会构建一个这样的 Fiber hook 链表

在之后的更新过程中,每次setState都会派发一个update 对象

1
2
3
4
5
6
var update = {
expirationTime: _expirationTime, // 过期时间(根据优先级计算)
action: action, // setState 更新的值
callback: callback !== undefined ? callback : null,
next: null
}

这其中的 action 就是我们调用 setState 传入的值,react 把这个update 对象加到 hook 对象的queue中,在之后 batchUpdate 的过程中,从 Hook 对象的queue中拿出所有的 update依次执行,更新 fiber node 中的 memoizedState,从而达到 存储内部state 的目的

为什么React Hook 必须在函数组件的最外层调用:

其实看了上面 hook 的内部实现,我们应该也可以理解,Hook 对象这种类似链表的存储方式,对hook次序的要求是很高的,如果我们某一次 的hook在 条件判断里,那么我们并不能保证这次useState是一定会执行的,假设 第一次render 的时候条件判断为 true,这个 hook 被加到了我们之前说的 那个链表结构里去,而在更新的时候条件判断变成了false,这个hook没有执行,那么其前一位hook结构的 next 虽然在更新,但是用问题hook的更新方法去更新了这个问题hook的下一个state,eg:

1
2
3
4
if (something) {
const [state1] = useState(1)
}
const [state2] = useState(2)

本来的对应关系是 hook1 -> state1, hook2 -> state2,结果某次render 导致 state1 没有执行,那么链表结构里就会拿 state1的更新方法去更新 state2 的对应数据,这一逻辑显然是错误的,所以才会有这样的限制

更简单的理解模型:

React hook: not magic, just arrays

参考

react源码解读

阅读源码后,来讲讲React Hooks是怎么实现的

React hook: not magic, just arrays