通过实践,更好的认识它。
我们都已经听说过它(指Hooks)。这个React16.7中的新的hook系统在社区中引起非常多的争议。我们都已经试用并且测试过它,对于它以及它的潜力都感到非常的兴奋。当你思考hooks的时候,会觉得他们有种魔力,不知为何,React甚至能够在不暴露组件实例的情况下管理你的组件(甚至不使用this
关键字)。所以究竟发生了什么,能让React做到那些呢?
今天我将深入hooks使用的内部,以便我们可以更好的理解它。这个神奇特性的问题是一旦出现问题就会很难进行定位,因为它被隐藏在了一个复杂的调用栈背后。因此,通过深入了解React的hook系统,我们将能够在我们遇到这些问题的时候快速解决,甚至是在第一时间避免他们。
在我们开始之前,我想说的是我不是一个React的开发/维护者,所以对于我所说的话应该保持一些怀疑。我确实有非常深入去是使用过React的hook,但总的来收,我不能保证这就是React真正确实这么工作的。这么说吧,我已经尽可能带着证据去斟酌我的描述,并且直接引用React的源代码,试着让我的论证尽可能的有效。
首先,让我们理清运行机制,确保hooks是在React作用域的内部被调用的,因为你现在应该已经知道hooks在错误的上下文中是没有意义的:
dispatcher
dispatcher是一个包含hook函数的可共享的对象。它会被动态的分配或者是在渲染ReactDOM的阶段被清理,它将会确保用户不能在React组件的外部使用hooks(见使用)。
通过简单的切换,获取得到dispatcher,在我们渲染跟组件的时候,hooks会被一个叫做enableHooks
的标志来被决定是被使用或者是被禁用;这意味着在技术上,我们可以在运行时使用或者禁用hooks。React16.6.X 的版本也有这个实验性质的功能被使用,但其实已经被禁用了。(见使用)
当我们执行完渲染工作,我们将dispatcher置为null,从来防止Hooks在ReactDOM的渲染周期外部被意外的使用。 这是一个可以确保用户不会作出愚蠢的事情的机制(见使用)。
dispatcher在每一个和任何一个hook调用时被解析,解析是通过调用一个resolveDispatcher()
函数。就是我之前说的那样,如果当前是在React渲染周期的外部,这些应该都是没有意义的,并且React会打印初告警信息:Hooks can only be called inside the body of a function component.(见使用)。
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
}
现在我们已经对这个封装机制的有了一定的了解,我想我们该进入本文的核心部分-- hooks。首先我想给你们介绍一个新的概念:
Hooks的队列
在内部,hooks是作为节点形式被保存的,并且是以他们被调用的顺序被结合在一起。他们被这样保存是因为hooks不是简单的被创造出来以后就被独立放置。他们有一个机制来确保他们能够是他们应该有的样子。一个Hook有几个特性,我建议你在深入使用前能够将他们记在脑中:
- 它的初始状态值是在首次渲染的时候被创建。
- 它的状态值可以非常容易的被更新。
- React会在之后的渲染中记住hooks的状态。
- React将会基于调用顺序给你提供正确的state状态值。
- React将会知道当前的hook属于那个fiber。
相对而言,我们需要重新思考下我们看待组件状态的方式。到目前位置,我们将它看成是一个普通的对象:
{
foo: 'foo',
bar: 'bar',
baz: 'baz',
}
但当处理hooks的时候,它应该被看成是一个队列,那里每个节点代表着一个state的简单模型:
{
memoizedState: 'foo',
next: {
memoizedState: 'bar',
next: {
memoizedState: 'bar',
next: null
}
}
}
一个简单的hook节点的要点可以在这个使用中看到。你将会看到这个hook有一些额外的属性,但是要理解hooks是怎么工作的关键依赖于内部的memoizedState
和next
。其余属性都是被useReducer()
hook用来缓存已经分发过的actions和基本的状态state,从而reduction的过程在不同的场景中可以作为fallback被重复:
baseState
- state对象,将会传递给reducer。baseUpdate
- 最近的被分发来创建baseState
的action。queue
- 被分发的action的队列,等待进入reducer。
不幸的是我没有对于reducer hook有很好的理解,因为我没有完成关于它的任何边界场景使用,因此我不能很好的进行详细的描述。我只想说reducer的使用是如此的不一致,甚至是在<ahref='https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/packages/react-reconciler/src/ReactFiberHooks.js:381'>这里,有一条评论是:不确定这些是否是合理的语法;所以我应该在这里确定吗?!
说回hooks,在任意一个和每一个函数组件声明之前,一个被称之为prepareHooks()
的函数将被调用,在那里当前的fiber和它的hooks队列中的第一个hook节点将会被存储在全局的变量中。这种方式下,任何时间我们调用一个hook函数(useXXX()
)它将会知道在哪个上下文中去运行。
let currentlyRenderingFiber
let workInProgressQueue
let currentHook
// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123
function prepareHooks(recentFiber) {
currentlyRenderingFiber = workInProgressFiber
currentHook = recentFiber.memoizedState
}
// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148
function finishHooks() {
currentlyRenderingFiber.memoizedState = workInProgressHook
currentlyRenderingFiber = null
workInProgressHook = null
currentHook = null
}
// Source: 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")
}
// Source: 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()
}
一旦一次更新已经完成,一个名叫finishHooks()
的函数将会被调用,在那里hooks队列中的第一个节点的引用将会被存储在渲染的fiber中,在memoizedState
属性中。这意味着hooks队列和他们的state状态值可以被放在外部护理:
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} />
)
}
让我们更具体一点,说出更具象的hooks,从所有hooks中最普通的开始-- state hook:
State hooks
你将会惊讶的发现,在useState
hook背后使用的是useReducer
,并且它仅仅是以简单的通过提前定义reducer处理器的方式来提供这个功能(查看<ahref='https://github.com/facebook/react/blob/5f06576f51ece88d846d01abd2ddd575827c6127/packages/react-reconciler/src/ReactFiberHooks.js#L339'>使用)。这意味着useState
返回的结果实际是一个reducer state,一个action dispatcher。我想你应该看下state hook使用的reducer处理器:
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
因此如预期的一样,我们可以直接用新的state来提供一个action dispatcher;但你会查看那个吗?!我们也可以提供一个dispatcher:一个action函数,接受旧的state,返回新的state。这意味着当你在组件树上向下传递一个state设置器的时候,你可以运行父组件中的state的变更,而不需要通过传入一个不同的prop属性值。举个例子:
const ParentComponent = () => {
const [name, setName] = useState()
return (
<ChildComponent toUpperCase={setName} />
)
}
const ChildComponent = (props) => {
useEffect(() => {
props.toUpperCase((state) => state.toUpperCase())
}, [true])
return null
}
最后,effect hooks -- 这个hook对于组件的声明周期以及它怎么工作的产生了一个重要的影响:
Effect hooks
Effect hooks运行有些许不同,有一层额外的逻辑层,我想来解释一下。再一次,在我深入讲解前, 这里有一些关于effect hooks的特性,我希望你们能够记在脑中:
- 他们在每次渲染(render)的时候被创建,但是在绘制(painting)以后运行。
- 如果有的话,他们将在下一次绘制(painting)之前被销毁。
- 他们以他们定义的顺序被调用。
注意的是我这里使用的是“绘制”(painting)这个词,而不是“渲染”(rendering)。这两个是不同的东西,在最近的 React Conf 上,我见到了许多的演讲者都使用错了他们!甚至在官方的 React 文档 ,说的是“在渲染(render)以后提交到屏幕上”,这有点像是“绘制(painting)”。render方法只是创建了fiber节点,但还没有绘制任何东西。
因此,那里应该有另外的额外的队列来存放这些副作用,并且应该在绘制(painting)以后来处理。通常来讲,一个fiber存放一个包含副作用节点的队列。副作用是一个不同的类型,并且应该在合适的阶段来处理:
- 在更新之前唤起
getSnapshotBeforeUpdate()
的实例。(查看使用) - 执行所有的宿主插入,更新,删除和实例的销毁。(查看使用)
- 执行所有的生命周期和实例的回调函数。生命周期是作为一个独立的部分运行,因此所有的替换、更新和整棵树上的删除动作都已经被唤起。这个运行也会触发任一一个渲染中的副作用的初始化。(查看使用)
- 通过
useEffect()
hook预先安排的副作用--在 使用 中也被称之为“passive effect”.(也许我们应该在React社区中开始使用这个称谓?!)
当涉及到hook的副作用,他们应该被存储在fiber中,在一个叫做updateQueue
的属性中,每一个作用的节点应该有以下的一些要点((查看使用)):
tag
-- 一个二进制的数组,用来规定副作用的行为。(后续我将详细的讲诉)create
-- 一个回调函数,应该在,绘制(painting)以后执行。destroy
--create()
返回的回调函数,应该在初始渲染(render)的时候 之前 执行。inputs
-- 一组数值的集合,用于决定哪些effect应该被销毁和重新创建。next
-- 下一个effect的引用,这是在函数组件中被决定的。
除了tag
属性,其他的属性值都比较容易理解。如果你已经很好的学习过hooks,你将会知道React给你提供了一系列的特殊的effect hooks:useMutationEffect()
和 useLayoutEffect()
。这两个effects内部都是使用的 useEffect()
, 本质上意味着他们也会创建一个effect节点,但是他们是通过使用不同的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;
对于这些二进制值使用最普遍的场景是使用一个管道线(|
),并将这些比特加成一个单一的数值。然后我们可以检测一个tag是否执行了一个特定的行为,或者是没有使用一个(&
)。如果结果非0,它意味着这个tag执行了特定的行为。
const effectTag = MountPassive | UnmountPassive
assert(effectTag, 0b11000000)
assert(effectTag & MountPassive, 0b10000000)
这里有一些React支持的hook effect的类型以及他们的tags。(查看使用)
- 默认的effect --
UnmountPassive | MountPassive
- 更新effect --
UnmountSnapshot | MountMutation
- 布局effect --
UnmountMutation | MountLayout
这里是React怎样检测行为实现的代码。(查看使用)
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
}
因此,基于我们已经学习到的effect hooks的内容,我们可以从外部注入一个effect到确切的fiber中:
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} />
)
view rawreact-hook-effect-injection.js hosted
这就是它!你从本文中获取到的最大收获是什么?你准备在你的React应用中怎么使用这个新的知识?非常希望看到有趣的评论!(Me Too!😄)