- 原文地址:Under the hood of React’s hooks system
- 原文做者:Eytan Manor
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:EmilyQiRabbit
- 校对者:sunui,hanxiansen
咱们将会一块儿查看它的实现方法,由内而外地学习 React Hook。html
咱们都已经据说过了:React 16.7 的新特性,hook 系统,并已在社区中激起了热议。咱们都试用过、测试过,对它自己和它的潜力都感到很是兴奋。你必定认为 hook 如魔法般神奇,React 居然能够在不暴露实例的状况下(不须要使用 this
关键字),帮助你管理组件。那么 React 到底是怎么作到的呢?前端
那么今天,让咱们一块儿深刻探究 React Hook 的实现方法,以便更好的理解它。可是,它的各类神奇特性的不足是,一旦出现问题,调试很是困难,这是因为它的背后是由复杂的堆栈追踪(stack trace)支持的。所以,经过深刻学习 React 的新特性:hook 系统,咱们就能比较快地解决遇到的问题,甚至能够直接杜绝问题的发生。react
在开始讲解以前,我先声明我不是 React 的开发者或者维护者,因此个人理解可能也并非彻底正确。我确实很是深刻地研究过了 React 的 hook 系统的实现,可是不管如何我仍没法保证这就是 React 实际的工做方式。话虽如此,我仍是会用 React 源代码中的证据和引用来支持个人文章,使个人论点尽量坚实。android
React hook 系统概要示意图ios
咱们先来了解 hook 的运行机制,并要确保它必定在 React 的做用域内使用,由于若是 hook 不在正确的上下文中被调用,它就是毫无心义的,这一点你或许已经知道了。git
dispatcher 是一个包含了 hook 函数的共享对象。基于 ReactDOM 的渲染状态,它将会被动态的分配或者清理,而且它可以确保用户不可在 React 组件以外获取 hook(详见源码)。github
在切换到正确的 Dispatcher 以渲染根组件以前,咱们经过一个名为 enableHooks
的标志来启用/禁用 hook。在技术上来讲,这就意味着咱们能够在运行时开启或关闭 hook。React 16.6.X 版本中也有对此的实验性实现,但它实际上处于禁用状态(详见源码)json
当咱们完成渲染工做后,咱们将 dispatcher 置空并禁止用户在 ReactDOM 的渲染周期以外使用 hook。这个机制可以保证用户不会作什么蠢事(详见源码)。后端
dispatcher 在每次 hook 的调用中都会被函数 resolveDispatcher()
解析。正如我以前所说,在 React 的渲染周期以外,这些都无心义了,React 将会打印出警告信息:“hook 只能在函数组件内部调用”(详见源码)。设计模式
let currentDispatcher
const dispatcherWithoutHooks = { /* ... */ }
const dispatcherWithHooks = { /* ... */ }
function resolveDispatcher() {
if (currentDispatcher) return currentDispatcher
throw Error("Hooks can't be called")
}
function useXXX(...args) {
const dispatcher = resolveDispatcher()
return dispatcher.useXXX(...args)
}
function renderRoot() {
currentDispatcher = enableHooks ? dispatcherWithHooks : dispatcherWithoutHooks
performWork()
currentDispatcher = null
}
复制代码
dispatcher 实现方式概览。
如今咱们简单了解了 dispatcher 的封装机制,下面继续回到本文的核心 —— hook。下面我想先给你介绍一个新的概念:
在 React 后台,hook 被表示为以调用顺序链接起来的节点。这样作缘由是 hook 并不能简单的被建立而后丢弃。它们有一套特有的机制,也正是这些机制让它们成为 hook。一个 hook 会有数个属性,在继续学习以前,我但愿你能牢记于心:
另外,咱们也须要从新思考看待组件状态的方式。目前,咱们只把它看做一个简单的对象:
{
foo: 'foo',
bar: 'bar',
baz: 'baz',
}
复制代码
旧视角理解 React 的状态
可是当处理 hook 的时候,状态须要被看做是一个队列,每一个节点都表示一个状态模型:
{
memoizedState: 'foo',
next: {
memoizedState: 'bar',
next: {
memoizedState: 'bar',
next: null
}
}
}
复制代码
新视角理解 React 的状态
单个 hook 节点的结构能够在源码中查看。你将会发现,hook 还有一些附加的属性,可是弄明白 hook 是如何运行的关键在于它的 memoizedState
和 next
属性。其余的属性会被 useReducer()
hook 使用,能够缓存发送过的 action 和一些基本的状态,这样在某些状况下,reduction 过程还能够做为后备被重复一次:
baseState
—— 传递给 reducer 的状态对象。baseUpdate
—— 最近一次建立 baseState
的已发送的 action。queue
—— 已发送 action 组成的队列,等待传入 reducer。不幸的是,我尚未彻底掌握 reducer 的 hook,由于我没办法复现它任何的边缘状况,因此讲述这部分就很困难。我只能说,reducer 的实现和其余部分相比显得很不一致,甚至它本身源码中的注解都声明“不肯定这些是不是所须要的语义”;因此我怎么可能肯定呢?!
因此咱们仍是回到对 hook 的讨论,在每一个函数组件调用前,一个名为 [prepareHooks()](https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123)
的函数将先被调用,在这个函数中,当前 fiber 和 fiber 的 hook 队列中的第一个 hook 节点将被保存在全局变量中。这样,咱们不管什么时候调用 hook 函数(useXXX()
),它都能知道运行上下文。
let currentlyRenderingFiber
let workInProgressQueue
let currentHook
// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123
function prepareHooks(recentFiber) {
currentlyRenderingFiber = workInProgressFiber
currentHook = recentFiber.memoizedState
}
// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148
function finishHooks() {
currentlyRenderingFiber.memoizedState = workInProgressHook
currentlyRenderingFiber = null
workInProgressHook = null
currentHook = null
}
// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:115
function resolveCurrentlyRenderingFiber() {
if (currentlyRenderingFiber) return currentlyRenderingFiber
throw Error("Hooks can't be called")
}
// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:267
function createWorkInProgressHook() {
workInProgressHook = currentHook ? cloneHook(currentHook) : createNewHook()
currentHook = currentHook.next
workInProgressHook
}
function useXXX() {
const fiber = resolveCurrentlyRenderingFiber()
const hook = createWorkInProgressHook()
// ...
}
function updateFunctionComponent(recentFiber, workInProgressFiber, Component, props) {
prepareHooks(recentFiber, workInProgressFiber)
Component(props)
finishHooks()
}
复制代码
hook 队列实现的概览。
一旦更新完成,一个名为 [finishHooks()](https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148)
的函数将会被调用,在这个函数中,hook 队列中第一个节点的引用将会被保存在已渲染 fiber 的 memoizedState
属性中。这就意味着,hook 队列和它的状态能够在外部定位到。
const ChildComponent = () => {
useState('foo')
useState('bar')
useState('baz')
return null
}
const ParentComponent = () => {
const childFiberRef = useRef()
useEffect(() => {
let hookNode = childFiberRef.current.memoizedState
assert(hookNode.memoizedState, 'foo')
hookNode = hooksNode.next
assert(hookNode.memoizedState, 'bar')
hookNode = hooksNode.next
assert(hookNode.memoizedState, 'baz')
})
return (
<ChildComponent ref={childFiberRef} /> ) } 复制代码
从外部读取某一组件记忆的状态
下面咱们来分类讨论 hook,首先从使用最普遍的开始 —— state hook:
你必定会以为很吃惊:useState
hook 在后台使用了 useReducer
,而且它将 useReducer
做为预约义的 reducer(详见源码)。这意味着,useState
返回的结果实际上已是 reducer 状态,同时也是一个 action dispatcher。请看,以下是 state hook 使用的 reducer 处理器:
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
复制代码
state hook 的 reducer,又名基础状态 reducer。
因此正如你想象的那样,咱们能够直接将新的状态传入 action dispatcher;可是你看到了吗?!咱们也能够传入 action 函数给 dispatcher,这个 action 函数能够接收旧的状态并返回新的。(在本篇文章写就时,这种方法并无记录在 React 官方文档中,很遗憾的是,它其实很是有用!)这意味着,当你向组件树发送状态设置器的时候,你能够修改父级组件的状态,同时不用将它做为另外一个属性传入,例如:
const ParentComponent = () => {
const [name, setName] = useState()
return (
<ChildComponent toUpperCase={setName} /> ) } const ChildComponent = (props) => { useEffect(() => { props.toUpperCase((state) => state.toUpperCase()) }, [true]) return null } 复制代码
根据旧状态返回新状态。
最后,effect hook —— 它对于组件的生命周期影响很大,那么它是如何工做的呢:
effect hook 和其余 hook 的行为有一些区别,而且它有一个附加的逻辑层,这点我在后文将会解释。在我分析源码以前,首先我但愿你牢记 effect hook 的一些属性:
注意,我使用了“绘制”而不是“渲染”。它们是不一样的,在最近的 React 会议中,我看到不少发言者错误的使用了这两个词!甚至在官方 React 文档中,也有写“在渲染生效于屏幕以后”,其实这个过程更像是“绘制”。渲染函数只是建立了 fiber 节点,可是并无绘制任何内容。
因而就应该有另外一个队列来保存这些 effect hook,而且还要可以在绘制后被定位到。一般来讲,应该是 fiber 保存包含了 effect 节点的队列。每一个 effect 节点都是一个不一样的类型,并能在适当的状态下被定位到:
在修改以前调用 getSnapshotBeforeUpdate()
实例(详见源码)。
运行全部插入、更新、删除和 ref 的卸载(详见源码)。
运行全部生命周期函数和 ref 回调函数。生命周期函数会在一个独立的通道中运行,因此整个组件树中全部的替换、更新、删除都会被调用。这个过程还会触发任何特定于渲染器的初始 effect hook(详见源码)。
useEffect()
hook 调度的 effect —— 也被称为“被动 effect”,它基于这部分代码(也许咱们要开始在 React 社区内使用这个术语了?!)。
hook effect 将会被保存在 fiber 一个称为 updateQueue
的属性上,每一个 effect 节点都有以下的结构(详见源码):
tag
—— 一个二进制数字,它控制了 effect 节点的行为(后文我将详细说明)。create
—— 绘制以后运行的回调函数。destroy
—— 它是 create()
返回的回调函数,将会在初始渲染前运行。inputs
—— 一个集合,该集合中的值将会决定一个 effect 节点是否应该被销毁或者从新建立。next
—— 它指向下一个定义在函数组件中的 effect 节点。除了 tag
属性,其余的属性都很简明易懂。若是你对 hook 很了解,你应该知道,React 提供了一些特殊的 effect hook:好比 useMutationEffect()
和 useLayoutEffect()
。这两个 effect hook 内部都使用了 useEffect()
,实际上这就意味着它们建立了 effect hook,可是却使用了不一样的 tag 属性值。
这个 tag 属性值是由二进制的值组合而成(详见源码):
const NoEffect = /* */ 0b00000000;
const UnmountSnapshot = /* */ 0b00000010;
const UnmountMutation = /* */ 0b00000100;
const MountMutation = /* */ 0b00001000;
const UnmountLayout = /* */ 0b00010000;
const MountLayout = /* */ 0b00100000;
const MountPassive = /* */ 0b01000000;
const UnmountPassive = /* */ 0b10000000;
复制代码
React 支持的 hook effect 类型
这些二进制值中最经常使用的情景是使用管道符号(|
)链接,将比特相加到单个某值上。而后咱们就可使用符号(&
)检查某个 tag 属性是否能触发一个特定的行为。若是结果是非零的,就表示能够。
const effectTag = MountPassive | UnmountPassive
assert(effectTag, 0b11000000)
assert(effectTag & MountPassive, 0b10000000)
复制代码
如何使用 React 的二进制设计模式的示例
这里是 React 支持的 hook effect,以及它们的 tag 属性(详见源码):
UnmountPassive | MountPassive
.UnmountSnapshot | MountMutation
.UnmountMutation | MountLayout
.以及这里是 React 如何检查行为触发的(详见源码):
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
}
复制代码
React 源码节选
因此,基于咱们刚才学习的关于 effect hook 的知识,咱们能够实际操做,从外部向 fiber 插入一些 effect:
function injectEffect(fiber) {
const lastEffect = fiber.updateQueue.lastEffect
const destroyEffect = () => {
console.log('on destroy')
}
const createEffect = () => {
console.log('on create')
return destroy
}
const injectedEffect = {
tag: 0b11000000,
next: lastEffect.next,
create: createEffect,
destroy: destroyEffect,
inputs: [createEffect],
}
lastEffect.next = injectedEffect
}
const ParentComponent = (
<ChildComponent ref={injectEffect} /> ) 复制代码
插入 effect 的示例
这就是 hooks 了!阅读本文你最大的收获是什么?你将如何把新学到的知识应用于 React 应用中?但愿看到你留下有趣的评论!
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。