4️⃣React-Transition-Group & Redux
00 分钟
2024-2-9
2024-8-1

一、React-Transition-Group

(一)介绍

在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验。当然,我们可以通过原生的CSS来实现这些过渡动画,但是React社区为我们提供了react-transition-group用来完成过渡动画。
React曾为开发者提供过动画插件react-addons-css-transition-group,后由社区维护,形成了现在的react-transition-group。这个库可以帮助我们方便的实现组件的入场和离场动画,使用时需要进行额外的安装,且 react-transition-group本身非常小,不会为我们应用程序增加过多的负担。
 

(二)主要组件

react-transition-group主要包含四个组件:
  • Transition
    • 该组件是一个和平台无关的组件(不一定要结合CSS);
      在前端开发中,我们一般是结合CSS来完成样式,所以比较常用的是CSSTransition;
  • CSSTransition
    • 在前端开发中,通常使用CSSTransition来完成过渡动画效果
  • SwitchTransition
    • 两个组件显示和隐藏切换时,使用该组件
  • TransitionGroup
    • 将多个动画组件包裹在其中,一般用于列表中元素的动画;
 

(三)<Transition>

(四)<CSSTransitio>

<CSSTransition> 是基于 <Transition> 组件构建的,其执行过程中,有三个状态:appear(加载时)、enter(出现)、exit(消失)。
三种状态,需要定义对应的CSS样式:
  • 第一类,开始状态:对于的类是 -appear、-enter、exit;
  • 第二类:执行动画:对应的类是 -appear-active、-enter-active、-exit-active;
  • 第三类:执行结束:对应的类是 -appear-done、-enter-done、-exit-done;
💡
假设我们有个组件的 className 为card,则我们对应的不同的状态的类名是 card+预设类名,例如:进入状态的类名称为 card-enter
 
CSSTransition常见对应的属性
  • in:触发进入或者退出状态
    • 如果添加了 unmountOnExit={true},那么该组件会在执行退出动画结束后被移除掉;
    • 当in为 true 时,触发进入状态,会添加-enter、-enter-acitve的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;
    • 当in为 false 时,触发退出状态,会添加-exit、-exit-active的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;
  • classNames:动画class的名称
    • 决定了在编写css时,对应的class名称:比如 card-entercard-enter-activecard-enter-done
  • timeout
    • 过渡动画的时间
  • appear
    • 是否在初次进入添加动画(需要和 in 同时为 true
  • unmountOnExit:退出后卸载组件
 
以antd的card组建为例,测试动画代码,以下是案例代码
⚠️
我们需要注意,假设 isShow 初始值是设置为 true,则一开始渲染是不会触发动画的,除非我们另外设置 appear
 
以下代码解决刷新不触发动画的方法
 
此外,<CSSTransition> 对应的钩子函数:主要为了检测动画的执行过程,来完成一些JavaScript的操作。如:
  • onEnter:在进入动画之前被触发;
  • onEntering:在应用进入动画时被触发;
  • onEntered:在应用进入动画结束后被触发;
以下是示例代码:
 

(五)<SwitchTransition>

SwitchTransition可以完成两个组件之间切换的炫酷动画,比如我们有一个按钮需要在on和off之间切换,我们希望看到on先从左侧退出,off再从右侧进入。这个动画在vue中被称之为vue transition modes,react-transition-group中使用SwitchTransition来实现该动画。
SwitchTransition中主要有一个属性:mode,有两个值
  • in-out:表示新组件先进入,旧组件再移除;
  • out-in:表示就组件先移除,新组件再进入;
 
使用方法
<SwitchTransition> 组件里面要有 <CSSTransition> 或者 <Transition> 组件,不能直接包裹需要切换的组件。
<SwitchTransition> 里面的 <CSSTransition><Transition> 组件不再像以前那样接受 in 属性来判断元素是何种状态,取而代之的是 key 属性。
以下是案例代码:
 

(六)<TransitionGroup>

当我们有一组动画时,需要将这些 <CSSTransition> 放入到一个 <TransitionGroup> 中来完成动画。
 

二、Redux

(一)补充:纯函数

纯函数定义详见JS高级笔记 ,以下是一些案例:
 
从上述案例中,我们可以看到纯函数在函数式编程中非常重要
  • 可以安心的写和安心的用;
  • 在写的时候保证了函数的纯度,只是但是实现自己的业务逻辑即可,不需要关心传入的内容或者依赖其他的外部变量;
  • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
 
React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的 props 不被修改。redux中,reducer也被要求是一个纯函数。
 

(二)Redux的需求

JavaScript开发的应用程序,已经变得越来越复杂了,其需要管理的状态越来越多,越来越复杂,这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页。
管理不断变化的 state 是非常困难的,状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;当应用程序复杂时,state 在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪。
但是,React是在视图层帮助我们解决了DOM的渲染过程,但是 state 依然是留给我们自己来管理。无论是组件定义自己的 state,还是组件之间的通信通过 props 进行传递;也包括通过 context 进行数据之间的共享。
Redux 就是一个帮助我们管理 state 的容器:Redux 是 JavaScript 的状态容器,提供了可预测的状态管理。Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)
 

(三)Redux 三大核心

1️⃣ Store
Redux的核心理念非常简单,比如我们有一个朋友列表需要管理。如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的
  • 比如页面的某处通过 products.push 的方式增加了一条数据;
  • 比如另一个页面通过 products[0].age = 25 修改了一条数据;
 
2️⃣ action
Redux要求我们通过 action 来更新数据:所有数据的变化,必须通过派发(dispatch)action来更新。action 是一个普通的JavaScript对象,用来描述这次更新的 type 和 content。
比如下面就是几个更新friends的action:
强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的。
💡当然,目前我们的action是固定的对象,真实场景中,我们会通过函数来定义,返回一个action。
 
3️⃣ reducer
reducer在于将 state 和 action 联系在一起。reducer是一个纯函数,其将传入的 state 和 action 结合起来生成一个新的 state。
具体可以参考Array的原型的 reduce(),一个遍历和累加函数。
 

(四)Redux 三大原则

  • 单一数据源
    • 整个应用程序的 state 被存储在一棵object tree中,并且这个object tree只存储在一个store 中;
    • Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;
    • 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改;
  • State是只读的
    • 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State:
    • 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;
    • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心 race condition(竟态)的问题;
  • 使用纯函数来执行修改
    • 通过 reducer 将旧 state 和 actions 联系在一起,并且返回一个新的 State;
    • 随着应用程序的复杂度增加,我们可以将 reducer 拆分成多个小的 reducers,分别操作不同 state tree 的一部分;
    • 但是所有的 reducer 都应该是纯函数,不能产生任何的副作用
 

(五)Redux 测试项目

5.1 项目搭建

💡
本节我们先脱离React框架,单独学习Redux
  1. 项目初始化,在新建文件夹。
  1. 执行命令 npm init / yarn init,并填写相关信息(或者加入参数 -y 跳过所有设置)
  1. 安装redux:yarn add redux
  1. 创建src目录,并且创建 index.js 文件
  1. 修改 package.json 可以执行 index.js
     

    5.2 Redux使用

    具体使用见以下代码:
     

    5.3 Redux结构划分(封装)

    如果我们将所有的逻辑代码写到一起,那么当redux变得复杂时,代码就难以维护。以下将对代码进行拆分,将store、reducer、action、constants拆分成一个个文件。
    创建store/index.js文件:redux的主入口 创建store/reducer.js文件:reducer管理 创建store/actionCreators.js文件:action管理 创建store/constants.js文件:定义action常量
    以下是示例代码,但是需要注意,因为没有webpack的加持,所以不支持部分语句缩写:
     
    外部进行调用:
     

    (六)Redux × React

    6.1 流程介绍

    在React中使用Redux的流程如下图所示
    React × Redux 总体流程
    React × Redux 总体流程
    Redux官方图
    Redux官方图
     
    根据第五节代码,我们在React项目直接复制store文件夹至当前项目,并对部分代码(导入方式)进行调整,具体代码如下:
    💡
    但是需要注意自从Redux Toolkit推出之后,推荐使用 configureStore 代替 createStoreconfigureStore 提供更多的默认配置,目标是简化Redux的使用,并消除许多常见的Redux错误和bug。
     

    6.2 Redux × React 案例之手动联系

    实现一个类似以下效果的计数器效果
    多组件计数器案例
    多组件计数器案例
    核心代码有主要两个:
    • 类组件在 componentDidMount 中定义数据的变化,当数据发生变化时重新设置 counter;而函数组件通过 useSelector 获取Store的数据
    • 在发生点击事件时,调用store的 dispatch 来派发对应的action;
    📄 src/pages/home.js
     
    📄 src/pages/about.js 💡about组件的代码与home组件相似
     
    以下是调用组件代码
     

    6.3 Redux × React 案例之自定义connect函数

    对于上述类组件,部分代码仍然是重复的,例如订阅和取消订阅以及获取store的数据是可以抽取到一个文件中的。
    解决方案:创建公共函数 connet() 连接UI和Redux,通过 context 来进行代码抽取并解耦,即提供一个 Provider,其来自于我们创建的Context,让用户将store传入到value中即可;
     
    1️⃣ connet() 函数是 Redux 库中的一个核心概念。connect 函数用于连接 React 组件和 Redux store,使得 React 组件可以访问和操作 Redux store 中的状态。
    connect 函数接收两个参数:mapStateToProps 和 mapDispatchToPropmapStateToProps 是一个函数,它接收 Redux store 的状态作为参数,返回一个对象,这个对象的属性会被合并到被包裹组件的 props 中;mapDispatchToProp 也是一个函数,它接收 Redux store 的 dispatch 方法作为参数,返回一个对象,这个对象的方法会被合并到被包裹组件的 props 中。
    connect 函数返回一个高阶组件(HOC)。这个高阶组件接收一个组件作为参数,返回一个新的组件。这个新的组件会订阅 Redux store 的更新,当 Redux store 的状态发生变化时,会调用 mapStateToProps 和 mapDispatchToProp,并将它们的返回值作为 props 传递给被包裹的组件。
    在这段代码中,EnhanceComponent 就是这个高阶组件返回的新组件。它在挂载时订阅 Redux store 的更新,在卸载时取消订阅。在渲染时,它会调用 mapStateToProps 和 mapDispatchToProp,并将它们的返回值作为 props 传递给 WrappedComponent
    useContext 钩子函数来获取上下文,使用 useState 钩子函数来管理状态,使用 useEffect 钩子函数来处理副作用(例如订阅和取消订阅)
     
    2️⃣ React.createContext() 是 React 库中的一个方法,用于创建一个新的上下文对象,可以实现父子组件多层级之间的数据传递。这个对象包含两个 React 组件,分别是 Provider 和 Consumer
    Provider 组件用于包裹需要共享状态的组件,并通过 value 属性来提供共享的状态。
    Consumer 组件用于接收共享的状态。在函数组件中,我们通常使用 useContext 钩子函数来代替 Consumer 组件。
    在这段代码中,StoreContext 就是通过 React.createContext() 创建的上下文对象。然后,这个对象被导出,所以其他模块可以导入并使用它。
    使用:在类组件中通过给组件 contextType 属性赋值 StoreContext;在函数组件中,通过使用 useContext 钩子函数来接收共享的状态。
     
    3️⃣ index.js 文件也要随之修改,作为全局提供 store 的入口
     

    6.4 react-redux使用

    💡
    6.26.3 两节本质上是模拟react-redux库的部分源码实现,所以在开发中我们可以直接使用react-redux库即可
    首先需要明确,redux和react没有直接的关系,我们完全可以在React, Angular, Ember, jQuery, or vanilla JavaScript中使用Redux。尽管这样说,redux依然是和React或者Deku的库结合的更好,因为他们是通过state函数来描述界面的状态,Redux可以发射状态的更新,让他们作出相应。
    安装yarn add react-redux
     
    然后对于Home和About组件,我们只需要将我们原来的 connect() 函数改为react-redux库导出的 connect() 函数。
     

    6.5 react-redux源码解读

    阅读ing…
     

    (七)redux-thunk与异步操作

    💡
    本节前面部分是关于redux-thunk的引入,如需了解redux-thunk的使用直接 点击跳转⏩
    上述案例中,redux中保存的 counter 是一个本地定义的数据,我们可以直接通过同步的操作来 dispatch(action)state 就会被立即更新。
    但是真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。
    在之前学习axios中,网络请求可以在类组件的 componentDidMount 中发送,所以我们可以有这样的结构:
    notion image
    以下是在Home和About组件中展示banners和recommends数据的案例:
     
    在Home组件中我们获取了服务器的数据,然后在另一个组件About上面进行测试。经过一点时间的等待,数据会渲染到About组件中,证明网络请求并保存到Redux中的流程是成功的。
     
    根据上述代码我们可以知道,我们网络请求的代码放在了Page UI中,但是网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给redux来管理(如下图所示)
    notion image
    具体实现进行异步的操作需要用到中间件(Middleware),中间件的概念在Express或Koa框架中非常常见(在这类框架中,Middleware可以帮助我们在请求和响应之间嵌入一些操作的代码,比如cookie解析、日志记录、文件压缩等操作)
    redux也引入了中间件(Middleware)的概念:这个中间件的目的是在 dispatchaction 和最终达到的 reducer 之间,扩展一些自己的代码:比如日志记录、调用异步接口、添加代码调试功能等等。
    我们现在的需求是发送异步的网络请求,所以我们需要添加对应的中间件,官网推荐的的网络请求的中间件是使用 redux-thunk。默认情况下,dispatch(action),action需要是一个JavaScript的对象,即本质上 dispatch({})
    redux-thunk可以让 dispatch() 传入一个函数。该函数会被调用,并且会传给这个函数一个 dispatch 函数和 getState 函数;dispatch 函数用于我们之后再次派发action;getState 函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态。
     
    安装redux-thunk:yarn add redux-thunk / npm install redux-thunk
    然后修改以下代码
     

    (八)redux-devtools

    redux官网为我们提供了redux-devtools的工具,利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等。
    安装工具需要两步:
    1️⃣ 在对应的浏览器中安装相关的拓展插件;
    2️⃣ 在redux中继承devtools的中间件
     
    以下是代码配置:
     

    (九)redux-saga

    9.1 generator

    ES6的 generator 语法作为saga中间件前置知识,在此列出与saga相关的用法,其他用法详见以下MDN文档。
    当我们在 function 语句后加上一个 *,那么改函数称为生成器函数,其返回的是一个迭代器iterator。当调用iterator的 next() 函数,会销毁一次迭代器,并且返回一个 yield 的结果,以下是生成器函数执行顺序的案例代码:
     
    案例二:定义一个生成器函数, 依次可以生成1~10的数字
     
    案例三:generator和Promise结合
     

    9.2 redux-saga的使用

    相比于redux-thunk允许我们在 dispatch() 的时候传递一个函数,redux-saga是另一个比较常用在redux发送异步请求的中间件,它的使用更加的灵活。
    安装:yarn add redux-saga
    然后我们在代码中集成redux-saga中间件:
     
    创建saga文件,其要求导出一个生成器函数。该函数中有部分关键概念:takeLatest()takeEvery()takeLatest() 一次只能监听一个对应的action(前面有的action会被取消),而 takeEvery() 每一个action都会被执行。
     
     

    (十)中间件

    10.1 redux-saga 中间件

    中间件的目的是在redux中插入一些自己的操作。比如我们现在有一个需求,在 dispatch 之前,打印一下本次的 action 对象,dispatch 完成之后可以打印一下最新的 storestate,即我们需要将对应的代码插入到redux的某部分,让之后所有的 dispatch 都可以包含这样的操作。
    中间件相比于其他输出日志的方法有如下的优势:
    1️⃣ 相比派发的前后进行打印,减少了大量重复代码。
     
    2️⃣ 相比于我们对函数二次封装,对于调用者来说,很难记住这样的API,更加习惯的方式是直接调用 dispatch
     
    所以最终的方案,我们采用Monkey Patching,利用它可以修改原有的程序逻辑
    💡
    Monkey Patching出自于Python,Python是一种典型的动态脚本语言。它不仅具有 动态类型(dynamic type) ,而且它的 对象模型(object model) 也是动态的。Python的类是可变的(mutable),方法(methods)只是类的属性(attributes);这允许我们在 运行时(run time) 修改其行为。这被称为猴子补丁(Monkey Patching),它指的是偷偷地更改代码。
     
    但是为了更强的适用性,我们对上述的过程进行封装
     

    10.2 redux-thunk 中间件

    redux-thunk的基本使用见上面第七节
     
     

    10.3 applyMiddleware 中间件

    单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并。
     
    但是上述 10.1节10.2节 的代码统一也需要做出优化
     
    上述代码的流程如下图所示
    notion image
     

    (十一)Reducer的优化

    💡
    Reducer一词源于数组原型的方法:Array.prototype.reduce(preVal = 0, item) => {},而其中 (preVal = 0, item) => {} 我们称为reducer。这与我们redux中的 (preState = defaultState, action) => {}; 类似,都是第二个参数都是经过一定操作后把值加给第一个参数并返回一个结果。
    假设将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。
    因此,我们可以对reducer进行拆分:
    1. 我们先抽取一个对counter处理的reducer;
    1. 再抽取一个对home处理的reducer;
    1. 最后将它们合并起来;
    具体代码如下:
    当然修改了Redux的代码之后,视图使用也需要跟着修改
     
     
    1. 虽然已经将不同的状态处理拆分到不同的reducer中,但是这些函数的处理依然是在同一个文件中,代码非常的混乱;
    1. 另外关于reducer中用到的constant、action等我们也依然是在同一个文件中,所以需要进行文件的拆分
    以下是文件结构图(点击可跳转):
    以下是全部的具体代码:
    以上是公共的页面的store的配置,以下是公共的配置
     

    (十二)combineReducers() 源码分析

    上述我们合并的方式是通过每次调用 reducer 函数自己来返回一个新的对象(⬆️如上一节),实际上,redux给我们提供了一个 combineReducers() 函数可以方便的让我们对多个reducer进行合并。
    对于我们自己实现的 reducer 函数而言,有一个缺点:假如我们传递的 action 没有改变任何的数据,返回一个新的原来的state对象,再重新刷新界面,这显然会浪费一定的资源。
    以下是 combineReducers() 函数的解析
     

    (十三)总结管理state

    目前我们主要学习了三种状态管理方式:
    1️⃣ 类组件中自己的state管理,或者函数组件中Hook → useState()
    2️⃣ Context数据的共享
    3️⃣ Redux管理应用状态
     
    那在开发中如何选择?首先,这个没有一个标准的答案:
    • 某些用户,选择将所有的状态放到redux中进行管理,因为这样方便追踪和共享;
    • 有些用户,选择将某些组件自已的状态放到组件内部进行管理;
    • 有些用户,将类似于主题、用户信息等数据放到Context中进行共享和管理;
     
    💡 建议
    • UI相关的组件内部可以维护的状态,在组件内部自己来维护
    • 大部分需要共享的状态,都交给redux来管理和维护
    • 从服务器请求的数据(包括请求的操作),交给redux来维护;
     

    (十四)Redux 新变化

    假如我们使用React@18版本,Redux结合Toolkit使用见Demo项目store部分
     
     
     
    notion image

     
    上一篇
    React的样式 × AntDesign × axios
    下一篇
    React-router & Hook

    评论
    Loading...