搬砖搬砖,分享点搬砖技巧。
第六章了,差不多结束了 ,还有些零零散散的,慢慢搬吧。
之前的章节内容可点这~
第六章 — redux
redux中文官网
6.1 什么是 redux
官方是这样子说的 :redux
是 JavaScript 应用程序的可预测状态容器 ,它可以帮助您编写行为一致、在不同环境(客户端、服务器和本机)中运行且易于测试的应用程序。
官方的解释有时候总是看不懂的
说一下我的理解。
JavaScript 应用程序 : redux 和 react 可是两回事,只是长的像而已。 redux 可以在任何应用程序使用。
redux 有一个核心的东西 ,就是 store . 那现在有了一个新的问题,什么是 store 。
store
英文的释义是商店。在 redux 里它是一个存储数据的地方,就相当于一个容器,一个仓库。整个程序只有一个 store 。这便于状态的集中管理。
为什么说是可预测呢?
因为 redux 管理的数据的修改过程是我们设置的,也就是我们已经告诉他应该怎么做。并且每次修改之前是会接收到之前的状态。过程是可预测,可跟踪的。
6.2 为什么需要 redux
当程序的功能比较复杂,组件较多,并且需要共享状态且共享状态困难时, 使用redux 是一个较好的解决方案。
我们可以通过这两张图理解:有 A , B ,C ,D, 四个房子(组件) ,它们总是会相互借东西。相互交流。(组件交互,状态共享,修改数据)。那么以我们之前的方法,像通过共同的父组件来传递数据,那么父组件会变得很臃肿,因为需要定义一些它本身不需要的状态和方法,只是为了方便儿子们交流。
或者使用消息订阅-发布机制。 B 需要 A的某个数据,就需要订阅,然后 A 发布 。然后 D也需要,于是 D 也需要订阅。个组件来来往往就像如图一样乱七八糟。
而使用 redux . 就相当于建了一个仓库 , 各房子把需要共享的东西放在仓库,谁需要谁去拿。那么不但把数据需要共享的资源统一一个地方存储,并且方便了其他组件去使用。
非必要,不使用 redux
并不是如何程序或者 使用了 react 就需要使用 redux 的 。
有句话这样子说
“如果你不知道是否需要 Redux,那就是不需要它。”
为什么呢 , redux 是便于组件共享状态的,如果组件组件交互少,需要共享的状态不多,且不需要 redux 便能解决的那么根本就不需要使用 redux 。
使用场景
- 组件间需要共享状态,交互较多
- 不同身份的用户有不同的使用方式(比如普通用户和管理员)
- 多个用户之间可以协作
- 与服务器大量交互,或者使用了WebSocket
- View要从多个来源获取数据
6.3redux 的工作流程图解
在介绍如何使用之前,先了解一下redux 的工作流程是很有必要的,这有助于理解。
Component 便是组件了
ActionCreators redux 分为 3个部分,核心部分是 Store , 然后是 ActionCreators 和 Reducers.
其中ActionCreators 是创建 action 的 。也就是创建行为的一个模块。
Reducers 处理 dispatch 发出的行为 。也就是实际做出行动的模块。
Store 便是保存状态的核心部分 无论是发出行为,存储数据,还是把结果反应出去都是通过它。
我们可以理解为 redux 就是一个饭店 ,各组件是客人,客人通过 服务员 ( ActionCreators) 下单 ,告诉需要什么菜。于是服务员跟老板说了(Store) , 然后老板让厨师(Reducers) 做菜。最后客人从老板这儿了拿菜。这当然与实际有些不同,但这不妨碍我们更好理解这个过程。
回到实际, 组件在需要发出行为时,调用了 store上的 dispatch 方法 ,途中向 ActionCreators 拿了一下 action。并作为 dispatch 的参数传给 store 。 也就是实际上调用 dispatch方法 的是 组件。
然后Store 把 当前的状态的 action 传给 reducer 。 reducer 处理完后返回新的 状态 给 store。组件再通过 store 的 getState() 方法拿到新的状态数据。
6.4 redux 的使用
安装及基础准备
通过 npm
npm install redux
通过 yarn
yarn add redux
创建相关文件夹和文件
创建组件和 redux 文件夹及 redux 内相关文件(如图所示)
因为 不同组件有不同的行为,因此可以把不同的 创建action 的文件放在一个文件夹里。如图中的 actions文件夹。
同理 不同组件发出行为的操作也不同 ,也需要不同的 reducer , 因此放在同一个文件夹下。
而 store 仅有一个,因此直接放于 redux文件夹下即可。
constant.js 文件, 用于存储 type 常量,因为 type 的值是我们事先给定的,通过不同 type 触发不同的操作。因此在组件和 reducer 里都需要使用到 type 的值。使用常量,是便于修改,放在打错字。
基本使用
假设我们需要完成一个例子
将结果的数据存放在 state 中 , 点击 + 或 - 可以对数据进行加一或者减一。结果默认显示 99 。
action
创建 action 的文件不需要引入什么 API ,我们需要创建相关方法,该方法返回通过 action
。
action
是一个对象, 包括两个属性 type
和 data
, 分别是类型和数据, type 会表明到时候 reducer 应该执行的操作, data 是传递的数据。此数据来源组件。也就是 action 文件暴露的方法是给组件调用的,并且组件会传入一个数据。
// 分别暴露 创建的相应的创建 action 的方法 ,当其他地方需要使用action时,调用这个方法便创建并可以返回 action。
export let createIncrease = (data) => ({ type: “increase”, data })
export let createDecrease = (data) => ({ type: ”decrease“, data })
reducer
reducer文件 也是不需要引入什么 API 的。它用来创建为某组件专门服务的 reducer 函数。仅一个,判断 type 提供不同的操作。
reducer 函数接收两个参数 , 第一个是 preState , 其实是当前的状态(因为经过此次操作后,会返回新的 State ,因此把它叫做 preState, 含义是之前的状态),第二个参数是 action 。
reducer 会在 redux 加载后执行一次,此次会返回一个默认值。
/*
1. 此文件用来创建一个为 Count 组件的 服务的 reducer . 本质是一个函数
2. 函数接收两个参数 , 第一个是之前的 preState , 第二个是动作对象 action (包括行为,和数据)
*/
export default function countReducer(preState, action) {
console.log(preState);
console.log(action);
const { type, data } = action
switch (type) {
case "increase":
return preState + data
case "decrease":
return preState - data
default:
return 99 // 初始返回的默认值
}
}
注意:
- preState 如果是原始值可以修改本身,再返回本身,如果是引用值切记不能修改本身然后返回本身 因为 redux 会对返回值与 preState 进行地址比较,如果是一样的地址会导致更新的失败。
// 如果是原始值,如此操作不影响结果
case "increase" :
preState += data
return preState
// 如果是引用值 ,以下操作是错误的
case "pop" :{
preState.pop()
return preState
}
- reducer 要求必须是一个纯函数。但如果不存并不会报错。存函数需要满足以下几点操作
1. 不得修改参数 (即使是原数组,也尽量不要修改)
2. 不应该具有其他副功能,如果发送网络请求,连接到其他输入输出设备
3. 不能使用不存的函数,如 Date.now() , Math.random() 等返回值不确定不唯一的 API。
store
store
文件 是 redux 的核心。
创建 store 需要从 redux 引入 createStore API 。 它是一个函数。此函数返回一个 Store。
createStore
函数可以接收三个参数。
第一个参数是 reducer(必须) 。 或者是 reducers (多个 reducer 的合并,接下来会讲)。
第二个参数是 初始状态(非必须) 。 如果这个参数没有给,那么第三个参数就成为了第二个参数。
第三个参是 支持异步 action 用的( 非必须,接下来会讲) 。如果没有第二参数,那么这个参数是第二参数。
import { createStore } from 'redux'
// 引入为 Count 专门服务的 reducer
import CountReducer from './reducers/count'
// 暴露 state
export default createStore(CountReducer)
如此 redux 的创建和基本代码便完成了
接下来学习 store 的几个 API 和使用 store
store.dispatch(action)
这是发出 action
的唯一方法 , 组件拿到action
, 拿到 store
. 便可调用 dispatch
方法,传递 action 给 store
/** Count Component **/
// 导入 store
import store from '../../redux/store'
// 导入创建 action 的函数
import { createIncrease } from '../../redux/actions/count'
// 组件实例的方法
incread = () => {
store.dispatch(
createIncrease(1)
)
}
// 当 action 比较少时, 我们也可以不需要 action函数 。
store.dispatch(
{
type:"increase",
data:1
}
)
store.getState()
store.getState()
是获取 store 当前数据的一个 API 。也就是获取 reducer 最后一次的返回值。
// 导入 store
import store from '../../redux/store'
// 组件内
render(){
return (<h1>结果为 :{store.getState()}</h1>) // 结果为 : 99
}
store.subscribe()
我们知道当组件的 state 改变时,会重新渲染组件 。 但无论是 store 还是 reducer 都不会把改完后的数据主动给组件,或者告诉组件一个更新了。因此 store 提供了 subscribe
API , 当 store 刷新时,会执行这个 subscribe 函数 , 调用传给 subscribe 的回调函数。
因此利用这个API ,我们可以在 store 改变时,刷新 组件即可
/** scr/index.js **/
import React from "react";
import ReactDOM from "react-dom";
import App from './App';
// 导入 stre
import store from './redux/store'
ReactDOM.render(< App />, document.querySelector("#root"));
// 当 store 刷新时,重新渲染组件,便重新调用 getState 从而获取新的状态。
store.subscribe(() => {
ReactDOM.render(< App />, document.querySelector("#root"));
})
// 如此全部组件都可以获取新的 store 数据了
完成例子 - 加减计算器
redux 相关文件和代码在上面的介绍中已经完成。接下来我们看看完整的组件代码。注意 scr/index.js内需要使用 subscribe
API
import React, { Component } from 'react'
// 导入 store
import store from '../../redux/store'
// 导入 action
import { createIncrease, createDecrease } from '../../redux/actions/count'
export default class Count extends Component {
incread = () => {
store.dispatch( createIncrease(1))
}
decread = () => {
store.dispatch(createDecrease( 1))
}
render() {
return (
<div>
{/* 显示结果 */}
<h1>结果为 :{store.getState()}</h1>
{/* 点击加一 */}
<button className="btn btn-default" onClick={this.incread} >+</button>
{/* 点击减一 */}
<button className="btn btn-default" onClick={this.decread} >-</button>
</div >
)
}
}
数据共享
redux 的主要目的就是多状态数据共享。所以这个功能是很重要的。
在前面的基本使用我们只创建一个了 reducer
。 然后把该 reducer 作为 createStore()
的第一个参数 。那如果我们使用 redux 几乎不会只有一个 reducer 的。那么多个reducer
又怎么集合到一个 store
里,组件并且如何获取数据呢?这就是接下来要讨论的问题。
combineReducers
这是 redux 暴露的一个 API 。它用来把所有的 reducer 集合一起,返回值然后作为第一参数传给 createStore ()
。 那么不同组件的 数据就可以集中保存到 store 中,需要就可以向 store 拿便可
// 额外导入 combineReducers
import { createStore ,combineReducers} from 'redux'
// 引入 Count 的 reducer
import CountR from './reducers/countR'
// 引入 其他 reducer
import OtherR from './reducers/otherR'
// 调用 combineReducers ,传入对象。返回作为 createStore的第一个参数
const allReducer = combineReducers({
data1:CountR, // 为方便解释,实际可不能这么顺便
data2:OtherR
})
// 创建store
export default createStore(allReducer)
除了增加一个 reducer 外,其他代码并不需要修改上面(同基本使用一样)
然后便是如何获取数据了
虽然 store
保存了多个状态了,但获取数据还是通过 store.getState()
来获取。
当只有一个 reducer 时 , store.getState()
的返回值就是 该 reducer 处理完后 return 的 newState
当有多个 reducer 时 , store.getState()
返回的是一个对象 ,可以理解为就是传给 combineReducers
的对象,不过键对于的值是每次执行对应的 reducer 后的返回值 。
// 如果 combineReducers 的参数是如此的
// const allReducer = combineReducers({
// data1:CountR, // 为方便解释,实际可不能这么顺便
// data2:OtherR
// })
// 在组件里获取相应 reducer 返回的状态就是
store.getState().key
// 如 获取 Count 的 reducer 返回的状态
store.getState().data1
异步 action
在上面的例子中, action 都是同步的,也就是点击加一后结果立即加一,如果有一个异步操作,比方说发个请求,或者倒计时几秒后再执行应该怎么如何操作呢?
假设我们有这样的一个需求,我们希望以上例子的基础上加一个按钮,点击后3 秒再增1.
如果把异步操作放在 视图容器可就是组件里,那么异步加确实是很方便操作。
Asyncdecread = () => {
setTimeout(() => {
store.dispatch(
createIncrease(1)
)
}, 3000);
}
但是我们是希望 组件是显示状态的 , 如果这种异步操作少,那么确实把异步放组件里很方便,也容易修改。
但一旦异步操作多了起来,数据处理更复杂了,那么组件就不该承当这些功能了。
Store 当然也不能做到异步操作, reducer 是纯函数,只执行 State 的修改。那么实现异步 action 的任务还是归给了 action Creators
applyMiddleware
我尝试了在 action 里直接写异步操作
setTimeout(() => {
return { type: "increase", 1 }
}, 3000);
点击加一后执行结果报错。 报错信息说 action Creators
返回的应该一个是一个对象,而不是 undefined
为什么呢?不是 3 秒后返回 一个对象了吗? 这就跟 JavaScript 的异步函数执行机制有关了。这说下去篇幅就大了。
总之呢就是不行。
为了实现异步 action , 我们需要 使用一个 东西, 叫 applyMiddleware
applyMiddleware
是一个函数 , 它可以对 store.dispatch()
进行改造 。 让我们在执行 dispatch()
之后并且 在执行 reducer 之前能够执行其他操作,如异步操作。
applyMiddleware
是 redux 提供了一个 API 。 可作为 createStore()
的 第二或 第三 个参数传入 。
export default createStore(reducer, applyMiddleware())
我们知道 action Creators
如果执行异步操作后再返回 action 是无法被 store.dispatch
接收的 。 因此我们可以把思路转到,执行异步操作后再执行 dispatch()
。
applyMiddleware
便支持我们这样做 。
// 组件
store.dispatch(createIncreaseaAsync(1, 3000)) // 表示 3 秒后加一
// action Creator
export let createIncreaseaAsync = (data, delay) => {
return (dispatch) => {
setTimeout(() => {
dispatch({type:"increase",data:data})
}, delay);
}
}
这里我们直接返回一个函数,在函数内部执行异步操作。结束后再次发送 dispatch
。 而这个 dispacth
方法等同于 store.dispatch
方法 。 它是 applyMiddleware 传进来的 。
如此我们便解决了异步的问题了 。但这又有一个问题,就是 action Creators
要求返回的是对象,可不是函数。
因此我们需要使用 一个 中间件 。叫 redux-thunk
redux-thunk
redux-thunk 是一种中间件 , 它可以让 action Creators
返回函数,并且让 dispatch()
可以接受函数参数。
中间件 - middleware , 它的使用方法就是作为 applyMiddleware
的参数。
使用过程
- 安装
yarn add redux-thunk
或者
npm install redux-thunk
- 导入
import thunk from ‘redux-thunk’
- 使用
export default createStore(allReducer, applyMiddleware(thunk))
如此我们便可顺利执行 异步 action 了 。
异步action 实现代码
store.js
// 额外导入 applyMiddleware
import { createStore, applyMiddleware} from 'redux'
// 导入 thunk
import thunk from 'redux-thunk';
// 引入 Count 的 reducer
import reducer from './reducers/count'
export default createStore(reducer, applyMiddleware(thunk))
action Creators
// 暴露创建action 加 减 的方法
export let createIncrease = (data) => ({ type: 'increase', data })
export let createDecrease = (data) => ({ type: 'decrease', data })
// 导出异步的 actiob.
export let createIncreaseaAsync = (data, delay) => { // 接收两个参数,数据和计时器倒计时长
// 返回函数
return (dispatch) => { // 函数接收到一个 dispatch 方法
setTimeout(() => {
dispatch(createIncrease(data)) // 异步操作结束后 重新 dispatch 发生 action
}, delay);
}
}
reducer
参考 redux 的基本使用 的 reducer 部分代码。
Count-component 注意 要使用 store.subscribe()
在改变状态时重新渲染组件
import React, { Component } from 'react'
// 导入 store
import store from '../../redux/store'
// 导入 action 和异步 action
import { createIncrease, createDecrease, createIncreaseaAsync} from '../../redux/actions/count'
export default class Count extends Component {
incread = () => {
store.dispatch( createIncrease(1))
}
decread = () => {
store.dispatch(createDecrease(1))
}
// 异步 action
increadAsync= ()=>{
store.dispatch(createDecrease(1,3000))
}
render() {
return (
<div>
{/* 显示结果 */}
<h1>结果为 :{store.getState()}</h1>
{/* 点击加一 */}
<button className="btn btn-default" onClick={this.incread} >+</button>
{/* 点击减一 */}
<button className="btn btn-default" onClick={this.decread} >-</button>
{/* 点击异步加一 */}
<button className="btn btn-default" onClick={this.increadAsync} >异步执行</button>
</div >
)
}
}
6.5 react-redux
react-redux 便是 react 官方出的了 。 它的作用是让 react 使用 redux 更加方便 。
但并不是必须要使用,因为需要学习成本,本身代码量也可能会增加。并且有新的使用规则。
为什么使用 react-redux
- 它区分了 UI 组件和 容器组件,进一步区别开逻辑和 UI 。
- 它提供的效率,减少不必要的重新渲染。部分未更新的 State 不会导致 组件的重新渲染。
- 它自动监听 state 变化,自动刷新组件 。
react-redux 工作原理图
在使用前我们还是先看看它的 工作原理图。便于接下来的学习。
在之前 , redux 和 组件是直接联系的 。 组件需要数据直接 store.getState()
也就是直接 store.dispatch()
而 react-redux
把组件分成了 容器组件 和 UI 组件 。 redux 的书写无需更改。
容器组件把 UI 组件需要的 状态和 创建 action 的函数 通过 props 传递给 UI 组件 。 UI 组件负责显示和 执行 函数修改状态 。 但 UI 组件不会 引入 store 和 创建action的方法 。 这些逻辑的实现全都有 容器组件负责 。
说白了 UI 组件就是 给数据我显示 ,给方法我执行 。 什么怎么来 , 方法的具体实现我都不管。
这里具体看看 两个组件的要求 :
UI组件
- 只负责 UI 的呈现,不带有任何业务逻辑
- 状态和操作状态的方法由容器组件提供
- 不使用任何 Redux 的 API
容器组件
- 负责管理数据和业务逻辑,不负责 UI 的呈现
- 带有内部状态
- 使用 Redux 的 API
具体实现
安装
yarn add react-redux
或者
npm install react-redux
创建组件
因为 react-redux 把组件分为了 容器组件和 UI 组件 。因此我们可以分为两个文件。
connect
connect()
是 react-redux 暴露的一个 API 。 它用来连接 容器组件和 UI 组件的 。
此方法需要传递两个函数参数 。并且都是必须参数
第一个参数
mapStateToProps : 字面意思是映射状态到 Props . 它是一个函数,此函数把需要的状态数据取出 ,最后返回一个对象 , 此对象里的键值会被传递到 UI 组件的 props属性 里
此函数会接收到一个参数 。 此参数的数据相当于 store.getState()
返回的数据。
function mapStateToProps(state){ // 由 react-redux 调用。会传入参数 state . 即reducer处理后的状态
return {
count:state // count 会被传到 UI 组件的 props 里
}
}
第二个参数
mapDispatchToProps : 意思是 映射 dispatch 到 props 。 它是一个函数 , 此函数返回一个对象。对象的键对应的值是一个函数,此函数执行的是 dispatch 方法。dispatch 是由 react-redux 传进来的参数 。
function mapDispatchToProps(dispatch){ // 传入参数 dispatch
return {
fun1:(data)=>{dispatch(createIncrease(data))}, // 此方法会传入到 UI 组件的 Props 里
fun2:(data)=>{dispatch(createDecrease(data))},
}
}
因此函数传给 UI 组件,组件调用函数就相当于调用 dispatch了 。
简写
-
当需要的状态不多,时我们可以把 mapStateToProps 以匿名箭头函数的方式直接作为 connect 的参数。
-
第二个参数可以直接是一个对象 , 重复的调用 dispatch 可不符合 react-redux 的目的。 因此我们只需要把键对应的值换成
action Creator
, 然后 react-redux 会处理好,等组件调用时就相当于调用 dsipacth 了 。
connect(
state => ({ count: state}),
{
fun1: createIncrease // 参数是正常接收的 。 UI 组件调用 fun1(data) , 此参数会传给 createIncrease函数。
fun2: createDecrease
}
)
connect 返回值
connect 返回一个函数,实际上来说 , 这个返回的函数才是真正创建 容器组件的函数 。
是的,容器组件可不是我们自己写的 。而是 react-redux 创建的 。
connect 返回值的函数接收一个参数 ,此参数是 UI 组件 。 最后的返回值才是容器组件 。
// 引入 UI 组件
import CountUI from '../../component/Count' // 会在下面的贴出代码
// 获取 创建 容器组件的函数
let containerCreator = connect(mapStateToProps,mapDispatchToProps)
// 创建容器组件
export default containerCreator(CountUI) // 暴露容器组件
// 简写
export default connect(mapStateToProps,mapDispatchToProps)(CountUI)
容器组件创建好了,就该使用了
在 容器组件中我们没有导入 store 。 store 需要我们在使用容器组件时传给容器组件的 props 。并且键就叫 store.
import React, { Component } from 'react'
// 导入容器组件
import Count from './container/Count'
// 导入 store
import store from './redux/store'
export default class App extends Component {
render() {
return (
<div>
<Count store={store} />
</div >
)
}
}
因为容器组件的子组件是 UI 组件,因此我们只需要把 容器组件渲染出去, UI 组件自然就显示出来了 。
Provider
Provider 是 react-redux 暴露的一个组件 。此组件没有什么其他功能,也不负责渲染 UI 。 它只做一件事,就是把 store 传给所有的容器组件 。 从而不需要我们使用一个容器组件就自己手动传一次 store 。
一般我们在 scr 下的入口文件 即 index.js 文件里使用 , 如此可以覆盖使用的 容器组件。
import React from "react";
import ReactDOM from "react-dom";
import App from './App';
// 导入 store
import store from './redux/store'
// 导入入 react-redux 提供 Provider 组件
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store} >
< App />
</Provider>,
document.querySelector("#root"));
如果还记得之前的 store.subscribe()
就会发现, 这里我们没有使用它了。因为 react-redux 会帮我们监听 store 里状态的改变 ,并且自动更新。
具体代码
container/Count.js
// 引入 UI 组件
import CountUI from '../../component/Count'
// 引入 connect 连接 UI 和 reudx
import { connect } from 'react-redux'
// 引入 action
import {createIncrease,createDecrease,createIncreaseaAsync} from '../../redux/actions/count'
// 第二个参数简便的写法
const mapDispatchToProps={
fun1:createIncrease,
fun2:createDecrease,
fun3:createIncreaseaAsync
}
// 调用 connect 函数,返回一个函数 , 再调用返回的函数。返回一个container组件 .此组件连接UI组件和redux
export default connect(
state=>({count:state}),
mapDispatchToProps)(CountUI) // 连接 UI组件
component/Count.jsx
import React, { Component } from 'react'
// 不可引入 store
export default class Count extends Component {
incread = () => {
// 调用 方法
this.props.fun1(1)
}
decread = () => {
this.props.fun2(1)
}
increadAsync = () => {
this.props.fun3(1,3000)
}
render() {
return (
<div> {/** 获取状态 **/}
<h1>结果为 :{this.props.count}</h1>
<button className="btn btn-default" onClick={this.incread} >+</button>
<button className="btn btn-default" onClick={this.decread} >-</button>
<button className="btn btn-default" onClick={this.increadAsync} >异步加油</button>
</div >
)
}
}
redux相关文件不变
在 App.jsx 里使用 Count的容器组件 , 在 index.js 里使用 Provide组价 并且 传入 store 即可
参考
redux概念之一:redux简介
阮一峰的 redux 教程
尚硅谷react教程 redux部分
一万 3千字,真干到流泪啊~ 。
阮一峰的教程和尚硅谷的教程都讲的很好。我写这些即方便自己以后回顾,也希望能帮助需要学习的人。
redux 自己出了一个开发者工具,可以检测,测试,回顾 state 的状态 。很好用。
对这个的使用我就不解释了 。尚硅谷的课程有讲,看一看视频便可
redux开发者工具的使用
如果你的谷歌游览器访问不了外网,那么可以在这个网站里下载插件
crx4下载redux devtools