本篇文章参考以下博文
最近在和同事们交流的时候,发现大家对 hook 的理解是,这玩意就是生命周期,原来放在生命周期里的东西,现在都要放到 hook 里。
如果刚接触 hook 的话,那么这个解释可以帮助你快速明白 hook 的用法,但是往深了说, hook 与生命周期还是有一定区别的。今天咱们就来研究研究,区别在哪?
阅读本节需要对 react 的 fiber 结构有一定的了解。
我们举一个形象一点的例子,让大家先有一个概念。比如下图:
react 的图标大家都很熟悉了哈,这个图标的含义是原子,中间的是原子核,外面的轨迹是飞行的核外电子。那么 hook 的概念,就是像是电子。
接下来我们自己实现一个最常用的 hook useState 。
首先过程很简单,大家先别害怕。看下面一个例子
function App() {
const [num, updateNum] = useState(0);
return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;
}
上面是一个简单的函数组件,一个按钮,点击之后会对 num 进行累加。那么这个组件中,涉及 useState 的工作有两部分:
上面第一步的过程,又可以分解为:
调用 ReactDOM.render 会产生 mount 的更新,更新内容为 useState 的 initialValue (即0)。
点击 p 标签触发 updateNum 会产生一次 update 的更新,更新内容为 num => num + 1 。
接下来我们研究下,这两步是怎么实现的。
页面产生 更新 后,组件 render ,那么这个 更新 是什么?源码里这个更新是一个链表的节点,
const update = {
// 更新执行的函数(num => num + 1)
action,
// 与同一个Hook的其他更新形成链表
next: null
}
这些更新节点被放在了一个环装单向链表里,类似如下结构:
调用 updateNum 的过程,执行 dispatchAction 函数。
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();
}
上面逻辑的效果是 queue.pending 始终指向最后一个插入的 update 。
这样做的好处是,当我们要遍历 update 时, queue.pending.next 指向第一个插入的 update 。
知道了 update 的逻辑之后,我们来看看状态是怎么保存在 queue 里面的。我们知道 ClassComponent 的状态是保存在属性 state 里面的。而 FunctionComponent 的状态是保存在 fiber 里面的,类似下面:
// App组件对应的fiber对象
const fiber = {
// 保存该FunctionComponent对应的Hooks链表
memoizedState: null,
// 指向App函数
stateNode: App
};
接下来我们关注 fiber.memoizedState 中保存的 Hook 的数据结构。
hook = {
// 保存update的queue,即上文介绍的queue
queue: {
pending: null
},
// 保存hook对应的state
memoizedState: initialState,
// 与下一个Hook连接形成单向无环链表
next: null
}
可以看到, Hook 与 update 类似,都通过链表连接。不过 Hook 是无环的单向链表。
注意区分 update 与 hook 的所属关系:
每个 useState 对应一个 hook 对象。
调用 const [num, updateNum] = useState(0) ;时 updateNum (即上文介绍的 dispatchAction )产生的 update 保存在 useState 对应的 hook.queue 中。
在上文 dispatchAction 末尾我们通过 schedule 方法模拟 React 调度更新流程。
function dispatchAction(queue, action) {
// ...创建update
// ...环状单向链表操作
// 模拟React开始调度更新
schedule();
}
现在我们来实现他。
我们用 isMount 变量指代是 mount 还是 update 。(源码中对于 mount 和 update 的处理是分开的,这里用一个 isMount 简化了)
// 首次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 对象。
到这里第一步就完成了,接下来我们进行第二步。即组件 render 时 useState 返回的 num 为更新后的结果。
组件 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;
完整的过程如下:
function useState(initialState) {
let hook;
if (isMount) {
hook = {
queue: {
pending: null
},
memoizedState: initialState,
next: null
}
if (!fiber.memoizedState) {
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
} else {
hook = workInProgressHook;
workInProgressHook = workInProgressHook.next;
}
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending)
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
到此为止,我们完整的实现了一个 hook ,它与源码中的 hook 还是有细微差异的,但是原理相同。
最后点以下题,看到现在大家应该感觉出来 hook 和生命周期还是很不一样的吧,那各位觉得 hook 到底是个啥?
我个人感觉更像是一个工具,这个工具会帮我们把数据更新到页面上,不同的 hook 更新效果不太相同,有的会同步更新,有的则会延迟更新,我们要做的则是在不同的业务需要下,使用这些工具,至于工具到底是怎么做到的,而且还做的这么好,那就需要我们反复的研究源码,才能熟知。
因篇幅问题不能全部显示,请点此查看更多更全内容