React中的数据流管理
我们是袋鼠云数栈 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 传递给展示组件实现通信
对于简单的通信,基于 props
串联父子和兄弟组件是很灵活的。
但对于嵌套深数据流组件,A→B→C→D→E,A 的数据需要传递给 E 使用,那么我们需要在 B/C/D 的 props
都加上该数据,导致最为中间组件的 B/C/D 来说会引入一些不属于自己的属性
使用 Context API 维护全局状态
Context API 是 React 官方提供的一种组件树全局通信方式
Context
基于生产者-消费者模式,对应 React 中的三个概念: React.createContext 、 Provider、 Consumer 。通过调用 createContext
创建出一组 Provider
。Provider
作为数据的提供方,可以将数据下发给自身组件树中的任意层级的 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工作流的简单图解:
在 v16.3 之前由于各种局限性不被推荐使用
- 代码不够简单优雅:生产者需要定义
childContextTypes
和getChildContext
,消费者需要定义ChildTypes
才能够访问this.context
访问到生产者提供的数据 - 数据无法及时同步:类组件中可以使用
shouldComponentUpdate
返回 false 或者是PureComponent
,后代组件都不会被更新,这违背了 Context 模式的设置,导致生产者和消费者之间不能及时同步
在 v16.3 之后的版本中做了对应的调整,即使组件的 shouldComponentUpdate
返回 false ,它仍然可以”穿透”组件继续向后代组件进行传播,更改了声明方式变得更加语义化,使得 Context 成为了一种可行的通信方案
但是 Context 的也是通过一个容器组件来管理状态的,但是 Consumer
和 Provider
是一一对应的,在项目复杂度高的时候,可能会出现多个 Provider
和Consumer
,甚至一个 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工作流
不可变性( 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
,并将 mapStateToProps
和 mapDispatchToProps
返回的 state
和 dispatch
传递给 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; // 解构出store
const state = store.getState(); // 拿到state
//使用useReducer得到一个强制更新函数
const [, forceComponentUpdateDispatch] = useReducer((count) => count + 1, 0);
// 订阅state的变化,当state变化的时候执行回调
store.subscribe(() => {
forceComponentUpdateDispatch();
});
// 执行mapStateToProps和mapDispatchToProps
const stateProps = mapStateToProps?.(state);
const dispatchProps = mapDispatchToProps?.(store.dispatch);
// 组装最终的props
const 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
做对应操作
打印日志
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) => {
//获得老的store
const 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
来加载 middleware
,applyMiddleware
接受三个参数,middlewares
数组 / redux
的 createStore
/ reducer
export default function applyMiddleware(...middlewares) {
return createStore => (reducer, ...args) => {
//由createStore和reducer创建store
const store = createStore(reducer, ...args)
let dispatch = store.dispatch
var 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 符合洋葱模型
总结
本文意在讲解 react 的数据流管理。从 react 本身的提供的数据流方式出发
- 基于
props
的单向数据流,串联父子和兄弟组件非常灵活,但是对于嵌套过深的组件,会使得中间组件都加上不需要的props
数据 - 使用 Context 维护全局状态,介绍了 v16.3 之前、v16.3之后的hooks ,不同版本
context
的使用,以及 v16.3 之前版本的context
的弊端。 - 引入 redux ,第三方的状态容器,以及 react-redux API ( Provider/connect )分析与丐版实现,最后介绍了 redux 强大的中间件是如何重写 dispatch 方法
参考连接
最后
欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star
- 大数据分布式任务调度系统——Taier
- 轻量级的 Web IDE UI 框架——Molecule
- 针对大数据领域的 SQL Parser 项目——dt-sql-parser
- 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
- 一个速度更快、配置更灵活、使用更简单的模块打包器——ko
- 一个针对 antd 的组件测试工具库——ant-design-testing
React中的数据流管理的更多相关文章
- React 回忆录(四)React 中的状态管理
Hi 各位,欢迎来到 React 回忆录!
- 浅谈React数据流管理
引言:为什么数据流管理如此重要?react的核心思想就是:UI=render(data),data就是我们说的数据流,render是react提供的纯函数,所以用户界面的展示完全取决于数据层.这篇文章 ...
- react --- React中state和props分别是什么?
props React的核心思想就是组件化思想,页面会被切分成一些独立的.可复用的组件. 组件从概念上看就是一个函数,可以接受一个参数作为输入值,这个参数就是props,所以可以把props理解为从外 ...
- React中state和props分别是什么?
整理一下React中关于state和props的知识点. 在任何应用中,数据都是必不可少的.我们需要直接的改变页面上一块的区域来使得视图的刷新,或者间接地改变其他地方的数据.React的数据是自顶向下 ...
- React中组件间通信的方式
React中组件间通信的方式 React中组件间通信包括父子组件.兄弟组件.隔代组件.非嵌套组件之间通信. Props props适用于父子组件的通信,props以单向数据流的形式可以很好的完成父子组 ...
- react实战系列 —— React 中的表单和路由的原理
其他章节请看: react实战 系列 React 中的表单和路由的原理 React 中的表单是否简单好用,受控组件和非受控是指什么? React 中的路由原理是什么,如何更好的理解 React 应用的 ...
- Immutable 详解及 React 中实践
本文转自:https://github.com/camsong/blog/issues/3 Shared mutable state is the root of all evil(共享的可变状态是万 ...
- React中父组件与子组件之间的数据传递和标准化的思考
React中父组件与子组件之间的数据传递的的实现大家都可以轻易做到,但对比很多人的实现方法,总是会有或多或少的差异.在一个团队中,这种实现的差异体现了每个人各自的理解的不同,但是反过来思考,一个团队用 ...
- React中的路由系统
React中的路由系统 提起路由,首先想到的就是 ASPNET MVC 里面的路由系统--通过事先定义一组路由规则,程序运行时就能自动根据我们输入的URL来返回相对应的页面.前端中的路由与之类似,前端 ...
- React 深入系列1:React 中的元素、组件、实例和节点
文:徐超,<React进阶之路>作者 授权发布,转载请注明作者及出处 React 深入系列,深入讲解了React中的重点概念.特性和模式等,旨在帮助大家加深对React的理解,以及在项目中 ...
随机推荐
- 【一步步开发AI运动小程序】八、利用body-calc进行姿态识别
随着人工智能技术的不断发展,阿里体育等IT大厂,推出的"乐动力"."天天跳绳"AI运动APP,让云上运动会.线上运动会.健身打卡.AI体育指导等概念空前火热.那 ...
- ES6 延展操作符
延展操作符(Spread operator) 延展操作符 = ...可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开,还可以在构造对象时,将对象表达式按key-value的方式展 ...
- SpringMVC-Mybatis-Maven项目整合
上次不知道为什么,把写好的系列文章都搞成一样了.结果,五篇文章,都变成了最后一篇文章. 悲剧,好吧,只好重新写了. 这系列文章写的是SpringMVC-Mybatis-Maven项目整合.说白了,其实 ...
- Java线程池Executors
一 简述 线程池,作为一个管理一组同构工作线程的资源.接受提交的任务,利用线程池中的线程进行工作的处理. 在另一篇<Java多线程设计模式(4)线程池模式>利用非Executors描述了线 ...
- 六、FreeRTOS学习笔记-任务挂起和恢复
任务的挂起与恢复的API函数介绍 API函数 描述 vTaskSuspend() 挂起任务 vTaskResume() 恢复被挂起的任务 xTaskResumeFromISR() 在中断中恢复被挂起的 ...
- vue结合element UI做checkbox全选的tree结构
由于element UI中的tree可能不能满足项目中的样式需求,所以自己动手结合element中的checkbox全选功能实现了一个符合项目需求的tree.效果如下: html部分: <tem ...
- Educational Codeforces Round 132 (Rated for Div
Educational Codeforces Round 132 (Rated for Div. 2) Recover an RBS 给你一个括号序列,里面存在?号,题目保证至少有一种方案使得该括号序 ...
- LALR语法分析表
LALR语法分析表 1.LALR(向前看-LR)技术 2.在实践中常用,分析表比规范LR分析表小 LALR(1)项集族的内核的高效计算方法 1.构造G的LR(0)项集族的内核 2.确定自发生的符号 3 ...
- 青少年学习C++参考视频
09C++选择结构(3) 第20课 初识算法 第21课 3个数排序 第22课 随机函数rand 第23课 if语句的应用 第24课 bug与debug 10C++选择结构(4) 第25课 成绩等级 第 ...
- 智能存储 | 超质感 HDR 生产,激活你的视神经
视频平台尊贵的会员可以享受 4K HDR 超清视界,各类新型旗舰机都具备拍摄 HDR 视频的能力,3C 产品发布会必提 HDR 超清显示.想必各位看官感受到视觉逐渐被 HDR 浪潮侵袭了,那 HDR ...