对于 react 16.7 中新的 hooks 系统在社区中引起的骚动,我们都有所耳闻了。人们纷纷动手尝试,并为之兴奋不已。一想到 hooks 时它们似乎是某种魔法,React 以某种甚至不用暴露其实例(起码没有用到这个关键词)的手段管理了你的组件。那么 React 究竟捣了什么鬼呢?
今天让我们来深入 React 关于 hooks 的实现以更好地理解它。这个魔法特性的问题就在于一旦其发生了问题是难以调试的,因为它隐藏在了一个复杂的堆栈追踪的背后。因此,深入理解 React 的 hooks 系统,我们就能在遭遇它们时相当快地解决问题,或至少能在早期阶段避免它们。
丑话说在前面,我并不是一名 React 的开发者/维护者,以及我的言论不需要太过当真。我非常深入的研究了 React 的 hooks 系统的实现,但不管怎么说我也不能保证这就是 React 如何工作的真谛。也就是说,我的言论基于 React 的源码,并尽可能地让我的论据可靠。
首先,让我们了解一遍确保 hooks 在 React 的作用域内被调用的机制,因为你大概已经知道如果不在正确的上下文中调用,hooks 是没有意义的:
dispatcher 是一个包含了 hooks 函数的共享对象。它将基于 Reactdom 的渲染阶段被动态地分配或清理,并且它将确保用户不会超出一个 React 组件去访问 hooks (https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/packages/react-reconciler/src/ReactFiberDispatcher.js#L24)。
hooks 被一个叫做 enableHooks 的标志位变量启用或禁用,在我们刚刚渲染根组件时,判断该标志位并简单的切换到合适的 dispatcher 上;这意味着从技术上来说我们能在运行时启用或禁用 hooks。React 16.6.X 也试验性的实现了该特性, 但实际上是被禁用的.
当我们完成了渲染工作,我们将 dispatcher 作废,这预防了 hooks 被意外地从 ReactDOM 的渲染循环之外访问。该机制将确保用户不出昏招。
在所有 hook 的每一次调用时,都会用 resolveDispatcher() 获得 dispatcher 的引用。正如我之前所说,在 React 渲染循环之外的访问应该是没有意义的,这种情况下 React 应该打印警告信息: “Hooks can only be called inside the body of a function component” 。
//react-hooks-dispatcher.js
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 表现为以其调用顺序被链接在一起的节点(nodes)。它们之所以表现成这样是因为 hooks 并非被简单的创建后就独自行事了。有一个允许它们按身份行事的机制。我想请你在深入其实现之前记住一个 hook 的若干属性:
其初始状态是在初次渲染中被创建的
其状态可以被动态更新
React 会在之后的渲染中记住 hook 的状态
React 会按照调用顺序提供给你正确的状态
React 知道该 hook 是属于哪个 fiber 的
相应的,我们需要重新思考我们看待一个组件的状态的方式了。至今为止我们是将其当作一个 plain object 的:
//react-state-old.js
{
foo: 'foo',
bar: 'bar',
baz: 'baz',
}
但当我们处理 hooks 时应将其视作一个队列,其每个节点都表现为一个单个的状态模型:
//react-state-new.js
{
memoizedState: 'foo',
next: {
memoizedState: 'bar',
next: {
memoizedState: 'baz',
next: null
}
}
}
单个 hook 节点的模式可以在实现中看到。你将发现 hook 有一些附加的属性,但理解 hooks 如何工作的关键就潜藏在 memoizedState 和 next 中。其余的属性被 useReducer() hook 特别的用来缓存已分发过的 actions 和基础状态,这样在 useReducer 的遍历过程中相关逻辑就可以在各种情况下作为一个 fallback 被重复执行:
baseState :会被传给 reducer 的状态对象
baseUpdate :最近一次 dispatch 过的用来创建 baseState 的 action
queue :一个 dispatch 过的 actions 列表,等待遍历 reducer
糟糕的是我无法全面领悟 reducer hook,因为我没能设法复现几乎任何一个它的边缘情况,所以也就不展开细说了。我只能说 reducer 的实现是如此的前后矛盾以至于其自己的一处注释中甚至说 “TODO: 不确定这是不是预期的语义...我不记得是为什么了” ;所以我又能如何确定呢?!
回到 hooks,在每个函数组件调用之前,一个叫做 prepareHooks() 的函数先被调用,当前 fiber 和其位于 hooks 队列中的首个 hook 会被存储在全局变量中。通过这种方式,每次我们调用一个 hook 函数( useXXX() )时,它都知道在哪个上下文中运行了。
//react-hooks-queue.js
let currentlyRenderingFiber
let workInProgressQueue
let currentHook
// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js#L123
function prepareHooks(recentFiber) {
currentlyRenderingFiber = workInProgressFiber
currentHook = recentFiber.memoizedState
}
// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js#L148
function finishHooks() {
currentlyRenderingFiber.memoizedState = workInProgressHook
currentlyRenderingFiber = null
workInProgressHook = null
currentHook = null
}
// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js#L115
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#L267
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 队列和它们的状态可被从外部处理:
//react-state-external.js
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,从最常用的 state hook 开始:
你知道了可能会惊讶,但 useState hook 在幕后使用了 useReducer 并简单地提供给后者一个预定义的 reducer 处理函数。这意味着从 useState 返回的结果实际上是一个 reducer state 以及一个 action dispatcher。我想让你看看 state hook 使用的 reducer 处理函数:
//react-basic-state-reducer.js
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
所以按照预期,我们可以向 action dispatcher 直接传入新的 state;但你看到了什么?!我们也能传入一个 action 函数,用以处理旧 state 并返回一个新的。 这在官方文档中从未提及(在本文成文之际)并且这有点遗憾因为这特别有用! 这意味着当你已经把 state setter 发往组件树后仍可改变父组件的当前状态,而不用向其传入一个不同的 prop。比如:
//react-state-dispatcher.js
const ParentComponent = () => {
const [name, setName] = useState()
return (
<ChildComponent toUpperCase={setName} />
)
}
const ChildComponent = (props) => {
useEffect(() => {
props.toUpperCase((state) => state.toUpperCase())
}, [true])
return null
}
最后来看看在一个组件的生命周期上施展魔法效果的 — effect hooks,以及它是如何工作的:
Effect hooks 表现得稍有不同,我也想说说其额外的一个逻辑层。再说一次,在我深入解释实现之前,希望你记住关于 effect hooks 属性的一些事情:
它们在渲染时被创建,但在绘制(painting)之后才运行
如果存在,它们会在下次绘制之前才被销毁
按定义的顺序被调用
注意我使用了术语 “painting” 而不是 “rendering”。这两者截然不同,而我注意到最近许多演说者最近在 React Conf (https://conf.reactjs.org/) 上使用了错误的词语!甚至在官方 React 文档中他们也说 “after the render is committed to the screen”,其实应该是类似 “painting” 的。render() 方法只是创建 fiber 节点但并不绘制任何东西。
相应 地,也应该有另一个额外的队列来保存这些 effects 并能在绘制后被处理。一般来说,一个 fiber 持有一个包含了 effect 节点的队列。每个 effect 都属于一个不同的类型并应该在其相应的阶段被处理:
在突变前调用 getSnapshotBeforeUpdate() 的实例
执行宿主上的所有插入、更新、删除和 ref 卸载
执行所有生命周期和 ref 回调。生命周期作为一个独立发生的阶段,整个树中的所有置入、更新和删除也都会被调用。该阶段也会触发任何特定于渲染器的初始化 effects
由 useEffect() hook 调度的 effects -- 从源码中可知其称呼为 “passive effects(消极影响)” (我们或许应该在 React 社区中开始用这个术语了?!)
hook effects 应该被存储在 fiber 的 updateQueue 属性上,并且每个 effect 节点应该有如下结构:
tag :一个二进制数字,表示该 effect 的行为(稍后我会详述)
create :绘制之后应该运行的回调
destroy :由 create() 回调返回,应该早于初次渲染运行
inputs :一个值的集合,用来决定 effect 是否应该被销毁或重建
next :一个对定义在函数组件中的下一个 effect 的引用
除了 tag 属性,其他属性都很易于理解。如果你熟悉 hooks,应该知道 React 提供了一对特殊的 effect hooks: useMutationEffect() 和 useLayoutEffect() 。两者内部都用了 useEffect() ,意味着本质上它们都创建了一个 effect 节点,但它们用了不同的 tag 值。
tag 由一组二进制值构成:
//react-hook-effects-types.js
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 是否实现了一个特定的行为。如果结果非零,就意味着 tag 的实现达到了预期。
//react-bin-design-pattern-test.js
const effectTag = MountPassive | UnmountPassive
assert(effectTag, 0b11000000)
assert(effectTag & MountPassive, 0b10000000)
这是被 React 支持的 hook effect 类型,以及其 tags:
Default effect — UnmountPassive | MountPassive.
Mutation effect — UnmountSnapshot | MountMutation.
Layout effect — UnmountMutation | MountLayout.
并且 React 是这样检查行为实现的:
//react-effect-hooks-real-usage.js
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
}
所以,基于我们以及学过的涉及 effect hooks 的知识,实际上可以从外部向一个特定 fiber 注入一个 effect:
//react-hook-effect-injection.js
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} />
)
大功告成!
原文:https://medium.com/the-guild/under-the-hood-of-reacts-hooks-system-eb59638c9dba
近日,据 MIT Technology Review 报道,一位名为“Repairnator”的机器人在 GitHub 上“卧底”数月,查找错误并编写和提交修复补丁,结果有多个补丁成功通过并被采纳,这位 Repairnator 到底是如何拯救程序员于水火的呢?
你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?你在还在为组件中的this指向而晕头转向吗?这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张。
我们将userReducer函数返回的原始dispath命名为origin_dispatch,自定义dispatch函数,当action为函数的时候,我们执行action函数,并将origin_dispatch当作参数传进去;action不是函数,直接调用origin_dispatch,不做处理
使用useEffect 就像瑞士军刀。它可以用于很多事情,从设置订阅到创建和清理计时器,再到更改ref的值。与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。
从 React Hooks 正式发布到现在,我一直在项目使用它。但是,在使用 Hooks 的过程中,我也进入了一些误区,导致写出来的代码隐藏 bug 并且难以维护。这篇文章中,我会具体分析这些问题,并总结一些好的实践,以供大家参考
Hooks 的 API 可以参照 React 官网。本文主要是结合 Demo 详细讲解如何用 Hooks 来实现 React Class Component 写法,让大家更深的理解 Hooks 的机制并且更快的入门。 注意:Rax 的写法和 React 是一致的
以下是上一代标准写法类组件的缺点,也正是hook要解决的问题,型组件很难拆分和重构,也很难测试。业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
Hooks出来已经有段时间了,相信大家都用过段时间了,有没有小伙伴们遇到坑呢,我这边就有个 setInterval 的坑,和小伙伴们分享下解决方案。写个 count 每秒自增的定时器,如下写法结果,界面上 count 为 1 ?
9月份开始,使用了React16.8的新特性React Hooks对项目进行了重构,果然,感觉没有被辜负,就像阮一峰老师所说的一样,这个 API 是 React 的未来。
由于篇幅所限文章中并没有给出demo的所有代码,大家如果有兴趣可以将代码clone到本地从commit来看整个demo的TDD过程,配合文章来看会比较清晰,从进公司前认识了TDD,到实践TDD
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!