React Hooks 详解
Hooks 的出现
2015 年之前,React 通过 React.createClass 方法创建组件实例
ES6 发布的 Class 语法得到支持后,React 遵循标准,从 v0.13.1 开始支持 Class 语法形式的组件,即 ClassComponent
为了解决 ClassComponent 存在的 “业务逻辑分散” 和 “有状态的组件复用困难” 两个问题,V16.8 带来了 Hooks
自 Hooks 问世后,React 的后续发展主要围绕 FC 展开,不再有关于 ClassComponent 的重要特性出现
心智模型——代数效应
概念
代数效应是函数式编程中的一个概念,用于将副作用从函数调用中分离。简单地说,就是一种在函数内部声明需要执行某些操作,但不立即执行它们的方式,这些操作的具体实现留给函数的调用者或者环境去处理。
React Hooks 为什么没有使用细粒度更新?
原因在于 React 属于应用级框架,从关注“自变量与应用的对应关系”角度看,其更新粒度不需要很细,因此无需使用细粒度更新。
但是,作为代价,React Hooks 在使用上会受到两个制约:
需要显式指明依赖
不能在条件语句中声明 Hooks
React Hooks 原理
引子
Hooks 的执行流程
FC 进入 render 流程之前,会先确定 ReactCurrentDispatcher.current 指向,此时涉及到
renderWithHooks进入 Hooks mount 流程时,执行 mount 对应逻辑,方法名一般为 “mountXXX”(例如调用 useState,实际会调用 mountState),此时涉及到
mountWorkInProgressHookHooks update 时,执行 update 对应逻辑,方法名一般为 “updateXXX”(例如调用 useState,实际会调用 updateState),此时涉及到
updateWorkInProgressHook
renderWithHooks 执行函数
renderWithHooks 执行函数包含 Hooks 功能的 FC 会在 beginWork 中执行 renderWithHooks,代码在:packages/react-reconciler/src/ReactFiberBeginWork.js 文件中
参数解释
current
类型:Fiber 节点
说明:表示组件在上一次渲染时的 Fiber 节点。如果是初次渲染,这个参数值为
null。它包含了上一次渲染的状态,包括子 Fiber 节点、DOM 节点、钩子状态等。
workInProgress
类型:Fiber 节点
说明:表示当前正在进行的工作的 Fiber 节点。这是对
current的克隆,React 将在这个节点上应用本次渲染期间的所有更新。一旦完成,它将成为下一次渲染的current。
Component
类型:函数组件
说明:当前正在渲染的函数组件本身。
renderWithHooks将执行这个函数,传入props和secondArg(如果有的话),并处理返回的 jsx 或其他元素。
props
类型:Object
说明:包含组件接收到的所有属性的对象。这些是传递给组件函数的参数,组件内部通过这些
props来渲染 UI 或执行逻辑。
secondArg
类型:任意
说明:这个参数通常用于传递给函数组件的第二个参数。对于大多数函数组件,这个参数是不需要的。在某些特殊情况下,比如使用
React.forwardRef时,这个参数会被用来传递ref。
nextRenderLanes
类型:Lanes
说明:表示本次渲染的优先级。在 React 的并发模式中,每个更新都有自己的优先级(lane)。这个参数帮助 React 确定哪些更新应该在当前渲染中被处理,哪些可以被推迟到未来的渲染中。
HooksDispatcherOnMount
初始化第一次渲染
HooksDispatcherOnUpdate
更新组件
ContextOnlyDispatcher

初始化阶段 mountWorkInProgressHook
mountWorkInProgressHookFC 初始化的时候,第一次调用 hooks,例如 useState,都会首先调用mountWorkInProgressHook
mountWorkInProgressHook 这个函数做的事情很简单,首先每次执行一个 hooks 函数,都产生一个 hook 对象,里面保存了当前 hook 信息,然后将每个 hooks 以链表形式串联起来,并赋值给 workInProgress Fiber 节点的 memoizedState。所以函数组件用 memoizedState 存放 hooks 链表。
更新 updateWorkInProgressHook
updateWorkInProgressHook首先如果是第一次执行 hooks 函数,那么从 current 树上取出 memoizedState ,也就是旧的 hooks。
然后声明变量 nextWorkInProgressHook,这里应该值得注意,正常情况下,一次 renderWithHooks 执行开始时,workInProgress 上的 memoizedState 会被置空,hooks 函数顺序执行,nextWorkInProgressHook 应该一直为 null,那么什么情况下 nextWorkInProgressHook 不为 null,也就是当一次 renderWithHooks 执行过程中,执行了多次函数组件
最后复制 current 的 hook 的字段,把它赋值给 workInProgressHook,用于更新新的一轮 hooks 状态。
useState 源码
useState 源码初始化 useState -> mountState
useState -> mountState调用 dispatch 实际是调用 dispatchSetState
dispatch 实际是调用 dispatchSetState例如:const [number, setNumber] = useState(0) 中的 setNumber 就是 dispatchSetState,它的第一个参数和第二个参数,已经被 bind 给改成 currentlyRenderingFiber 和 queue,我们传入的参数是第三个参数 action
如果当前 fiber 没有处于更新阶段。那么通过调用 lastRenderedReducer 获取最新的 state,和上一次的 currentState,进行浅比较,如果相等,那么就退出,这就证实了为什么 useState,两次值相等的时候,组件不渲染的原因了
如果两次 state 不相等,那么调用 scheduleUpdateOnFiber 调度渲染当前 fiber,scheduleUpdateOnFiber 是 react 渲染更新的主要函数。
更新 useState -> updateState
useState -> updateStateuseEffect 源码
useEffect 源码React 中用于定义“有副作用因变量”的 Hook 有三个。
useEffect:回调函数会在 commit 阶段完成后异步执行,所以不会阻塞视图渲染。
useLayoutEffect:回调函数会在 commit 阶段的 Layout 子阶段同步执行,一般用于执行“DOM 相关操作”
useInsertionEffect:回调函数会在 commit 阶段的 Mutation 子阶段同步执行,useInsertionEffect 执行时无法访问“对 DOM 的引用”,这个 Hook 是转为 CSS-in-JS 库“插入全局 Style 元素或 Defs 元素(对于 SVG)”而设计的。
对于三个 “effect 相关 Hook”,hook.memoizedState 共用一套数据结构,就是 effect 对象
初始化 useEffect -> mountEffect
useEffect -> mountEffectmountEffect 内部调用 mountEffectImpl
首先创建一个 effect ,判断组件如果第一次渲染,那么创建 componentUpdateQueue ,就是 workInProgress 的 updateQueue。然后将 effect 放入 updateQueue.lastEffect 中。
更新 useEffect -> updateEffect
useEffect -> updateEffectupdateEffectImpl 主要逻辑是判断两次 deps 相等,如果相等说明此次更新不需要执行,则直接调用 pushEffect,这里注意 effect 的标签是 hookEffectTag,如果不相等,那么更新 effect ,并且赋值给 hook.memoizedState,此时 effect 的标签是 HookHasEffect | hookEffectTag,然后在 commit 阶段,react 会通过标签来判断,是否执行当前的 effect 函数。
useMomo 源码
useMomo 源码初始化 useMemo -> mountMemo
useMemo -> mountMemo初始化 useMemo,就是创建一个 hook,然后执行 useMemo 的第一个参数,得到需要缓存的值,然后将值和 deps 记录下来,赋值给当前 hook 的 memoizedState。
更新 useMemo -> updateMemo
useMemo -> updateMemouseRef 源码
useRef 源码初始化 useRef -> mountRef
useRef -> mountRefuseRef 初始化很简单, 创建一个 ref 对象, 对象的 current 属性来保存初始化的值,最后用 memoizedState 保存 ref,完成整个操作。
更新 useRef -> updateRef
useRef -> updateRef函数组件更新 useRef 做的事情更简单,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次,hook.memoizedState 内存中都指向了一个对象,所以解释了 useEffect,useMemo 中,为什么 useRef 不需要依赖注入,就能访问到最新的改变值。
React Hooks 实现细粒度更新
在 React 中定义因变量时需要显式指明“因变量依赖的自变量”,例如 useMemo 的第二个参数:const y = useMemo(() => x * 2 + 1, x)。
但是在 Vue 中并不需要显式指明,这是因为 Vue 使用“能自动追踪依赖的技术”被称为“细粒度更新”(Fine-Grained Reactivity)例如:const y = computed(() => x.value * 2 + 1)。
模拟实现 React Hooks 细粒度更新
示例
示例 1
示例 2
React Hooks 为什么没有使用细粒度更新?
因为 React 属于应用级框架,从关注“自变量与应用的对应关系”角度看,其更新粒度不需要很细,因此无需使用细粒度更新。
hooks 合集
https://usehooks.com/
参考
Last updated