【React Hooks原理 - useReducer】

概述

众所周知useState是基于useReducer来实现状态更新的,在前面我们介绍过useState的原理,所以在这里介绍下useReducer。本文主要从基础使用入手,再进一步从源码来看useReducer实现原理两个方面来介绍useReducer这个Hook。由于本文省略了部分之前提及的代码和流程,为了避免冗余,有兴趣的可以优先浏览这篇文章:【React Hooks原理 - useState】

基础使用

我们都知道useReducer 是通过传入一个 reducer 函数和初始值,返回state和一个更新 dispatcher,通过返回的dispatcher来触发 action 以更新 state,以此来进行状态管理的Hook 。下面我们从代码来看他的使用。

export function useReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: I => S,
): [S, Dispatch<A>] 

从定义来看,其接收三个参数和我们上面的描述所对应:

  • reducer: 进行状态更新逻辑的函数
  • 初始值:可以是任意类型的值,也可以是返回值的函数
  • init: 可选,传递初始化函数本身,可以避免组件更新渲染而重新执行初始化函数

这里对第三个参数init函数,进行补充说明:

function createInitialState(username) {// ...
}function TodoList({ username }) {const [state, dispatch] = useReducer(reducer, createInitialState(username));// ...

上面代码中虽然 createInitialState(username) 的返回值只用于初次渲染,但是在每一次渲染的时候都会被调用。如果它创建了比较大的数组或者执行了昂贵的计算就会浪费性能。所以为了避免重复执行的问题,提供了init参数。

function createInitialState(username) {// ...
}function TodoList({ username }) {const [state, dispatch] = useReducer(reducer, username, createInitialState);// ...

需要注意的是你传入的参数是 createInitialState 这个 函数自身,而不是执行 createInitialState() 后的返回值。这样传参就可以保证初始化函数不会再次运行。当传入第三个函数时,React会将第二个参username作为createInitialState函数的入参传递,并且只会在初次渲染时执行。

会重复执行的原因是React内部使用Object.is判断值是否改变,而组件每次渲染都会产生一个新的值,所以也可以通过useMemo来缓存createInitialState(username)结果来避免,但React推荐以第三个参数来处理

所以使用reducer进行状态管理的话,需要自己手动写状态更新规则reduer,一下是官网中的一个简单demo:

import { useReducer } from 'react';function reducer(state, action) {if (action.type === 'incremented_age') {return {age: state.age + 1};}throw Error('Unknown action.');
}export default function Counter() {const [state, dispatch] = useReducer(reducer, { age: 42 });return (<><button onClick={() => {dispatch({ type: 'incremented_age' })}}>Increment age</button><p>Hello! You are {state.age}.</p></>);
}

源码解析

同其他Hooks一样(useContext除外),useReducer也分为mount、update阶段并通过dispatcher根据不同阶段执行不同函数。

export function useReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: I => S,
): [S, Dispatch<A>] {const dispatcher = resolveDispatcher();return dispatcher.useReducer(reducer, initialArg, init);
}

下面我们仍然分别介绍useReducer在mount和update阶段分别做了什么。

mount挂载时

相比经过前面几篇文章的学习我们对代码已经相对熟悉了,所以下面会省略部门代码的解释(因为在其他两篇已经解释过,这里不再冗余)。在mountReducer函数中主要做了以下功能(详情可以看代码注释):

  • 创建并挂载当前fiber节点的hook链表
  • 处理初始化函数(如果有)
  • 保存初始值,用于对比和更新
  • 创建当前hook的更新队列(循环链表)
  • 绑定dispatcher用于触发更新reducer
  • 返回包含[value, setValue]
function mountReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: (I) => S
): [S, Dispatch<A>] {// 创建初始hook绑定fiber的memoizedState属性const hook = mountWorkInProgressHook();let initialState;if (init !== undefined) {// 传递了初始化函数则使用第二个值为入参并执行initialState = init(initialArg);} else {initialState = ((initialArg: any): S);}// 缓存初始值,和更新的起始值hook.memoizedState = hook.baseState = initialState;// 创建当前hook的更新队列(循环链表),并将reducer、初始值绑定到更新对象update中const queue: UpdateQueue<S, A> = {pending: null,lanes: NoLanes,dispatch: null,lastRenderedReducer: reducer,lastRenderedState: (initialState: any),};hook.queue = queue;// 绑定dispatcher为dispatchReducerAction,当通过set函数更新时,调用该函数const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(null,currentlyRenderingFiber,queue): any));// 返回包含当前state和setState的dispatcher数组return [hook.memoizedState, dispatch];
}

首次挂载时,state就只执行了这个函数完成了Function Component -> FIber -> Hook -> update 之间的连接和初始化

这里说的mount、update指的是组件首次渲染和更新渲染时state的操作,并不是执行set函数之后更新state操作

update更新时

在更新渲染时,通过dispatcher派发,最终执行updateReducer函数,其中updateWorkInProgressHook使用复用hook进行性能优化,updateReducerImpl进行更新队列的处理以及状态更新

function updateReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: (I) => S
): [S, Dispatch<A>] {// 复用hook避免重新创建,优先复用workInprogress.nextHook,没有则克隆页面显示的current.nextHook,都没有则抛出异常const hook = updateWorkInProgressHook();return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}

updateWorkInProgressHookupdateReducerImpl在介绍useState时都详细介绍过,所以这里简单说明了其功能:

updateReducerImpl函数:

/**** hook:指向当前 Fiber 节点正在处理的具体 Hook 实例(即 Hook 链表中的一个节点)。* current:指向当前 Fiber 节点中对应的 Hook 实例的当前状态(即已渲染到页面上的状态)。*/
function updateReducerImpl<S, A>(hook: Hook,current: Hook,reducer: (S, A) => S
): [S, Dispatch<A>] {// 获取当前指向hook的更新队列,以及绑定reducer更新函数const queue = hook.queue;queue.lastRenderedReducer = reducer;let baseQueue = hook.baseQueue;// 如果有上次渲染未处理的更新队列const pendingQueue = queue.pending;if (pendingQueue !== null) {// 有上次为处理的更新以及本次也有需要处理的更新,则将两个更新队列合并,否则将上次未处理的赋值给更新队列等待本次渲染更新if (baseQueue !== null) {const baseFirst = baseQueue.next;const pendingFirst = pendingQueue.next;baseQueue.next = pendingFirst;pendingQueue.next = baseFirst;}current.baseQueue = baseQueue = pendingQueue;queue.pending = null;}// 如果本次没有更新队列,则更新memoizedState为baseStateconst baseState = hook.baseState;if (baseQueue === null) {hook.memoizedState = baseState;} else {// 更新队列有状态需要更新const first = baseQueue.next;let newState = baseState;let newBaseState = null;let newBaseQueueFirst = null;let newBaseQueueLast: Update<S, A> | null = null;let update = first;let didReadFromEntangledAsyncAction = false;do {const updateLane = removeLanes(update.lane, OffscreenLane);const isHiddenUpdate = updateLane !== update.lane;const shouldSkipUpdate = isHiddenUpdate? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane): !isSubsetOfLanes(renderLanes, updateLane);// 根据优先级判断当前是否需要跳过更新,并保存在newBaseQueueLast中在下次渲染时更新,然后调用markSkippedUpdateLanes跳过本次更新if (shouldSkipUpdate) {...} else {const revertLane = update.revertLane;// 根据优先级判断当前是否需要跳过更新,并保存在newBaseQueueLast中在下次渲染时更新if (!enableAsyncActions || revertLane === NoLane) {...} else {// 将符合本次更新条件的状态保存在update链表中,等待更新if (isSubsetOfLanes(renderLanes, revertLane)) {update = update.next;if (revertLane === peekEntangledActionLane()) {didReadFromEntangledAsyncAction = true;}continue;} else {// 不符合的保存在newBaseQueueLast等待下次渲染时候更新...}}// 开始更新,如果比较紧急的状态更新则直接处理,否则通过reducer处理const action = update.action;if (update.hasEagerState) {// If this update is a state update (not a reducer) and was processed eagerly,// we can use the eagerly computed statenewState = ((update.eagerState: any): S);} else {newState = reducer(newState, action);}}update = update.next;} while (update !== null && update !== first);// 遍历本次更新队列之后,判断是否有跳过的更新,如果有则保存在newBaseState中,等待下次渲染时更新if (newBaseQueueLast === null) {newBaseState = newState;} else {newBaseQueueLast.next = (newBaseQueueFirst: any);}// 判断上一次的状态和reducer更新之后的状态是否一致,发生变化则通过markWorkInProgressReceivedUpdate函数给当前fiber打上update标签if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();if (didReadFromEntangledAsyncAction) {const entangledActionThenable = peekEntangledActionThenable();if (entangledActionThenable !== null) {throw entangledActionThenable;}}}// 将本次新的state保存在memoizedState中hook.memoizedState = newState;// 保存下次更新的初始值,如果本次没有跳过更新,该值为更新后通过reducer或者eagerState计算的新值,有跳过的更新则会本次更新前原来的初始值hook.baseState = newBaseState;// 将本次跳过的更新保存在baseQueue更新队列中中,下次渲染时更新hook.baseQueue = newBaseQueueLast;queue.lastRenderedState = newState;}// 没有状态更新时,将当前队列优先级设置为默认if (baseQueue === null) {queue.lanes = NoLanes;}const dispatch: Dispatch<A> = (queue.dispatch: any);return [hook.memoizedState, dispatch];
}
  • 处理更新队列,并发起调度申请
  • 处理跳过的更新,追加到当前更新队列
  • 遍历更新队列,根据优先级判断是否跳过优先级低的任务
  • 符合当前更新的任务,通过进行状态更新,在set函数如果提前计算则直接使用计算后值,没有则通过reducer计算状态
  • 通过Object.is判断值是否变化,进行跳过更新步骤
  • 返回计算后的[hook.memoizedState, dispatch]

至此我们将组件在首次渲染和更新渲染中对于state的处理以及梳理了,下面介绍下当发生交互触发set函数进行状态更新的原理。

触发set更新状态

通过mountReducer介绍我们知道暴露的set函数其实就是通过bind绑定dispatchReducerAction的一个dispatcher,所以我们实际执行的是dispatchReducerAction这个函数

function dispatchReducerAction<S, A>(fiber: Fiber,queue: UpdateQueue<S, A>,action: A
): void {
// 获取本次更新优先级const lane = requestUpdateLane(fiber);// 创建更新任务const update: Update<S, A> = {lane,revertLane: NoLane,action,hasEagerState: false,eagerState: null,next: (null: any),};
// 判断是否是渲染阶段的更新if (isRenderPhaseUpdate(fiber)) {// 直接添加到本次渲染队列后面,当渲染完成之后立即执行,即在本次渲染之后就能看到更新结果enqueueRenderPhaseUpdate(queue, update);} else {// 通过事件触发`set`函数进行更新,则将该更新任务添加到更新队列enqueueUpdate中,等待下次渲染更新,并获取当前渲染组件的根fiber节点const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);if (root !== null) {// 发起申请调度请求scheduleUpdateOnFiber(root, fiber, lane);entangleTransitionUpdate(root, queue, lane);}}
}

从代码能得知在dispatchReducerAction中就是创建本身更新任务然后添加到更新队列中,并发起调度申请。具体步骤如下:

  • 获取本次更新的优先级
  • 创建本次更新任务
  • 判断本次更新是否是渲染阶段直接发起的,如果是则直接将更新任务添加到当前更新队列中,当本次渲染完成之后会立即执行,即然本次渲染结束后也能看到更新后的状态。如果是通过事件触发,则通过enqueueConcurrentHookUpdate函数将更新任务添加到下次更新队列中等下下次渲染更新,然后发起调度申请,等待更新。

enqueueConcurrentHookUpdate函数如下,就是将本次更新任务update添加到下次更新队列中EnqueueUpdate中,并返回当前更新fiber
的root节点,以便发起调度。

export function enqueueConcurrentHookUpdate<S, A>(fiber: Fiber,queue: HookQueue<S, A>,update: HookUpdate<S, A>,lane: Lane
): FiberRoot | null {const concurrentQueue: ConcurrentQueue = (queue: any);const concurrentUpdate: ConcurrentUpdate = (update: any);enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);return getRootForUpdatedFiber(fiber);
}

dispatchReducerAction函数中有个这个判断if (isRenderPhaseUpdate(fiber))用于将决定当前更新任务的执行时机,里面提到了当前是否是渲染阶段进行的更新,可能会有些疑惑,所以在这里举个例,简单说明一下:

function MyComponent() {const [count, setCount] = useState(0);function handleClick() {setCount(count + 1);  // 在事件处理器中触发的更新}if (count > 0) {setCount(count + 1);  // 在渲染阶段中触发的更新}return <button onClick={handleClick}>{count}</button>;
}

如上诉代码在组件中直接执行set函数就是在渲染阶段执行,因为每次渲染执行该函数组件时就会调用set函数更新状态,即这种场景下isRenderPhaseUpdate(fiber) === true,在页面渲染完成之后显示的是更新后的值,而要通过点击按钮触发更新的set任务是在下一次渲染完成之后触发更新的。

useReducer和useState的区别

我们都可以useState是基于useReducer来实现状态管理的,主要区别整理了一下几点:

  • 使用参数:useState接收一个参数,可以是值或者函数。useReducer接收三个参数reducer,state, ?:init
  • useState用于简单数据管理,一般在组件内部顶层定义单个状态,而useReducer用于复杂状态管理,可以将手动更新规则封装在组件外部,可以用于整个项目的状态管理。比如可以通过useReducer + useContext代替Redux进行应用间状态共享
  • useState状态是覆盖式的,所以通过state.key修改无法响应。而useReducer通常是累加式的return {...oldState, ...newState}

利用useReducer实现useState

先看代码demo:

function useState(initialState) {return useReducer(basicStateReducer, initialState);
}// React会自动保存当前的state并作为第一个参数传递给reducer
function basicStateReducer(state, action) {return typeof action === 'function' ? action(state) : action;
}
import React, { useState } from 'react';function Counter() {const [count, setCount] = useState(0);return (<div><p>Count: {count}</p><button onClick={() => setCount(count + 1)}>Increment</button></div>);
}

如上面例子所示的流程如下:

第一次渲染:

  • useState(0) 调用 useReducer,basicStateReducer 接受初始状态 0 并返回 [0, dispatch]。
  • dispatch 是内部由 useReducer 生成的函数,用于处理状态更新。

调用 setCount:

  • 调用 setCount(count + 1) 会触发 dispatch,其中 action 是 count + 1,即 1。
  • basicStateReducer 接受当前状态 0 和 action 1,直接返回 1 作为新的状态。

函数式更新:

  • 调用 setCount(prevCount => prevCount + 1) 会触发 dispatch,其中 action 是一个函数 prevCount => prevCount + 1。
  • basicStateReducer 调用这个函数,并传入当前状态 1,函数返回 2 作为新的状态。

总结

总结一下useReduer(useState也大致一样,毕竟useState也是基于reducer来的)在React核心包中主要就在mount、update阶段进行状态初始化和hook、更新队列的创建挂载,然后会一个触发更新的dispatcher,然后当调用该dispatcher时会创建一个更新任务等待Scheduler调度,当之前该更新任务时会在Reconciler协调中进行新的fiber树构造,然后进入update阶段会计算新值,并根据新旧值对比判断是否要更新。流程可以理解为: mount -> set更新 -> 新fiber构造 -> update渲染更新 当然这个流程比较粗糙,这里只是解释下本文提到过的几个点的执行时机和顺序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/44975.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Google 面试:从忐忑不安到意外的友善体验

一、面试前的忐忑不安 站于Google门前&#xff0c;犹如面临生死抉择&#xff0c;心脏如擂鼓般狂跳不止。我手中紧握着精心准备的简历&#xff0c;心中忐忑不安。Google&#xff0c;这一科技巨头的名字&#xff0c;对我而言&#xff0c;既是理想的彼岸&#xff0c;亦是恐惧的深…

Ozon俄罗斯哪些产品热销中?Ozon7月市场热卖趋势放送

Ozon俄罗斯哪些产品热销工具&#xff1a;D。DDqbt。COm/74rD 据Ozon数据&#xff0c;2023年&#xff0c;在自提服务方面&#xff0c;Ozon投资了100亿扩展自提网络&#xff0c;自提点数量激增至超过5万个&#xff0c;是之前的2.6倍。 物流基础设施方面&#xff0c;Ozon在仓库建…

【vueUse库Reactivity模块各函数简介及使用方法--上篇】

vueUse库是一个专门为Vue打造的工具库,提供了丰富的功能,包括监听页面元素的各种行为以及调用浏览器提供的各种能力等。其中的Browser模块包含了一些实用的函数,以下是这些函数的简介和使用方法: vueUse库Sensors模块各函数简介及使用方法 vueUseReactivity函数1. compute…

树莓派4B_OpenCv学习笔记19:OpenCV舵机云台物体追踪

今日继续学习树莓派4B 4G&#xff1a;&#xff08;Raspberry Pi&#xff0c;简称RPi或RasPi&#xff09; 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: Opencv 版本是4.5.1&#xff1a; Python 版本3.7.3&#xff1a; ​​ 今日学习&#xff1…

【数据结构】排序——快速排序

前言 本篇博客我们继续介绍一种排序——快速排序&#xff0c;让我们看看快速排序是怎么实现的 &#x1f493; 个人主页&#xff1a;小张同学zkf ⏩ 文章专栏&#xff1a;数据结构 若有问题 评论区见&#x1f4dd; &#x1f389;欢迎大家点赞&#x1f44d;收藏⭐文章 ​ 目录 …

前端JS特效第30波:jquery图片列表按顺序分类排列图片组效果

jquery图片列表按顺序分类排列图片组效果&#xff0c;先来看看效果&#xff1a; 部分核心的代码如下&#xff1a; <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> &…

Profibus协议转Profinet协议网关模块连接智能电表通讯案例

一、背景 在工业自动化领域&#xff0c;Profibus协议和Profinet协议是两种常见的工业通讯协议&#xff0c;而连接智能电表需要用到这两种协议之间的网关模块。本文将通过一个实际案例&#xff0c;详细介绍如何使用Profibus转Profinet模块&#xff08;XD-PNPBM20&#xff09;实…

vue2 实现原生 WebSocket

原生WebSocket&#xff1a; new WebSocket WebSocket | ThinkTS官网 export default {data() {return {socket: null};},created() {// 1. 创建 WebSocket 实例this.socket new WebSocket(ws://localhost:3000);// 2. 监听 WebSocket 连接打开事件this.socket.onopen () &g…

MySQL常见的几种索引类型及对应的应用场景

MySQL 提供了多种索引类型&#xff0c;每种索引类型都有其特定的应用场景和优势。以下是 MySQL 中常见的几种索引类型及其具体应用场景&#xff1a; 1. B-Tree 索引 特点&#xff1a; B-Tree&#xff08;Balanced Tree&#xff0c;平衡树&#xff09;是 MySQL 的默认索引类型…

启航IT之旅:为新生绘制的学习路线图

随着七月的热浪悄悄席卷而来&#xff0c;各地高考成绩陆续放榜&#xff0c;对于刚迈过高考这座独木桥的你们&#xff0c;这不仅仅是一个故事的终章&#xff0c;更是另一段冒险的序曲。特别是那些心中有一团IT火焰燃烧的少年们&#xff0c;暑假的钟声已经敲响&#xff0c;是时候…

坑3.上传图片(阿里云空间,oss验证)(未验证)

笔记 20240710 未验证&#xff0c;现在还没有阿里云空间&#xff0c;等买个sit环境就可以验证一下。 前端 页面 <!--页面--> <el-form-item label"优惠券图片" prop"couponImg"><single-upload v-model"dataForm.couponImg"&g…

2493-04A-6 同轴连接器

型号简介 2493-04A-6是Southwest Microwave的连接器。该连接器是一种端子连接器&#xff0c;采用 1.0 毫米插头&#xff08;公头&#xff09;进行连接。它由多个部件组成&#xff0c;包括过渡块、接地板、螺纹夹紧板、发射针、冷板、底座、电路板和外壳等。 型号特点 外壳&…

【数据结构】深入理解哈希及其底层数据结构

目录 一、unordered系列关联式容器 二、底层结构 2.1 哈希的概念 2.2 哈希冲突&#xff08;哈希碰撞&#xff09; 2.3 哈希函数 2.4 哈希冲突处理 2.4.1 闭散列&#xff08;开放定址法&#xff09; 2.4.1.1 代码实现&#xff1a; 2.4.2 开散列&#xff08;链地址法&…

Visual Studio 2022 安装及使用

一、下载及安装 VS 官网&#xff1a;Visual Studio: IDE and Code Editor for Software Developers and Teams 下载免费的社区版 得到一个.exe文件 右键安装 选择C开发&#xff0c;并修改安装位置 等待安装 点击启动 二、VS的使用 1.创建项目 打开VS&#xff0c;点击创建新项…

Python爬虫教程第6篇-使用session发起请求

为什么要使用session 前面介绍了如何使用reqesuts发起请求&#xff0c;今天介绍如何使用session发起请求。session简单理解就是一种会话机制&#xff0c;在浏览器中我们登录完之后&#xff0c;后面再请求服务数据都不需要再登录了&#xff0c;以为Cookie里已经保存了你的会话状…

【Cesium开发实战】火灾疏散功能的实现,可设置火源点、疏散路径、疏散人数

Cesium有很多很强大的功能&#xff0c;可以在地球上实现很多炫酷的3D效果。今天给大家分享一个可自定义的火灾疏散人群的功能。 1.话不多说&#xff0c;先展示 火灾疏散模拟 2.设计思路 根据项目需求要求&#xff0c;可设置火源点、绘制逃生路线、可设置逃生人数。所以点击火…

荷兰花海元宇宙的探索

在数字科技日新月异的今天&#xff0c;我们有幸见证了一个全新的概念——元宇宙的诞生。元宇宙是一个虚拟世界&#xff0c;它通过高科技手段&#xff0c;将现实世界的各种元素和场景数字化&#xff0c;使人们能够在虚拟世界中体验现实世界的生活。而在这个虚拟世界中&#xff0…

java设计模式(十六)职责链模式(Chain of Responsibility Pattern)

1、模式介绍&#xff1a; 职责链模式是一种行为设计模式&#xff0c;其中多个对象按顺序处理请求&#xff0c;直到其中一个对象能够处理请求为止。请求沿着链传递&#xff0c;直到有一个对象处理它为止。 2、应用场景&#xff1a; 职责链模式适用于以下场景&#xff1a;请求…

初学51单片机之UART串口通信

CSDN其他博主的博文&#xff08;自用&#xff09;嵌入式学习笔记9-51单片机UART串口通信_51uart串口通讯-CSDN博客 CSDN其他博主的博文写的蛮好&#xff0c;如果你想了解51单片机UART串口可以点进去看看&#xff1a; UART全称Universal Asynchronous Receiver/Transmitter即通…