Hooks

可参考之前文章:React Hooks详解

Hooks 出现

没有Hooks时,函数组件能够做的只是接受 Props、渲染 UI ,以及触发父组件传过来的事件。

有的处理逻辑都要在类组件中写,这样会使 class 类组件内部错综复杂,每一个类组件都有一套独特的状态,相互之间不能复用。

Hooks 出现 本质原因

  • 让函数组件也能做类组件的事,有自己的状态,可以处理一些副作用,能获取 ref ,也能做数据缓存。
  • 解决逻辑复用难的问题。
  • 放弃面向对象编程,拥抱函数式编程。

hooks与fiber(workInProgress)

hooks 作为函数组件本身和函数组件对应的 fiber 之间的沟通桥梁。

在这里插入图片描述
hooks 对象本质上是主要以三种处理策略存在 React 中:

  • ContextOnlyDispatcher: 防止开发者在函数组件外部调用 hooks ,所以第一种就是 报错形态,只要开发者调用了这个形态下的 hooks ,就会抛出异常。
  • HooksDispatcherOnMount: 函数组件 初始化 mount ,hooks 是函数组件和对应 fiber 桥梁,这个时候的 hooks 作用就是建立这个桥梁,初次建立其 hooks 与 fiber 之间的关系。
  • HooksDispatcherOnUpdate:函数组件 更新 update,既然与 fiber 之间的桥已经建好了,那么组件再更新,就需要 hooks 去获取或者更新维护状态。

hooks对象:

const HooksDispatcherOnMount = { /* 函数组件初始化用的 hooks */
    useState: mountState,
    useEffect: mountEffect,
    ...
}
const  HooksDispatcherOnUpdate ={/* 函数组件更新用的 hooks */
   useState:updateState,
   useEffect: updateEffect,
   ...
}
const ContextOnlyDispatcher = {  /* 当hooks不是函数内部调用的时候,调用这个hooks对象下的hooks,所以报错。 */
   useEffect: throwInvalidHookError,
   useState: throwInvalidHookError,
   ...
}

函数组件触发

在 fiber 调和过程中,到 FunctionComponent 类型的 fiber,调用 updateFunctionComponent 更新 fiber,内部就会调用 renderWithHooks

renderWithHooks

在这里插入图片描述

  1. hooks 内部通过 currentlyRenderingFiber 读取当前 fiber 信息。(workInProgress ——正在调和更新函数组件对应的 fiber 树)
  2. memoizedState 保存 hooks 信息。
  3. updateQueue 存放每个 useEffect / useLayoutEffect产生的副作用组成的链表。在 commit 阶段更新这些副作用。
  4. 判断组件是 初始化流程还是更新流程,如果初始化用 HooksDispatcherOnMount 对象,如果更新用 HooksDispatcherOnUpdate 对象
  5. 执行 Component ( props , secondArg ),里面每一个 hooks 也将依次执行。
  6. 函数组件执行完毕,将 hooks 赋值给 ContextOnlyDispatcher 对象

1、hooks 初始化——hooks 和 fiber 建立起关系

每一个hooks 初始化都会执行 mountWorkInProgressHook,将 hooks 和 fiber 建立起联系。

mountWorkInProgressHook

在这里插入图片描述

  • 函数组件对应 fiber 用 memorizedState 保存 hooks 对象
  • 每个 hooks 通过 next链表 建立关系

2、hooks 更新

双缓冲树 更新类似。参考: React 调和(Reconciler)原理理解

即:取出 workInProgres.alternate 里面对应的 hook ,然后根据之前的 hooks 复制一份,形成 新的 hooks 链表关系,进行调和更新。

问:React Hooks 为什么不能写在条件语句中?

答:在更新过程中,如果通过 if 条件语句,增加或者删除 hooks,在复用 hooks 过程中,会产生复用 hooks 状态和当前 hooks 不一致的问题。


状态派发——useState(useReducer)原理

useState 和 useReducer 原理大同小异,本质上都是触发更新的函数都是 dispatchAction。

执行useState()

1、mountState

在这里插入图片描述

  • state 会被当前 hooks 的 memoizedState 保存下来,每一个 useState 都会创建一个 queue 里面 保存了更新的信息

  • 每一个 useState 都会 创建一个更新函数 dispatchAction

  • 当前的 fiber 被 bind 绑定了固定的参数传入 dispatchAction 和 queue

  • 最后把 memoizedState,dispatch 返回 给开发者使用。

2、dispatchAction
在这里插入图片描述

  • 每一次调用 dispatchAction 都会先 创建一个 update ,然后把它 放入待更新 pending 队列中。
  • 判断如果当前的 fiber 正在更新,那么也就不需要再更新。
  • 反之,说明当前 fiber 没有更新任务,那么会拿出 上一次 state 和 这一次 state 进行对比
  • 如果相同,那么 直接退出更新
  • 如果不相同,那么 发起更新调度任务

3、 updateReducer

当再次执行useState的时候,会触发更新hooks逻辑,本质上调用的就是 updateReducer

在这里插入图片描述

  • 把待更新的队列 pendingQueue 拿出来,合并baseQueue,循环进行更新。
  • 循环更新的流程,得到最新的 state 。
  • 接下来就可以从 useState 中得到最新的值

处理副作用——useEffect(useLayoutEffect)原理

在 render 阶段,实际没有进行真正的 DOM 元素的增加,删除,React 把想要做的不同操作打成不同的 effectTag ,等到 commit 阶段统一处理这些副作用,包括 DOM 元素增删改,执行一些生命周期等

Hooks 中的 useEffect 和 useLayoutEffect 也是副作用。

1、初始化
在这里插入图片描述

  • mountWorkInProgressHook 产生一个 hooks ,并 和 fiber 建立起关系
  • 通过 pushEffect 创建一个 effect,并保存到当前 hooks 的 memoizedState 属性下。
  • pushEffect 除了创建一个 effect , 还有一个重要作用,就是如果存在多个 effect 或者 layoutEffect 会形成一个副作用链表,绑定在函数组件 fiber 的 updateQueue 上。独立形成链表结构,在 commit 阶段 统一处理和执行

2、更新
在这里插入图片描述

  • 判断 deps 项有没有 发生变化,如果没有发生变化,更新副作用链表
  • 如果发生变化,更新链表同时,打执行副作用的标签fiber => fiberEffectTag,hook => HookHasEffect。在 commit 阶段就会根据这些标签,重新执行副作用

EffectTag

React 会 用不同的 EffectTag 来标记不同的 effect

  • 对于useEffect 会用 HookPassive 标识符
  • 对于 useLayoutEffect 会用 HookLayout 的标识符

React 就是在 commit 阶段,通过标识符,证明是 useEffect 还是 useLayoutEffect ,接下来 React 会同步处理 useLayoutEffect ,异步处理 useEffect

  • 如果函数组件需要更新副作用,会标记 UpdateEffect,哪个effect 需要更新,看 hooks 上有没有 HookHasEffect标记,所以初始化或者 deps 不相等,就会给当前 hooks 标记上 HookHasEffect ,所以会执行组件的副作用钩子。

问:useEffect 和 useLayoutEffect 的区别?

  • useEffect 在渲染时是 异步执行,并且要等到浏览器将所有变化渲染到屏幕后才会被执行。
  • useLayoutEffect 在渲染时是 同步执行,其执行时机与 componentDidMount,componentDidUpdate 一致。在DOM更新完成后,里面的callback函数会立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制。

状态获取与缓存——useRef(useMemo)原理

1、对于 ref 处理

useRef :创建并维护一个 ref 原始对象。用于获取原生 DOM 或者组件实例,或者保存一些状态等。

1)初始化

  • 创建 ref对象
    在这里插入图片描述

2)更新

  • 取出复用 ref对象
    在这里插入图片描述

useRef 的好处:

useRef 可以创建出一个 ref 原始对象,只要组件没有销毁,ref 对象就一直存在,那么完全可以把一些不依赖于视图更新的数据储存到 ref 对象中。这样做的好处有两个:

  • 第一个能够直接修改数据,不会造成函数组件冗余的更新作用。
  • 第二个 useRef 保存数据,如果有 useEffect ,useMemo 引用 ref 对象中的数据,无须将 ref 对象添加成 dep 依赖项,因为 useRef 始终指向一个内存空间,所以这样一点好处是可以随时访问到变化后的值。

2、对于 memo 处理

useMemo:会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,但是 deps 中如果有一项改变,就会重新执行 create ,返回值作为新的值记录到 fiber 对象上。

1)初始化
在这里插入图片描述

  • 会执行第一个函数得到想要缓存的值,将 值缓存 到 hook 的 memoizedState 上。

2)更新
在这里插入图片描述

  • 对比两次的 dep 是否发生变化,如果没有发生变化,直接返回缓存值,如果发生变化,执行第一个参数函数,重新生成缓存值,缓存下来,供开发者使用。

总结

1、hooks对象、初始化和更新

2、状态派发

3、处理副作用

4、状态获取与缓存

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐