从源码理解 React Hook 是如何工作的

网站建设3年前发布
68 0 0

2023030615243044752c8360f9d50f80268035e057c876fc9d4a826,大家好,我是前端西瓜哥。,今天我们从源码来理解 React Hook 是如何工作的。,React Hook 是 React 16.8 后新加入的黑魔法,让我们可以 在函数组件内保存内部状态。,Hook 的优势:,在讲解源码之前,先认识一些 重要的全局变量:,currentlyRenderingFiber:正在处理的函数组件对应 fiber。在执行 useState 等 hook 时,需要通过它知道当前 hook 对应哪个 fiber。,workInProgressHook:挂载时正在处理的 hook 对象。我们会沿着 workInProcess.memoizedState 链表一个个往下走,这个 workInProgressHook 就是该链表的指针。,currentHook:旧的 fiber 的 hooks 链表(current.memorizedState)指针。,ReactCurrentDispatcher:全局对象,是一个 hook 调度器对象,其下有 useState、useEffect 等方法,是我们业务代码中 hook 底层调用的方法。ReactCurrentDispatcher 有三种:,构建函数实例是在 renderWithHooks 方法中进行的。,主要逻辑为:,下面看看在函数组件一些常见 Hook 是如何工作的。,首先讨论 状态 Hook 中最常见的一种:useState。,useState 在挂载阶段,调用的是 HooksDispatcherOnMount.useState,也就是 mountState。,mountWorkInProgressHook 实现:,之前 mountState 时,我们返回了一个绑定了 fiber、queue 参数的 dispatchSetState。setState 更新操作调用的正是这个 dispatchSetState。,第一个 setState 在被调用时会立即计算新状态,这是为了 做新旧 state 对比,决定是否更新组件。如果对比发现状态没变,继续计算下一个 setState 的新状态,直到找到为止。如果没找到,就不进行更新。,其后的 setState 则不会计算,等到组件重新 render 再计算。,为对比新旧状态计算出来的状态值,会保存到 update.eagerState,并将 update.hasEagerState 设置为 true,之后更新时通过它来直接拿到计算后的最新值。,dispatchSetState 会拿到对应的 fiber、queue(对应 hook 的 queue)、action(新的状态)。,我们先了解一个前置知识:useState 是特殊的 useReducer。,useState 本质上在使用 useReducer,在 React 源码层提供了特殊的名为 basicStateReducer 的 reducer,后面源码解析中会看到它。,回到正题。,useState 在更新阶段会拿到上一次的状态值,此阶段调用的是 HooksDispatcherOnUpdate.useState,也就是 updateState。,updateState 会调用 updateReducer(useReducer 更新阶段也用这个),这也是为什么我说 setState 是特殊 useReducer 的原因。,updateReducer 主要工作有两个:,先看看 updateWorkInProgressHook 方法。,该方法中,currentHook 设置为 current.memoizedState 链表的下一个 hook,拷贝它到 currentlyRenderingFiber.memoizedState 链表上,返回这个 hook。,拿到拷贝后的 hook,就可以计算新状态值了。,首先将 hook.queue.pending 队列合并到 currentHook.baseQueue 下。该队列包含了一系列 update 对象(因为可能调用了多次 setState),里面保存有 setState 传入的最新状态值(函数或其他值)。,然后遍历 update 计算出最新状态,保存回 hook,并返回最新状态值和 setState 方法。,有些逻辑类似 useState,比如创建 hook 的 mountWorkInProgressHook 方法实现,所以一些重复逻辑就不说了,直奔核心。,核心函数是 mountEffectImpl。,pushEffect 实现:,核心实现在 updateEffectImpl。,我们看下依赖项对比算法 areHookInputsEqual 的细节,它同时遍历到新旧依赖项最长的尾部,进行 Object.is 对比。在空数组情况下,这个比较一定返回 true,所以能模拟 componentDidMount / Unmount 的效果。,当 commit 阶段结束后,useEffect 的 create 和 destroy 会被 Schedule 调度器异步调度执行。,fiber.updateQueue 下的 effect 会按顺序取出,然后一个个执行。,之前依赖项相同的话,虽然也创建 effect,但它的 tag 对不上,是不会执行的。,1、React Hooks 为什么不能写在条件语句中?,我们要保证 React Hooks 的顺序一致。,函数组件的状态是保存在 fiber.memorizedState 中的。它是一个链表,保存调用 Hook 生成的 hook 对象,这些对象保存着状态值。当更新时,我们每调用一个 Hook,其实就是从 fiber.memorizedState 链表中读取下一个 hook,取出它的状态。,如果顺序不一致了或者数量不一致了,就会导致错误,取出了一个其他 Hook 对应的状态值。,2、React Hooks 为什么必须在函数组件内部执行?React 如何能够监听 React Hooks 在外部执行并抛出异常?​,Hooks 底层调用的是一个全局变量 ReactCurrentDispatcher 的一系列方法。,这个全局变量会在不同阶段设置为不同的对象。render 过程中,挂载阶段设置为 HooksDispatcherOnMount,更新阶段设置为 HooksDispatcherOnUpdate。它们会读取 currentlyRenderingFiber 全局变量,这个全局变量代表正在处理的 fiber,读取它进行一些设置状态和读取状态等操作。,在 render 阶段外,会设置为 ContextOnlyDispatcher,这个对象下所有方法都会抛出错误,因为此时不存在正常处理的 fiber,使用时机是并不对。,本文只讲了状态 Hook 代表 useState,和 副作用 Hook 代表 useEffect,其他 Hook 其实也差不多。

© 版权声明

相关文章