React Hooks 原理理解
Posted YuLong~W
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Hooks 原理理解相关的知识,希望对你有一定的参考价值。
文章目录
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
- hooks 内部通过 currentlyRenderingFiber 读取当前 fiber 信息。(
workInProgress
——正在调和更新函数组件对应的 fiber 树) - 用 memoizedState 保存 hooks 信息。
- 用 updateQueue 存放每个
useEffect
/useLayoutEffect
产生的副作用组成的链表。在 commit 阶段更新这些副作用。 - 判断组件是 初始化流程还是更新流程,如果初始化用
HooksDispatcherOnMount
对象,如果更新用HooksDispatcherOnUpdate
对象。 - 执行
Component ( props , secondArg )
,里面每一个 hooks 也将依次执行。 - 函数组件执行完毕,将 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、状态获取与缓存
以上是关于React Hooks 原理理解的主要内容,如果未能解决你的问题,请参考以下文章