目录
一、前言
在使用react等前端MVVM框架的时候,比较好的开发模式是组件化开发——将具有相似功能的元素抽象成一个组件,这样就可以在不同页面中引用该组件,减少了代码量,同时也缩减了开发成本。
引入组件化开发的影响就是,在开发过程中需要面对纷繁复杂的组件之间、页面之间的通信问题,虽然react有提供props
概念,允许父组件向子组件传参,但是面对更多“子组件向父组件传参”、“子组件向兄弟组件传参”等问题时就显得有些无力。
当然,只使用props
也不是不能实现奇奇怪怪的传参需求,只是实现起来比较费劲且实现方式不够优雅罢了。
这也是我学习redux
的重要原因——需要一种更优雅更有效的状态管理方式。
不过也需要注意一个误区,不要“唯redux是瞻”,redux虽然好。引用曾经有一个人的话说就是——如果你不知道是否需要 Redux,那就是不需要它。
用我的话来讲,就是使用redux之前需要先评估一下不使用redux会面临多少的工作量与使用redux之后需要面临多少的工作量
,请记住没有哪一种技术的引入是完全缩减开发成本的而不带有别的成本的,引入之前的学习成本也属于成本。
闲话不多说,都点进来了,我想以上的问题你也都考虑的比较清楚了。
二、redux基础
2.1 安装redux
先从安装说起,使用npm安装redux
npm install redux --save
2.2 redux的基本组成
redux由一下三部分组成:
- store
- action
- reducer
2.2.1 store
store
从字面意思就可以知道,他是和存储相关的 (废话,redux整个都和存储有关系) 。
这里插入一句话,redux的作用就是在你的应用全局维护一个对象,这个对象存储着你想要存储的所有数据。既然可以存数据,那用于组件之间的通信也是洒洒水啦。
store
就是一个数据仓库(就是维护了一个对象,这个对象存储了你想要存储的数据,说得这么高大上=。=)。不过请注意,一个应用有且仅有一个store(单一数据源)。且store是只读的
因为state是只读的
,所以无法在应用中直接对state变量进行修改
,如果想要修改state
必须使用store.dispatch()
方法,该方法参数是一个对象,该对象必须包含一个type
属性,用于明确触发的是什么操作来进行state的修改。有点类似于事件绑定,这里触发事件,然后响应的事件监听器接收到事件后执行相应的处理。
- 定义一个store
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer);
export default store;
第一行:引入createStore
方法,该方法用于实例化一个store
。
第二行:从本地文件引入reducer
,实例化store的时候,需要将该reducer
作为参数。(reducer的定义请继续往下看)
第三行:以reducer
为参数,调用createStore
方法,实例化一个store
。
第四行:暴露该store
以供外部引用。
2.2.2 reducer
既然用store.dispatch()
方法时“触发事件”,那么“事件监听”又在哪里定义呢?这就是reducer
的工作了。reducer
用来接收所有的store.dispatch()
方法发来的消息,并根据对应的type
值,执行相应的关于store
的操作。注意修改store
必须使用纯函数。
可能又有人要问了,什么是纯函数?已知函数的参数是一个引用类型,在函数的执行过程中,修改了该引用类型的值或者引用,这都不叫纯函数;纯函数就是在函数执行过程中不会对该引用类型的值或者引用做任何修改,对原有变量没有任何副作用的函数,就叫纯函数。
例如以下两个例子:
- 非纯函数
const user = {
name: 'zhangsan'
};
function notPureFunc(param) {
param.name = 'lisi';
return param;
}
const result = notPureFunc(user);
console.log('user = ', user);
console.log('result = ', result);
当notPureFunc()
方法执行完之后,原有的引用类型变量user
的属性值也受到了影响,最终输出结果为:
user = { name: 'lisi' }
result = { name: 'lisi' }
- 纯函数
const user = {
name: 'zhangsan'
};
function pureFunc(param) {
return Object.assign({}, param, { name: 'lisi' });
}
const result = pureFunc(user);
console.log('user = ', user);
console.log('result = ', result);
此时pureFunc()
方法执行完之后,原有的引用类型变量user
的值并未受到改变,仅仅只是再次基础上重新生成了一个新的对象并返回。最终输出结果为:
user = { name: 'zhangsan' }
result = { name: 'lisi' }
reducer
中所有关于store
的修改都要使用纯函数进行,并且返回一个全新的store
对象(该对象就是经过修改之后的新对象)。
- 定义一个reducer
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return { value: action.value };
default:
return state;
}
}
export default reducer;
reducer
根据action
的type
属性来分别执行对应的操作,并且返回一个新的state
值。
定义的reducer
方法需要定义两个参数,如下表:
参数名 | 类型 | 说明 |
---|---|---|
state | Object | 原本的state |
action | Object | 调用的action对象 |
2.2.3 action
在使用store.dispatch()
方法时,每次都要传入一个对象,作为一个数据仓库,修改同一个属性的操作可以归为一类操作,该类操作可以使用同一个type
值,如果每次在调用store.dispatch()
是都要单独写store.dispatch({ type: 'add', value: 1 })
这样的形式,就会显得比较冗余,且没有将同一type
值的action
进行统一管理,在日后的维护中会比较难以维护。
这里就需要引入action
的概念。
action是把数据从应用传到store的载体。一般来说,可以说成是通过store.dispatch()将action传递给store。
其实说了这么多行业黑话,什么载体
……不如看一下代码:
- 一个action的定义
const addAction = (value) => {
return {
type: 'add',
value
}
};
丫就是把store.dispatch()
方法中的参数统一用一个工厂函数封装了一下罢了,每次调用的时候,实例化一个对应的对象来使用。
三、Show me the code
以下从一个示例来演示如何在react应用中使用redux。该示例将会在页面中设置两个按钮,一个【+】和一个【-】,并在旁边显示一个计数器。
3.1 创建一个react程序
创建一个redux-demo
程序
npx create-react-app redux-demo
创建完之后,进入该react-demo
文件夹。
3.2 安装redux
进入该项目文件之后,执行下面语句安装redux
。
npm i redux --save
3.3 创建reducer
在redux-demo/src
文件夹下创建一个文件夹reducer
,并在该文件夹内创建index.js
文件,编写相关reducer
代码如下:
const reducer = (state, action) => {
switch(action.type) {
case 'add':
return { ...state, value: state.value + 1 };
case 'decrese':
return { ...state, value: state.value - 1 };
default:
return state;
}
}
export default reducer;
约定了两个action.type
值的操作,当点击【+】按钮时,执行add
条件语句;当点击【-】按钮时,执行decrese
条件语句。其余的action
则不会有任何修改,返回原本的state
。
这样写的话,程序最开始启动后,state
的值将会是一个undefined
,所以我们需要对state
参数赋予一个初始值。修改后的代码如下:
const initState = {
value: 0,
};
const reducer = (state = initState, action) => {
switch(action.type) {
case 'add':
return { ...state, value: state.value + 1 };
case 'decrese':
return { ...state, value: state.value - 1 };
default:
return state;
}
}
export default reducer;
3.4 创建store
在redux-demo/src
文件夹下创建一个文件夹store
,并在该文件夹内创建index.js
文件,编写相关store
代码如下:
import { createStore } from 'redux';
import reducer from '../reducer';
const store = createStore(reducer);
export default store;
3.5 创建action
在redux-demo/src
文件夹下创建一个文件夹action
,并在该文件夹内创建index.js
文件,编写相关action
代码如下:
export const addAction = () => {
return {
type: 'add',
}
}
export const decreseAction = () => {
return {
type: 'decrese',
}
}
分别定义了两个action
:
- addAction
- decreseAction
这两个action
都会返回一个带有type
属性的对象。
3.6 修改app.js
为该应用创建两个按钮,和一个显示当前计数的元素。
修改src/app.js
文件内容如下:
import React from 'react';
import store from './store';
import { addAction, decreseAction } from './action';
class App extends React.Component {
addHandler = () => {
store.dispatch(addAction());
}
decreseHandler = () => {
store.dispatch(decreseAction());
};
render() {
return (
<div className="App">
<button onClick={this.addHandler}>+</button>
<button onClick={this.decreseHandler}>-</button>
<div>{store.getState().value}</div>
</div>
);
}
}
刷新页面,点击按钮,奇怪的事情发生了,计数器的数字没有跟随按钮的点击而增加/减少。打日志进行排查也可以看到reducer
已经执行了,store
也已经更改了,但是页面上的计数器丝毫未变。
其实store
变化了不假,但是页面没有变化,是因为没有刷新store.getState().value
的值,这里需要触发重新render
一下。
改进后的代码:
import React from 'react';
import store from './store';
import { addAction, decreseAction } from './action';
class App extends React.Component {
addHandler = () => {
store.dispatch(addAction());
}
decreseHandler = () => {
store.dispatch(decreseAction());
};
// 页面挂载时添加一个store的订阅器,当store改变时,使用this.setState({})触发render
componentDidMount(){
store.subscribe(() => {
this.setState({});
})
}
render() {
return (
<div className="App">
<button onClick={this.addHandler}>+</button>
<button onClick={this.decreseHandler}>-</button>
<div>{store.getState().value}</div>
</div>
);
}
}
export default App;
再次刷新页面,可以看到,此时计数器就开始跟随按钮进行对应的加减操作了。