目录
- 一、理解JavaScript纯函数
- 1.1 纯函数的概念
- 1.2 副作用概念的理解
- 1.3 纯函数在函数式编程的重要性
- 二、Redux的核心思想
- 2.1 为什么需要 Redux
- 2.2 Redux的核心概念
- 2.2.1 store
- 2.2.2 action
- 2.2.3 reducer
- 2.3 Redux的三大原则
- 2.3.1 单一数据源
- 2.3.2 State是只读的
- 2.3.3 使用纯函数来执行修改
- 2.4 Redux 工作流程
- 三、Redux基本使用
- 3.1 创建Store的过程
- 3.2 dispatch派发action
- 3.3 subscribe定位state
- 3.4 代码优化
- 四、Redux 在 React中使用
- 4.1 先来一个案例
- 4.2 react-redux使用
- 4.3 组件中的异步操作
- 4.3.1 类组件生命周期中请求数据
- 4.3.2 使用中间件
- 4.4 redux-devtools
- 4.5 模块拆分
一、理解JavaScript纯函数
1.1 纯函数的概念
纯函数的维基百科定义:
- 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数
- 此函数在相同的输入值时,需产生相同的输出
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关
- 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等
纯函数概念,总结如下:
- 确定的输入,一定会产生确定的输出
- 函数在执行过程中,不能产生副作用
案例(数组的两个方法):
- slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组
- splice:splice截取数组, 会返回一个新的数组,也会对原数组进行修改
1.2 副作用概念的理解
什么是副作用?
- 副作用(side effect)表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储
纯函数在执行的过程中就是不能产生这样的副作用:
- 副作用往往是产生bug的 “温床”
1.3 纯函数在函数式编程的重要性
为什么纯函数在函数式编程中非常重要呢?
- 可以安心的编写和安心的使用
- 在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改
- 在用的时候,确定的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出
- React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的
props
不被修改 - 在下面的redux中,reducer也被要求是一个纯函数
二、Redux的核心思想
2.1 为什么需要 Redux
JavaScript开发的应用程序变得越来越复杂:
- JavaScript需要管理的状态越来越多,越来越复杂
- 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页
管理不断变化的state是非常困难的:
- 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化
- 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪
React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:
- 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享
- React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定
- Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理
- Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue、小程序),并且它非常小(包括依赖在内,只有2kb)
2.2 Redux的核心概念
2.2.1 store
可以定义一些初始化的数据,通过 reducer 传入
2.2.2 action
- store 中数据的变化,必须通过派发(dispatch)action来更新
- action是一个普通的JavaScript对象,用来描述这次更新的type和content
2.2.3 reducer
将传入的state和action结合起来生成一个新的state
2.3 Redux的三大原则
2.3.1 单一数据源
- 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中
- Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护
- 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改
2.3.2 State是只读的
- 唯一修改State的方法是触发action,不要试图在其他地方通过任何的方式来修改State
- 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state
- 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题
2.3.3 使用纯函数来执行修改
- 通过reducer将 旧state和 actions联系在一起,并且返回一个新的State
- 随着应用程序的复杂度增加,可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分
- 但是所有的reducer都应该是纯函数,不能产生任何的副作用
2.4 Redux 工作流程
建议看完Redux基本使用后再来看这幅图:
三、Redux基本使用
注意:以下 3 部分代码在 node 环境下
- 需要安装redux:
npm install redux
补充:node中对ES6模块化的支持
node v13.2.0
开始,对ES6模块化提供了支持:
node v13.2.0之前,需要进行如下操作:
- 在package.json中添加属性: "type": "module"
- 在执行命令中添加如下选项:node --experimental-modules src/index.js
node v13.2.0之后,只需要进行如下操作:
- 在package.json中添加属性: "type": "module"
- 注意:导入文件时,需要跟上
.js
后缀名
3.1 创建Store的过程
定义reducer:必须是一个纯函数,不要直接修改state
createStore 传入 reducer
const { createStore } = require('redux')
// 初始化的数据
const initialState = {
name: '李雷',
counter: 100
}
// 定义reducer函数:纯函数
// 两个参数:
// 参数一:store中目前保存的state
// 参数二:本次需要更新的action(dispatch传入的action)
// 返回值:返回值会作为store之后存储的state
function reducer(state = initialState, action) {
switch (action.type) {
case 'change_name':
return { ...state, name: action.name }
case 'add_numer':
return { ...state, counter: state.counter + action.num }
default:
return state
}
}
// 创建store
const store = createStore(reducer)
module.exports = store
3.2 dispatch派发action
- store 通过 dispatch 来派发 action
- 通常会有 type 属性,也可以携带其他数据
const store = require('./store')
console.log(store.getState()) // { name: '李雷', counter: 100 }
// 修改store中的数据:必须action
const nameAction = { type: 'change_name', name: '韩梅梅' }
store.dispatch(nameAction)
console.log(store.getState()) // { name: '韩梅梅', counter: 100 }
const nameAction2 = { type: 'change_name', name: '夏洛' }
store.dispatch(nameAction2)
console.log(store.getState()) // { name: '夏洛', counter: 100 }
// 修改counter
const counterAction = { type: 'add_numer', num: 10 }
store.dispatch(counterAction)
console.log(store.getState()) // { name: '夏洛', counter: 110 }
3.3 subscribe定位state
store.subscribe()
传入一个函数能够监听数据的变化store.subscribe()
会返回一个函数,执行该函数取消监听
const store = require('./store')
const unSubscribe = store.subscribe(() => {
console.log('订阅数据的变化:', store.getState())
})
// 修改store中的数据:必须action
store.dispatch({ type: 'change_name', name: '韩梅梅' })
store.dispatch({ type: 'change_name', name: '夏洛' })
// 取消订阅
unSubscribe()
// 修改counter
store.dispatch({ type: 'add_numer', num: 10 })
3.4 代码优化
- 优化方向:action的创建放到一个函数中
- 抽取到actionCreators.js文件中
- 所有的字符串常量放到constants.js文件
- reducer函数和初始化值, 放到reducer.js文件
- index.js中创建store和导出store
示例:
actionCreators.js
const { ADD_NUMBER, CHANGE_NAME } = require("./constants")
const changeNameAction = (name) => ({
type: CHANGE_NAME,
name
})
const addNumberAction = (num) => ({
type: ADD_NUMBER,
num
})
module.exports = {
changeNameAction,
addNumberAction
}
const ADD_NUMBER = "add_number"
const CHANGE_NAME = "change_name"
module.exports = {
ADD_NUMBER,
CHANGE_NAME
}
const { CHANGE_NAME, ADD_NUMBER } = require('./constants')
// 初始化的数据
const initialState = {
name: '李雷',
counter: 100
}
function reducer(state = initialState, action) {
switch (action.type) {
case CHANGE_NAME:
return { ...state, name: action.name }
case ADD_NUMBER:
return { ...state, counter: state.counter + action.num }
default:
return state
}
}
module.exports = reducer
const { createStore } = require('redux')
const reducer = require('./reducer')
// 创建store
const store = createStore(reducer)
module.exports = store
const store = require('./store')
const { changeNameAction, addNumberAction } = require('./store/actionCreators')
store.dispatch(changeNameAction('独孤月'))
store.dispatch(addNumberAction(100))
console.log(store.getState()) // { name: '独孤月', counter: 200 }
- constants.js
- reducer.js
- index.js
- util.js 中使用
四、Redux 在 React中使用
4.1 先来一个案例
有两个组件,组件上展示同一个counter,并且两者能够对counter进行操作
- 创建redux对应的store文件夹
actionCreators.js
import * as actionTypes from './constants'
export const addNumberAction = (num) => ({
type: actionTypes.ADD_NUMBER,
num
})
export const subNumberAction = (num) => ({
type: actionTypes.SUB_NUMBER,
num
})
constants.js
export const ADD_NUMBER = "add_number"
export const SUB_NUMBER = "sub_number"
reducer.js
import * as actionTypes from './constants'
const initialState = {
counter: 100
}
function reducer(state = initialState, action) {
switch (action.type) {
case actionTypes.ADD_NUMBER:
return { ...state, counter: state.counter + action.num }
case actionTypes.SUB_NUMBER:
return { ...state, counter: state.counter - action.num }
default:
return state
}
}
export default reducer
index.js
import { createStore } from "redux"
import reducer from "./reducer"
const store = createStore(reducer)
export default store
组件中使用:
import React, { PureComponent } from 'react'
// 引入store
import store from '../store'
import { addNumberAction } from '../store/actionCreators'
export default class Home extends PureComponent {
constructor() {
super()
this.state = {
counter: store.getState().counter
}
}
componentDidMount() {
store.subscribe(() => {
const state = store.getState()
this.setState({ counter: state.counter })
})
}
addNumber(num) {
store.dispatch(addNumberAction(num))
}
render() {
const { counter } = this.state
return (
<div>
<h2>Home Counter: {counter}</h2>
<div>
<button onClick={e => this.addNumber(1)}>+1</button>
<button onClick={e => this.addNumber(5)}>+5</button>
<button onClick={e => this.addNumber(8)}>+8</button>
</div>
</div>
)
}
}
import React, { PureComponent } from 'react'
// 引入store
import store from '../store'
import { subNumberAction } from '../store/actionCreators'
export default class Profile extends PureComponent {
constructor() {
super()
this.state = {
counter: store.getState().counter
}
}
componentDidMount() {
store.subscribe(() => {
const state = store.getState()
this.setState({ counter: state.counter })
})
}
subNumber(num) {
store.dispatch(subNumberAction(num))
}
render() {
const { counter } = this.state
return (
<div>
<h2>Profile Counter: {counter}</h2>
<div>
<button onClick={e => this.subNumber(1)}>-1</button>
<button onClick={e => this.subNumber(5)}>-5</button>
<button onClick={e => this.subNumber(8)}>-8</button>
</div>
</div>
)
}
}
- componentDidMount生命周期
- store.subscribe(() => {}) => this.state => render
- 修改数据:store.dispatch(addNumberAction(num))
- Home.jsx
- Profile.jsx
4.2 react-redux使用
安装:npm install react-redux
在使用时在入口文件中导入 Provider
,传入 store
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import store from './store'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
在 About 组件中使用:
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { addNumberAction, subNumberAction } from '../store/actionCreators'
export class About extends PureComponent {
clacNumber(num, isAdd) {
if(isAdd) {
this.props.addNumber(num)
} else {
this.props.subNumber(num)
}
}
render() {
const { counter } = this.props
return (
<div>
<h2>About Counter: {counter}</h2>
<button onClick={e => this.clacNumber(6, true)}>+6</button>
<button onClick={e => this.clacNumber(9, true)}>+9</button>
<button onClick={e => this.clacNumber(6, false)}>-6</button>
<button onClick={e => this.clacNumber(9, false)}>-9</button>
</div>
)
}
}
// connect() 返回值是一个高阶组件
// function mapStateToProps(state) {
// return {
// counter: state.counter
// }
// }
const mapStateToProps = (state) => ({ counter: state.counter })
const mapDispatchToProps = (dispatch) => ({
addNumber: num => dispatch(addNumberAction(num)),
subNumber: num => dispatch(subNumberAction(num))
})
export default connect(mapStateToProps, mapDispatchToProps)(About)
connect():
- 传入的第一个函数是映射当前组件所需要的数据(store中可能有很多数据,比如books、counter,而此处只需要counter)
- 传入的第二个函数是映射
dispatch
到 props - 返回一个高阶组件
4.3 组件中的异步操作
4.3.1 类组件生命周期中请求数据
- 在class组件的componentDidMount中发送请求
通过发起action
将请求的数据保存到store中
action方法:
export const changeBannersAction = (banners) => ({
type: actionTypes.CHANGE_BANNERS,
banners
})
export const changeRecommendsAction = (recommends) => ({
type: actionTypes.CHANGE_RECOMMENDS,
recommends
})
import * as actionTypes from './constants'
const initialState = {
counter: 100,
banners: [],
recommends: []
}
function reducer(state = initialState, action) {
switch (action.type) {
case actionTypes.CHANGE_BANNERS:
return { ...state, banners: action.banners }
case actionTypes.CHANGE_RECOMMENDS:
return { ...state, recommends: action.recommends }
default:
return state
}
}
export default reducer
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import axios from 'axios'
import { changeBannersAction, changeRecommendsAction } from '../store/actionCreators'
export class Category extends PureComponent {
componentDidMount() {
// 发送请求
axios.get('http://123.207.32.32:8000/home/multidata').then(res => {
const banners = res.data.data.banner.list
const recommends = res.data.data.recommend.list
this.props.changeBanners(banners)
this.props.changeRecommends(recommends)
})
}
render() {
return (
<div>
<h2>Category Page</h2>
</div>
)
}
}
const mapDispatchToProps = (dispacth) => ({
changeBanners: banners => dispacth(changeBannersAction(banners)),
changeRecommends: recommends => dispacth(changeRecommendsAction(recommends))
})
export default connect(null, mapDispatchToProps)(Category)
- reducer
- category组件
4.3.2 使用中间件
上面的代码有一个缺陷:
- 我们必须将网络请求的异步代码放到组件的生命周期中来完成
- 事实上,网络请求到的数据也属于状态管理的一部分,更好的一种方式应该是将其也交给redux来管理
如何将异步请求交给 Redux?
- 一个普通的action,返回的是一个对象
{ type: CHANGE_COUNTER, num: 10 }
- 对象中是无法直接拿到服务器请求到的异步数据的,但是如果返回一个函数呢?
- 返回一个函数,然后在组件中发起 Action 的时候,执行这个函数是不是就能够拿到数据了呢!!!
- !!! 普通的 action 不能返回函数,可以借助中间件来增强一下,让他支持返回一个函数,官网推荐的中间件:redux-thunk
- 中间件的目的:是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码
redux-thunk 做了什么呢
- 让
dispatch(action函数)
中的action可以是一个函数; - 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;
- dispatch函数用于之后再次派发action
- getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于获取之前的一些状态
代码演示:
import { createStore, applyMiddleware } from "redux"
import thunk from "redux-thunk"
import reducer from "./reducer"
// 正常情况下 store.dispatch(object)
// 想要派发函数 store.dispatch(function)
// applyMiddleware 可以传入多个中间件,","隔开
const store = createStore(reducer, applyMiddleware(thunk))
export default store
import * as actionTypes from './constants'
import axios from 'axios'
export const changeBannersAction = (banners) => ({
type: actionTypes.CHANGE_BANNERS,
banners
})
export const changeRecommendsAction = (recommends) => ({
type: actionTypes.CHANGE_RECOMMENDS,
recommends
})
export const fetchHomeMultidataAction = () => {
// 如果是一个普通的action,需要返回action对象
// 问题: 对象中不能直接拿到从服务器请求的异步数据
// redux 不允许返回一个函数,需要中间件
return (dispatch, getState) => {
// console.log(getState().counter) // 100
// 进行异步操作: 网络请求
axios.get('http://123.207.32.32:8000/home/multidata').then(res => {
const banners = res.data.data.banner.list
const recommends = res.data.data.recommend.list
// dispatch({type: actionTypes.CHANGE_BANNERS, banners})
// dispatch({type: actionTypes.CHANGE_RECOMMENDS, recommends})
dispatch(changeBannersAction(banners))
dispatch(changeRecommendsAction(recommends))
})
}
}
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { fetchHomeMultidataAction } from '../store/actionCreators'
export class Category extends PureComponent {
componentDidMount() {
this.props.fetchHomeMultidata()
}
render() {
return (
<div>
<h2>Category Page</h2>
</div>
)
}
}
const mapStateToProps = state => ({
counter: state.counter
})
const mapDispatchToProps = (dispacth) => ({
fetchHomeMultidata: () => dispacth(fetchHomeMultidataAction())
})
export default connect(mapStateToProps, mapDispatchToProps)(Category)
- store(index.js) 中引入thunk
- actionCreators.js
- 组件中使用
4.4 redux-devtools
redux可以方便的对状态进行跟踪和调试
- redux官网提供了redux-devtools的工具
- 利用这个工具,可以知道每次状态是如何被修改的,修改前后的状态变化等等
安装该工具需要两步:
- 在对应的浏览器中安装相关的插件(比如Chrome浏览器扩展商店中搜索Redux DevTools即可)
- 在redux中继承devtools的中间件
默认该工具是未开启的,开发环境开启需要进行配置,生产环境千万千万不要打开哦!!!
import { createStore, applyMiddleware, compose } from "redux"
import thunk from "redux-thunk"
import reducer from "./reducer"
// redux-devtools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)))
export default store
复制代码
4.5 模块拆分
正常情况下,我们的 store 中应该是有不同状态的数据,比如:购物车、用户信息等等, 如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。因此,我们可以对reducer进行拆分。
以上面提到的案例为例,抽取一个 counter 的reducer和一个 home 的reducer,再将其合并起来
分不同的模块,每个模块都包含自己的核心:
>reducer:接收action对象,返回最新的state
- constants:定义常量数据
- actioncreators:定义创建action对象的函数
- index:导出reducer
在 index.js
中导入每一个模块的内容,通过combineReducers
合并之后放入createStore
import { createStore, applyMiddleware, compose, combineReducers } from "redux"
import thunk from "redux-thunk"
import counterReducer from './counter'
import homeReducer from './home'
import userReducer from './user'
// 将reducer合并到一起
const reducer = combineReducers({
counter: counterReducer,
home: homeReducer,
user: userReducer
})
// redux-devtools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)))
export default store
combineReducers 如何实现合并呢?
- 事实上,它是将我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们的reducer函数了)
- 在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state
- 新的state会触发订阅者发生对应的刷新,而旧的state可以有效的阻止订阅者发生刷新
// combineReducers 原理
function reducer(state = {}, action) {
// 返回一个对象,store中的state
return {
counter: counterReducer(state.counter, action),
home: homeReducer(state.home, action),
user: userReducer(state.user, action)
}
}