React中的数据流管理

news/2025/1/26 15:12:58/文章来源:https://www.cnblogs.com/dtux/p/18691833

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:霜序

前言

💡 为什么数据流管理重要?
React 的核心思想为:UI=render(data) ,data 就是所谓的数据,render 是 React 提供的纯函数,所以 UI 展示完全由数据层决定。

在本文中,会简单介绍 React 中的数据流管理,从自身的 context 到三方库的 redux 的相关概念,以及 redux 附属内容丐版实现。

在正文之前,先简单介绍数据状态的概念。React 是利用可复用的组件来构建界面,组件本质上是有限状态机,能够记住当前组件的状态,根据不同的状态变化做出相关的操作。在React中,把这种状态定义为 state 。通过管理状态来实现对组件的管理,当 state 发生改变时,React 会自动去执行相应的操作。

而数据,它不仅指 server 层返回给前端的数据,React 中的状态也是一种数据。当数据改变时,我们需要改变状态去引发界面的变更。

React自身的数据流方案

基于Props的单向数据流

React 是自上而下的单向数据流,容器组件&展示组件是最常见的 React 组件设计方案。容器组件负责处理复杂的业务逻辑和数据,展示组件负责处理 UI 层。通常我们会把展示组件抽出来复用或者组件库的封装,容器组件自身通过 state 来管理状态,setState 更新状态,从而更新 UI ,通过 props 将自身的 state 传递给展示组件实现通信

file

对于简单的通信,基于 props 串联父子和兄弟组件是很灵活的。

但对于嵌套深数据流组件,A→B→C→D→E,A 的数据需要传递给 E 使用,那么我们需要在 B/C/D 的 props 都加上该数据,导致最为中间组件的 B/C/D 来说会引入一些不属于自己的属性

使用 Context API 维护全局状态

Context API 是 React 官方提供的一种组件树全局通信方式

Context 基于生产者-消费者模式,对应 React 中的三个概念: React.createContextProviderConsumer 。通过调用 createContext 创建出一组 ProviderProvider 作为数据的提供方,可以将数据下发给自身组件树中的任意层级的 Consumer ,而 Consumer 不仅能够读取到 Provider 下发的数据还能读取到这些数据后续的更新值

const defaultValue = {count: 0,increment: () => {}
};const ValueContext = React.createContext(defaultValue);<ValueContext.Provider value={this.state.contextState}><div className="App"><div>Count: {count}</div><ButtonContainer /><ValueContainer /></div>
</ValueContext.Provider><ValueContext.Consumer>{({ increment }) => (<button onClick={increment} className="button">increment</button>)}
</ValueContext.Consumer>

16.3之前的用法,16.3之后的createContext用法,useContext用法

Context工作流的简单图解:

file

在 v16.3 之前由于各种局限性不被推荐使用

  • 代码不够简单优雅:生产者需要定义 childContextTypesgetChildContext ,消费者需要定义 ChildTypes 才能够访问 this.context 访问到生产者提供的数据
  • 数据无法及时同步:类组件中可以使用 shouldComponentUpdate 返回 false 或者是 PureComponent ,后代组件都不会被更新,这违背了 Context 模式的设置,导致生产者和消费者之间不能及时同步

在 v16.3 之后的版本中做了对应的调整,即使组件的 shouldComponentUpdate 返回 false ,它仍然可以”穿透”组件继续向后代组件进行传播,更改了声明方式变得更加语义化,使得 Context 成为了一种可行的通信方案

但是 Context 的也是通过一个容器组件来管理状态的,但是 ConsumerProvider 是一一对应的,在项目复杂度高的时候,可能会出现多个 ProviderConsumer ,甚至一个 Consumer 需要对应多个 Provider 的情况

当某个组件的业务逻辑变得非常复杂时,代码会越写越多,因为我们只能够在组件内部去控制数据流,这样导致 Model 和 View 都在 View 层,业务逻辑和 UI 实现都在一块,难以维护

所以这个时候需要真正的数据流管理工具,从 UI 层完全抽离出来,只负责管理数据,让 React 只专注于 View 层的绘制

Redux

Redux 是 JS应用 的状态容器,提供可预测的状态管理

Redux 的三大原则

  • 单一数据源:整个应用的 state 都存储在一棵树上,并且这棵状态树只存在于唯一的 store 中
  • state 是只读的:对 state 的修改只有触发 action
  • 用纯函数执行修改:reducer 根据旧状态和传进来的 action 来生成一个新的 state (类似于 reduce 的思想,接受上一个 state 和当前项 action ,计算出来一个新值)

Redux工作流

file

不可变性( Immutability )

mutable 意为可改变的,immutability 意为用不可改变的

在JS的对象( object )和数组( array )默认都是 mutable,创建一个对象/数组都是可以改变内容

const obj = { name: 'FBB', age: 20 };
obj.name = 'shuangxu';const arr = [1,2,3];
arr[1] = 6;
arr.push('change');

改变对象或者数组,内存中的引用地址尚未改变,但是内容已经改变

如果想用不可变的方式来更新,代码必须复制原来的对象/数组,更新它的复制体

const obj = { info: { name: 'FBB', age: 20 }, phone: '177xxx' }
const cloneObj = { ...obj, info: { name: 'shuangxu' } }//浅拷贝、深拷贝

Redux期望所有的状态都采用不可变的方式。

react-redux

react-redux 是 Redux 提供的 react 绑定,辅助在 react 项目中使用 redux

它的 API 简单,包括一个组件 Provider 和一个高阶函数 connect

Provider

❓为什么 Provider 只传递一个 store ,被它包裹的组件都能够访问到 store 的数据呢?

Provider 做了些啥?

  • 创建一个 contextValue 包含 redux 传入的 store 和根据 store 创建出的 subscription ,发布订阅均为 subscription 做的
  • 通过 context 上下文把contextValue传递子组件

Connect

❓connect 做了什么事情讷?

使用容器组件通过 context 提供的 store ,并将 mapStateToPropsmapDispatchToProps 返回的 statedispatch 传递给 UI 组件

组件依赖 redux 的 state ,映射到容器组件的 props 中,state 改变时触发容器组件的 props 的改变,触发容器组件组件更新视图

const enhancer = connect(mapStateToProps, mapDispatchToProps)
enhancer(Component)

react-redux丐版实现

Provider

export const Provider = (props) => {const { store, children, context } = props;const contextValue = { store };const Context = context || ReactReduxContext;return <Context.Provider value={contextValue}>{children}</Context.Provider>
};

connect

import { useContext, useReducer } from "react";
import { ReactReduxContext } from "./ReactReduxContext";export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent
) => (props) => {const { ...wrapperProps } = props;const context = useContext(ReactReduxContext);const { store } = context; // 解构出storeconst state = store.getState(); // 拿到state//使用useReducer得到一个强制更新函数const [, forceComponentUpdateDispatch] = useReducer((count) => count + 1, 0);// 订阅state的变化,当state变化的时候执行回调store.subscribe(() => {forceComponentUpdateDispatch();});// 执行mapStateToProps和mapDispatchToPropsconst stateProps = mapStateToProps?.(state);const dispatchProps = mapDispatchToProps?.(store.dispatch);// 组装最终的propsconst actualChildProps = Object.assign({},stateProps,dispatchProps,wrapperProps);return <WrappedComponent {...actualChildProps} />;
};

redux Middleware

“It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.” – Dan Abramov

middleware 提供分类处理 action 的机会,在 middleware 中可以检查每一个 action ,挑选出特定类型的 action 做对应操作

file

middleware示例

打印日志

store.dispatch = (action) => {console.log("this state", store.getState());console.log(action);next(action);console.log("next state", store.getState());
};

监控错误

store.dispatch = (action) => {try {next(action);} catch (err) {console.log("catch---", err);}
};

二者合二为一

store.dispatch = (action) => {try {console.log("this state", store.getState());console.log(action);next(action);console.log("next state", store.getState());} catch (err) {console.log("catch---", err);}
};

提取 loggerMiddleware/catchMiddleware

const loggerMiddleware = (action) => {console.log("this state", store.getState());console.log("action", action);next(action);console.log("next state", store.getState());
};
const catchMiddleware = (action) => {try {loggerMiddleware(action);} catch (err) {console.error("错误报告: ", err);}
};
store.dispatch = catchMiddleware

catchMiddleware 中都写死了,调用 loggerMiddleware ,loggerMiddleware 中写死了 next(store.dispatch) ,需要灵活运用,让 middleware 接受 dispatch 参数

const loggerMiddleware = (next) => (action) => {console.log("this state", store.getState());console.log("action", action);next(action);console.log("next state", store.getState());
};
const catchMiddleware = (next) => (action) => {try {/*loggerMiddleware(action);*/next(action);} catch (err) {console.error("错误报告: ", err);}
};
/*loggerMiddleware 变成参数传进去*/
store.dispatch = catchMiddleware(loggerMiddleware(next));

middleware中接受一个store,就能够把上面的方法提取到单独的函数文件中

export const catchMiddleware = (store) => (next) => (action) => {try {next(action);} catch (err) {console.error("错误报告: ", err);}
};export const loggerMiddleware = (store) => (next) => (action) => {console.log("this state", store.getState());console.log("action", action);next(action);console.log("next state", store.getState());
};const logger = loggerMiddleware(store);
const exception = catchMiddleware(store);
store.dispatch = exception(logger(next));

每个 middleware 都需要接受 store 参数,继续优化这个调用函数

export const applyMiddleware = (middlewares) => {return (oldCreateStore) => {return (reducer, initState) => {//获得老的storeconst store = oldCreateStore(reducer, initState);//[catch, logger]const chain = middlewares.map((middleware) => middleware(store));let oldDispatch = store.dispatch;chain.reverse().forEach((middleware) => (oldDispatch = middleware(oldDispatch)));store.dispatch = oldDispatch;return store;};};
};const newStore = applyMiddleware([catchMiddleware, loggerMiddleware])(createStore
)(rootReducer);

Redux 提供了 applyMiddleware 来加载 middlewareapplyMiddleware 接受三个参数,middlewares 数组 / reduxcreateStore / reducer

export default function applyMiddleware(...middlewares) {return createStore => (reducer, ...args) => {//由createStore和reducer创建storeconst store = createStore(reducer, ...args) let dispatch = store.dispatchvar middlewareAPI = {getState: store.getState,dispatch: (action, ...args) => dispatch(action, ...args)}//把getState/dispatch传给middleware,//map让每个middleware获得了middlewareAPI参数//形成一个chain匿名函数数组[f1,f2,f3...fn]const chain = middlewares.map(middleware => middleware(middlewareAPI))//dispatch=f1(f2(f3(store.dispatch))),把所有  的middleware串联起来dispatch = compose(...chain)(store.dispatch)return {...store,dispatch}}
}

applyMiddleware 符合洋葱模型

file

总结

本文意在讲解 react 的数据流管理。从 react 本身的提供的数据流方式出发

  1. 基于 props 的单向数据流,串联父子和兄弟组件非常灵活,但是对于嵌套过深的组件,会使得中间组件都加上不需要的 props 数据
  2. 使用 Context 维护全局状态,介绍了 v16.3 之前、v16.3之后的hooks ,不同版本 context 的使用,以及 v16.3 之前版本的 context 的弊端。
  3. 引入 redux ,第三方的状态容器,以及 react-redux API ( Provider/connect )分析与丐版实现,最后介绍了 redux 强大的中间件是如何重写 dispatch 方法

参考连接

  • 对 React 状态管理的理解及方案对比
  • 聊一聊我对 React Context 的理解以及应用
  • redux middleware 详解
  • 手写 react-redux

最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

  • 大数据分布式任务调度系统——Taier
  • 轻量级的 Web IDE UI 框架——Molecule
  • 针对大数据领域的 SQL Parser 项目——dt-sql-parser
  • 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
  • 一个速度更快、配置更灵活、使用更简单的模块打包器——ko
  • 一个针对 antd 的组件测试工具库——ant-design-testing

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

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

相关文章

代码随想录算法训练营第9天 | 151.翻转字符串里的单词、右旋字符串、28. 实现 strStr()、重复的子字符串、字符串:总结篇、双指针回顾

一、刷题部分 1.1 151.翻转字符串里的单词原文链接:代码随想录 题目链接:151. 反转字符串中的单词 - 力扣(LeetCode)1.1.1 题目描述 给你一个字符串 s ,请你反转字符串中 单词 的顺序。 单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。…

《CPython Internals》阅读笔记:p360-p377

《CPython Internals》学习第 20 天,p360-p377 总结,总计 18 页。 一、技术总结 1.seaborn 可视化工具。 2.dtrace (1)安装 sudo apt-get install systemtap-sdt-dev(2)编译 ./configure --with-dtrace make clean make3.snakeviz 适用于 cProfile 的可视化工具。 二、英语总…

goland2024如何安装?附安装包和激活方式

前言 大家好,我是小徐啊。goland是我们开发Go语言时的常用的开发工具,功能强大,今天,小徐就来介绍下如何安装和获取激活方式。文末附获取方式。 如何安装和激活goland 首先,我们双击下goland2024安装包,开始安装。然后,我们点击下运行按钮。然后,我们点击下一步按钮。然…

AI实现个人阅读网页插件

背景 日常工作中经常需要浏览各种Github项目或者一些网页,逐字逐句看总觉得不够效率,如果在每次打开一个新的页面时就能够掌握大概的内容,然后再决定是否深入阅读就方便多了。有了这样的念头后,总想自己开发一款简单的浏览器插件,但是始终没有提上日程。目前随着AI编程工具…

RabbitMQ之Exchange、Queue参数详解

1.先来介绍RabbitMQ中的成员Producer(生产者): 将消息发送到Exchange Exchange(交换器):将从生产者接收到的消息路由到Queue Queue(队列):存放供消费者消费的消息 BindingKey(绑定键):建立Exchange与Queue之间的关系(个人看作是一种规则,也就是Exchange将什么样的消息路由…

[日记]轻量回测框架 Backtesting.py 与 Streamlit集成

找到一个目前觉得比较轻量级的框架,结构简单易用,几行代码搞定即可搞定回测。 对于回测结果提供可视化的找到一个目前觉得比较轻量级的框架,结构简单易用,几行代码搞定即可搞定回测。 对于回测结果提供可视化的图表分析。如下图:同时提供常用的收益和风险指标数据作为量化…

【开源】Pi-hole控制面板:深入解析你的网络流量和广告拦截

今天要给大家介绍一个非常实用的开源项目——Pi-hole。这是一款基于树莓派的全能广告屏蔽助手,能够在不安装任何客户端软件的前提下为设备提供网络内容屏蔽服务,非常轻量易用。Pi-hole的主要功能: 1、 全网广告拦截: Pi-hole 充当 DNS 污水坑,阻止网络上所有设备上不需要的…

Rust多线程中安全的使用变量

在Rust语言中,一个既引人入胜又可能带来挑战的特性是闭包如何从其所在环境中捕获变量,尤其是在涉及多线程编程的情境下。 如果尝试在不使用move关键字的情况下创建新线程并传递数据至闭包内,编译器将很可能返回一系列与生命周期、借用规则及所有权相关的复杂错误信息。 不过…

ARC_069 D - Menagerie 题解

atcoder 一道很有意思的模拟题啊。 思路很重要。 首先,我们只要知道连续两只动物的身份,就可以根据 \(s\) 推出所有动物的身份。 不妨假设我们知道第一只和第二只动物的身份,一共有几种情况呢? 用 \(1\) 代表羊,\(0\) 代表狼。 那么,共有 \(2^2=4\) 种情况,分别为: 00 …

『学习笔记』二分算法

今天记录二分知识点。 二分是一个简单清晰,实用性强的算法。 也是本人最喜欢的算法之一。 先给出二分模板吧!int l = 1, r = n;//初始值,根据情况而定while (l + 1 < r) {int mid = (l + r) >> 1;if (check(mid)) l = mid;// check函数判断左半部分是否不符合,更新…

回家之难难于蜀道难

回家难 之难于蜀道难 (仿写李白蜀道难)噫吁嚱,困乎难乎,回家之途,难于上班路。 盘古及女娲,开天辟地捏人烟,尓来文明已万年,难解归家争吵事。 游子无钱难上路,漂留外地护空城。 千思万想定下来,踏上归途望团年。 上有爸妈在老家,下有孩童八九岁。 列车无票不得行,驱…

MAC|Edge——下载视频

解码错误解码错误指的是当前音/视频帧与浏览器不兼容,可以尝试以下方式:1.chrome/edge 浏览器打开chrome://flags,搜索 Hardware-accelerated video decode,选择 disabled2.如果解码错误仍然存在,请对视频进行转码处理,以修复问题帧3.firefox浏览器请打开about:support,…