我们是袋鼠云数栈 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 做对应操作

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) => {
//获得老的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 本身的提供的数据流方式出发

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

参考连接

最后

欢迎关注【袋鼠云数栈UED团队】~

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

React中的数据流管理的更多相关文章

  1. React 回忆录(四)React 中的状态管理

    Hi 各位,欢迎来到 React 回忆录!

  2. 浅谈React数据流管理

    引言:为什么数据流管理如此重要?react的核心思想就是:UI=render(data),data就是我们说的数据流,render是react提供的纯函数,所以用户界面的展示完全取决于数据层.这篇文章 ...

  3. react --- React中state和props分别是什么?

    props React的核心思想就是组件化思想,页面会被切分成一些独立的.可复用的组件. 组件从概念上看就是一个函数,可以接受一个参数作为输入值,这个参数就是props,所以可以把props理解为从外 ...

  4. React中state和props分别是什么?

    整理一下React中关于state和props的知识点. 在任何应用中,数据都是必不可少的.我们需要直接的改变页面上一块的区域来使得视图的刷新,或者间接地改变其他地方的数据.React的数据是自顶向下 ...

  5. React中组件间通信的方式

    React中组件间通信的方式 React中组件间通信包括父子组件.兄弟组件.隔代组件.非嵌套组件之间通信. Props props适用于父子组件的通信,props以单向数据流的形式可以很好的完成父子组 ...

  6. react实战系列 —— React 中的表单和路由的原理

    其他章节请看: react实战 系列 React 中的表单和路由的原理 React 中的表单是否简单好用,受控组件和非受控是指什么? React 中的路由原理是什么,如何更好的理解 React 应用的 ...

  7. Immutable 详解及 React 中实践

    本文转自:https://github.com/camsong/blog/issues/3 Shared mutable state is the root of all evil(共享的可变状态是万 ...

  8. React中父组件与子组件之间的数据传递和标准化的思考

    React中父组件与子组件之间的数据传递的的实现大家都可以轻易做到,但对比很多人的实现方法,总是会有或多或少的差异.在一个团队中,这种实现的差异体现了每个人各自的理解的不同,但是反过来思考,一个团队用 ...

  9. React中的路由系统

    React中的路由系统 提起路由,首先想到的就是 ASPNET MVC 里面的路由系统--通过事先定义一组路由规则,程序运行时就能自动根据我们输入的URL来返回相对应的页面.前端中的路由与之类似,前端 ...

  10. React 深入系列1:React 中的元素、组件、实例和节点

    文:徐超,<React进阶之路>作者 授权发布,转载请注明作者及出处 React 深入系列,深入讲解了React中的重点概念.特性和模式等,旨在帮助大家加深对React的理解,以及在项目中 ...

随机推荐

  1. Python中dict支持多个key的方法

    在Python中,字典(dict)是一种非常强大的数据结构,它允许我们通过键(key)来存储和检索值(value).有时候,我们可能想要根据多个键来检索或操作字典中的数据.虽然Python的字典不直接 ...

  2. C语言八股文(温故知新)

    1.volatile关键字 volatile int i=10; int j = i; ... int k = i; volatile告诉编译器i变量是随时可能发生变化的,例如IO端口的输入值,所以每 ...

  3. 解决页面隐藏的元素闪现的问题v-cloak

    在v-if处加上v-cloak属性 <div class="sys-changeBox" v-cloak v-if="showTosat" @click. ...

  4. Linux之命令提示神器tldr

    github:tldr-pages/tldr: Collaborative cheatsheets for console commands (github.com) 一款很好用的命令帮助工具, 之前 ...

  5. brew之加速

    有没有出现这种场景:使用brew install 安装程序,一直卡在brew updating,这可能是使用着默认的github镜像源导致,那么我们就需要将其切换到国内 1.镜像切换(推荐中科大) 1 ...

  6. 指针, C语言的精髓

    指针, C语言的精髓 莫队先咕几天, 容我先讲完树剖 (因为后面树上的东西好多都要用树剖求 LCA). 什么是指针 保存变量地址的变量叫做指针. 这是大概的定义, 但是Defad认为这个定义不太好理解 ...

  7. 19号CSS学习

    一.CSS的复合选择器 更高效的选择目标元素. 后代选择器.子选择器.并集选择器.伪类选择器等. 1.后代选择器 可以选择父元素里的子元素. 又称包含选择器. 必须是空格,必须是后代,ul li {c ...

  8. 《前端运维》五、k8s--2pod、services与Ingress部署

    前一篇啊,我们学完了基本的配置.这一篇,我们来看下服务部署的配置.我们先来看张图,理解下k8s的应用场景和调用流程: 看上图,首先,master是控制节点,负责编排.管理.调度用户提交的作业.kube ...

  9. C#/.NET/.NET Core技术前沿周刊 | 第 17 期(2024年12.09-12.15)

    前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...

  10. 内网穿透之http代理服务器

    在公网访问内网http服务可以用内网穿透工具,例如frp和nps等优秀工具.但我发现这类工具会在服务器启动不止一个端口,对于有些网络审查来说很容易发现在进行内网穿透.因此我想是否可以只在服务器启动一个 ...