React中redux的使用(11)
在学习redux之前先了解一下纯函数:
函数式编程中有一个概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念
纯函数的维基百科定义:
上面的定义会过于的晦涩,简单总结:
- 确定的输入,一定会产生确定的输出;
- 函数在执行过程中,不能产生副作用;
<script>
// 下面是纯函数吗? 不是
function printfNum(num) {
return num + 20;
}
let num1 = 20;
printfNum(num1);
num = 30;
// 如果把 let num1 改为 const num1 就是一个纯函数了
// ....
</script>
React中的纯函数
纯函数还有很多的变种,但是我们只需要理解它的核心就可以了。
为什么纯函数在函数式编程中非常重要呢?
- 因为你可以安心的写和安心的用;
- 你在写的时候保证了函数的纯度,只是但是实现自己的业务逻辑即可,不需要关心传入的内容或者依赖其他的外部变量;
- 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:
在学习redux中,reducer也被要求是一个纯函数。
10.1redux介绍
Redux 是 JavaScript 应用的状态容器,提供可预测的状态管理。
JavaScript开发的应用程序已经变得越来越复杂了,它需要管理的状态越来越多,越来越复杂。
- 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页。
- 管理不断变化的state是非常困难的
- 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
- 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:
- 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享;
- React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定:
Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理;
Redux 除了和 React 一起用外,还支持其它界面库。它体小精悍(只有2kB,包括依赖),却有很强大的插件扩展生态。
10.2redux核心概念
当我们有一个朋友列表需要管理:
- 如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;
- 比如页面的某处通过products.push的方式增加了一条数据;
- 比如另一个页面通过products[0].age = 25修改了一条数据;
整个应用程序错综复杂,当出现bug时,很难跟踪到底哪里发生的变化;
用普通对象描述:
上面这种是不可追踪不可预测的!
1.核心概念:Action
Redux要求我们通过action来更新数据:
所有数据的变化,必须通过派发(dispatch)action来更新;
action是一个普通的JavaScript对象,用来描述这次更新的type和content;
比如下面就是几个更新的action:
强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的;
目前我们的action是固定的对象(如果是这样的话要修改多少状态就会有多少action),真实应用中,我们会通过函数来定义,返回一个action;
2.核心概念:reducer
action只是描述了数据要发生改变,并没有描述应用如何更新 state。
而将state和action联系在一起,Reducers 指定了应用状态的变化如何响应 action 并发送到 store 的。
reducer是一个纯函数,接受旧的state和action,返回一个新的state。
需要谨记 reducer 一定要保持纯净。
// 第一个参数是es6的默认参数写法
function reducer(state = initialState, action) {
switch (action.type) {
case "INCRMENT":
return { ...state, counter: state.counter + 1 };
case "DECRMENT":
return { ...state, counter: state.counter - 1 };
case "Add_NUM":
return { ...state, counter: state.counter + action.num }
case "SUB_NUM":
return { ...state, counter: state.counter - action.num }
default:
return state
}
}
3.核心概念: Store
Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
Redux 提供createStore
这个函数,用来生成 Store。
const redux = require("redux");
const store = redux.createStore(fn);
上面代码中,createStore
函数接受另一个函数作为参数,返回新生成的 Store 对象。
前面两个核心概念是使用action描述 "发生了什么",reducer来根据它的参数来更新state的用法。
Store 就是把它们联系到一起的对象。
Store 有以下职责:
- 维持应用的 state;
- 提供
getState()
方法获取 state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器。
const redux = require("redux");
const initialState = {
counter: 0
}
function reducer(state = initialState, action) {
switch (action.type) {
case "INCRMENT":
return { ...state, counter: state.counter + 1 };
case "DECRMENT":
return { ...state, counter: state.counter - 1 };
case "Add_NUM":
return { ...state, counter: state.counter + action.num }
case "SUB_NUM":
return { ...state, counter: state.counter - action.num }
default:
return state
}
}
const store = redux.createStore(reducer);
store.subscribe(() => {
console.log("counter:", store.getState().counter);
});
// actions
const action1 = {
type: "INCRMENT"
}
const action2 = {
type: "DECRMENT"
}
const action3 = {
type: "Add_NUM",
num: 5
}
const action4 = {
type: "SUB_NUM",
num: 5
}
store.dispatch(action1)
store.dispatch(action2)
store.dispatch(action3)
store.dispatch(action4)
10.3redux的三大原则
1.单一数据源
- 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中
- Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;
- 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改;
2.State是只读的
- 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State
- 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state
- 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题
3.使用纯函数来执行修改
- Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state
- 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分;
- 但是所有的reducer都应该是纯函数,不能产生任何的副作用
10.4redux测试项目搭建
安装:yarn add redux
使用过程:
- 创建一个对象,作为我们保存的一些状态
创建store来存储这个state
- 创建时必须定义reducer
- 可以通过 store.getState 来获取当前的state
通过action来修改state
- dispatch来派发action
- 通常action中都有type属性,也可也携带其他属性
编写reducer中的处理代码
- reducer是一个纯函数,不需要直接修改state;
- 在派发之前可以通过store.subscribe(fn) 来监听state的变化
结构划分:
//代码在上面这个目录里面
10.5redux融入react
1.默认写法:
App.js:
Home.js:
About.js:
store文件:
,如果是上面那样来写,假如再添加一个类似页面,多处写法都差不多,咱说不够优雅:
把它们都抽离到一个函数里,函数是需要参数的,就可以把它们不相同的地方state/dispatch传过去。
2.改进:把相同代码抽离到一个文件
把所有代码放到connect.js
文件里:utils/connect
connect.js
About.js
理解connect这个函数所做的操作,它本身是一个函数返回的又是一个高阶函数(组件),和它传入的两个参数是回调函数,一定要多写几遍。
3.再改进:更加独立的connect
如果要把connect文件打包发布到npm共所有人使用,目前来说是做不到的,因为它里面依赖了store,如果别人要使用还需到connect文件里做操作,这样看来connect并不够独立。
使用createContext:
把context供所有组件使用:
在这里把store传进去了,里面就可以在context里得到传过去的值。
10.6使用react-redux
安装:yarn add react-redux
在需要用的组件引入里面的 connect
:import { connect } from 'react-redux';
用法更上面的抽离的connect一样。
Provider里面需要传入store,实际上它内部传的还是value。
10.7简单阅读react-redux源码
10.8组件中的异步操作
之前的操作中,redux里保存的counter是一个本地定义的数据
- 我们可以直接通过同步的操作来dispatch action,state就会被立即更新。
- 但是真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中
在之前学习网络请求的时候我们讲过,网络请求可以在class组件的componentDidMount中发送,所以我们可以有这样的结构:
注意:现在我们的状态都是写在initialState
对象里面
上面的代码有一个缺陷:
如何在redux中进行异步操作?
- 答案就是使用中间件(Middleware);
- 学习过Express或Koa框架的童鞋对中间件的概念一定不陌生;
- 在这类框架中,Middleware可以帮助我们在请求和响应之间嵌入一些操作的代码,比如cookie解析、日志记录、文件压缩等操作;
1.理解中间件
redux也引入了中间件(Middleware)的概念:
- 这个中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码;
- 比如日志记录、调用异步接口、添加代码调试功能等等;
我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:
- 这里官网推荐的、包括演示的网络请求的中间件是使用 redux-thunk;
redux-thunk是如何做到让我们可以发送异步的请求呢?
- 我们知道,默认情况下的dispatch(action),action需要是一个JavaScript的对象
- redux-thunk可以让dispatch(action函数),action可以是一个函数
该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数
- dispatch函数用于我们之后再次派发action;
- getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;
2.使用redux-thunk
安装:yarn add redux-thunk
1.在redux中引入applyMiddleware
2.应用安装的redux-thunk
- 通过applyMiddleware可以应用多个Middleware,返回一个enhancer
- 把返回的enhancer作为第二个参数传入createStore里
3.定义一个返回函数的action
store/index.js
About.js
ActionCreator.js
10.9 React-devtools
开头说过redux提供了一个我们可预测的状态管理, redux可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?
- redux官网为我们提供了redux-devtools的工具;
- 利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;
1.安装:
- 在对用的浏览器安装相关的插件,如Chrome浏览器在扩展商店搜索Redux-devtools即可,或者在GitHub搜索Redux-devtools进入扩展商店。
2.使用:
3.然后就可以在控制台使用了:
10.10reudx-saga
sage的中间件使用了ES6的generator语法。简单了解一下generator:
1.generator
generator的基本使用:
<script>
// 1.普通函数
// function gen() {
// }
// gen();
// 2.生成器函数 它返回的是一个generator对象
function* gen() {
yield 1;
yield 2;
yield 3;
}
// 调用一个生成器函数并不会直接返回结果,而是返回一个生成器对象我们称为iterator(迭代器)
let ret = gen();
console.log(ret.next());
// 3.调用一次next,就会消耗一次迭代器,同时它返回一个对象里面包含value和done(value的值就是当前的返回值,done是一个布尔值表示是否执行完毕)
console.log(ret.next());
console.log(ret.next());
// 4.生成器函数中代码的执行顺序
// 5.练习:定义一个生成器函数,依次可以生成1-10的数字
// function* generatorNumber() {
// for (let i = 1; i <= 10; i++) {
// yield i;
// }
// }
// let ret = generatorNumber();
// console.log(ret.next().value);
// console.log(ret.next().value);
// console.log(ret.next().value); //....
// 6.generator和promise一起使用
function* foo() {
// console.log('a');
const ret = yield new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Hello Promise");
}, 3000);
})
console.log(ret);
}
let ret = foo();
ret.next().value.then(ret => {
console.log(ret);
}) // 这里value值既然是个promise那么就可以调用它的then方法
</script>
2.使用redux-sage
redux-sage是另一个比较常用在redux中发送异步请求的中间件,它的使用相对redux-thunk来说更加的灵活。
1.安装redux-sage:
yarn add redux-saga
2.集成redux-saga中间件
3.saga.js
- takeEvery:可以传入多个监听的actinType,每一个都可以被执行(对应有一个takeLatest,只能执行一个,会取消前面的)
- put:在saga中派发action不再是通过dispatch,而是通过put;
- all:可以在yield的时候put多个action;
理一理思路:
1.首先导入saga函数,应用到middleware,然后启动saga并且需要传入一个生成器函数,
2.新建saga.js 来存放生成器函数,然后再这个文件里创建一个生成器函数把它导出,saga启动监听的生成器函数就是它。
3.在里面的生成器函数中使用takeEvery来监听actionType,并且做出相应的操作。
3.一些redux-saga原理
后面再看,在17集的80分钟左右开始讲。
10.11 Reducer拆分
1.为什么叫做reducer?
先来了解一下为什么这个函数叫做reducer?
其实在官方文档中已有说明:所以将reducer函数称之为reducer,因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue)
里的回调函数属于相同的类型。
可以看出来不管是它们的参数和功能都是非常的相似。
2.拆分
我们来看一下目前我们的reducer:
- 当前这个reducer既有处理counter的代码,又有处理home页面的数据;
- 后续counter相关的状态或home相关的状态会进一步变得更加复杂;
- 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;
- 如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护
因此,我们可以对reducer进行拆分:
- 我们先抽取一个对counter处理的reducer;
- 再抽取一个对home处理的reducer;
- 将它们合并起来;
暂时对它做一个这样的拆分:
每个状态都有它们自己的action、reducer、等等
home:
其实就是把自己的状态都抽到单独的文件夹中,最后把它们导出,放到主reducer中合并。
combineReducers是如何实现的呢?:
- 事实上,它也是讲我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函数了);
- 在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;
- 新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新;
具体可以查看源码来学习。
3.React中state的管理
目前来说我们主要学习了以下三种:
- 组件内部state;
- Context数据的共享状态
- Redux管理应用状态。
- 当state需要被多组件或多页面共享,我们就需要在路由发生改变的时候持久化一些data(这些应该放在redux store中)
其实state的怎样管理并没有一个标准的答案,选择怎样的状态管理,需要找一个比较平衡的方式。
目前比较好的方式是:
- UI相关的组件内部可以维护的状态,在组件内部自己来维护
- 大部分需要共享的状态,都交给redux来管理和维护
- 从服务器请求得数据(包括请求的操作-异步thunk,saga),交给redux来维护
- ..
10.12 补充一个概念:单向数据流
react官网中有提到单向数据流
- 指的是通过props进行数据的传递
- Vue和React中组件内部都有单向数据流的概念
- Redux中
形容像水流一样。只能往下传递,不能颠倒。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
评论已关闭