使用redux
让我们闭上眼睛想想,如果用一个词描述React 和Redux 给我们留下了什么印象,我想到的不是难学,不是繁琐,而是“限制”。
“限制”在这里绝不是贬义词,恰恰相反,是对技术框架的最高夸奖,因为限制能够确保程序按照可控的方式进化。
在计算机软件世界里,造物主就是人类自己,没有物理化学的限制,一切皆有可能。也正因为一切皆有可能,一个问题即使没有无数种解法,也会有很多很多种解法。
但是,拥有很多方案并不表示我们应该使用所有的方案。
软件要由程序员来维护和开发,研发部门管理也是程序员。而程序员是人,不是机器。当负担多个开发任务的时候,牵一发而动全身,bug 层出不穷,即使最专业的程序员,我想也会丧失勇气吧。
React和Redux技术框架最大的好处,并不是让我们无所不能,而是设定了一规矩,让每个模块只做最单一的事情。让开发者只能按照这套规矩来完成代码。这样,只要理解了这套规矩,无论产生的代码由谁来维护由谁来继续开发都不会有大问题。
redux其实借鉴与flux的思想,它是单向数据流的最佳实践(也许吧)。
和vuex的区别: 没有getters和actions,仅仅关注状态的变更。更加纯粹(dispatch),vuex包括dispatch和commit。 而且redux的dispatch是同步操作。redux并非react独有,适用范围非常广。但vuex高度依赖于vue。
本文将基于上一讲的水果购物车(Hook.js)继续开发。再次展示一段代码重构的过程。
源代码的注释里阐述了redux三大原则:
代码语言:javascript复制* Creates a Redux store that holds the state tree.
* The only way to change the data in the store is to call `dispatch()` on it.
* There should only be a single store in your app. To specify how different
* parts of the state tree respond to actions, you may combine several
* reducers
应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。 惟一改变 state 的办法是触发 action,一个描述发生什么的对象。 为了描述 action 如何改变 state 树,你需要编写 reducers。
安装:
代码语言:javascript复制npm i redux react-redux -S
在react下,还需要创建reac相关依赖
代码语言:javascript复制npm install --save react-redux
npm install --save-dev redux-devtools
创建 store
实例,在根组件比如 App.js
中注册store,通过上下文(react-redux提供的Provider)的方式注入进去。
创建store实例:createStore
createState
创建了状态并储存。全局应用中只能有一个。创建一个 store.js
store同时必须对应一个 reducer
函数:他接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。
import {createStore} from 'redux'
function fruitReducer(state={
list:[]
}, action) {
switch (action.type) {
case "init": // 初始化fruits
return {state,list:action.payload}
case "add": // 新增
return {...state, list:[...state.list,action.payload]};
default:
return state;
}
}
//创建一个全局实例
const store = createStore(fruitReducer)
export default store
Store的返回值: 保存了应用所有 state 的对象。改变 state 的惟一方法是 dispatch action。你也可以 subscribe 监听 state 的变化,然后更新 UI。
代码语言:javascript复制// 可以手动订阅更新,也可以事件绑定到视图层。
store.subscribe(() =>
console.log(store.getState())
);
注册provider
provider是为组件提供上下文环境。在根组件中
代码语言:javascript复制import { Provider } from "react-redux";
import store from "./store";
import ReduxTest from "./ReduxTest";
...
<Provider store={store}>
<ReduxTest />
</Provider>
#### 使用状态映射(connect)
store的状态,如何正确反映到组件中,dispatch如何调用?这需要react-redux提供的另外一个函数:connect
代码语言:javascript复制connect(state=>({
fruits:state.list,
}))(原来的函数组件)
原来的函数组件,映射出来,自动带上了各种状态,包括dispatch方法。(接收的这两个参数)
代码语言:javascript复制import React, { useState } from 'react';
import { useEffect } from "react";
import { connect } from 'react-redux'
// 水果列表
function FruitList({ fruits, setFruit }) {
let result = fruits.map(f => <li onClick={() => { setFruit(f) }} key={f}>{f}</li>)
return result;
}
// 添加水果
function AddFruit({ onAddFruit, fruits }) {
return (
<input type='text' onKeyUp={(e) => {
if (e.keyCode == 13) {
onAddFruit(e.target.value)
e.target.value = '';
}
e.persist()
}} />
)
}
export default connect(state=>({
fruits:state.list,
}))(function HooksTest({fruits,dispatch}) {
console.log(1,fruits)
const [fruit, setFruit] = useState("草莓");
// 全局属性
useEffect(() => {
setTimeout(() => {
// 变更状态,提交
dispatch({ type: "init", payload: ["香蕉", "西瓜"] });
}, 1000);
}, []);
return (
<div>
<p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
<AddFruit onAddFruit={pname => dispatch({ type: 'add', payload: pname })} fruits={fruits}></AddFruit>
<FruitList setFruit={setFruit} fruits={fruits}></FruitList>
</div>
);
})
如果子组件也需要,就再创建一个connect。即可。
重构
当前代码很不友好,应该重构一下。
重点思考,connect的两个参数是什么含义?
在组件中dispatch操作(add,init)会造成很大的耦合。如果能结构到组件的参数中,就好了。
首先用一个语义化的函数名指代第一个参数:
代码语言:javascript复制//给包装的组件传属性
const mapStateToProps=state=>({
fruits:state.list,
})
第二个参数本质上是一个actionCreater。它是一个对象,声称了了你想定义的action操作。
代码语言:javascript复制// store.js
// action-creater
export const init=(payload)=>({
type:'init',
payload
})
export const addFruit=(payload)=>({
type:'add',
payload
})
// 组件.js
import {addFruit,init} from '../store'
const mapDispatchToProps={
init,addFruit
}
export default connect(mapStateToProps,mapDispatchToProps)(function HooksTest({fruits,init,addFruit}) {
const [fruit, setFruit] = useState("草莓");
useEffect(() => {
setTimeout(() => {
init(["香蕉", "西瓜"])
}, 1000);
}, []);
return (
<div>
<p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
<AddFruit onAddFruit={(payload)=>addFruit(payload)} fruits={fruits}></AddFruit>
<FruitList setFruit={setFruit} fruits={fruits}></FruitList>
</div>
);
})
改完之后的代码语意清晰,春风拂面,不用注视即可看懂,简直是redux操作中的一股清流
异步处理
redux是不支持异步的。如果需要异步,需装中间件。redux中间件概念:
派发的action本来是直接到中间件中的。但经过中间件(强化器)处理后,可以做异步操作,或者打日志
- 安装redux-thunk和redux-logger:
npm i redux-thunk redux-logger-S
- 应用中间件,store.js 中
import { createStore, applyMiddleware } from "redux";
import logger from "redux-logger";
import thunk from "redux-thunk";
// 对store应用中间件,注意有先后顺序
const store = createStore(fruitReducer, applyMiddleware(logger, thunk));
- 定义异步动作
// store
// 在把异步请求的动作放到一个异步操作中。
export const asyncFetch = (payload) => {
return dispatch => {
setTimeout(() => {
dispatch({type:'init', payload: ["草莓", "香蕉"]}); }, 1000);
};
};
- 使用(hook.js)
那么原来的init都可以不要了。
代码语言:javascript复制import {addFruit,asyncFetch} from '../store'
const mapDispatchToProps = {
asyncFetch,
addFruit
};
function HooksTest({fruits,addFruit,asyncFetch}) {
const [fruit, setFruit] = useState("草莓");
// 全局属性
useEffect(() => {
asyncFetch(["草莓", "香蕉"] )
}, []);
return (
<div>
<p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
<AddFruit onAddFruit={(payload)=>addFruit(payload)} fruits={fruits}></AddFruit>
<FruitList setFruit={setFruit} fruits={fruits}></FruitList>
</div>
);
}
export default connect(mapStateToProps,mapDispatchToProps)(HooksTest)
你用类似同步的写法,轻易地写出了功能,异步操作的日志也被轻易地打印出来了。
模块化(combineReducers)
当前尽管hook.js已经非常好读,但是store还是一团糟。应该考虑把hook相关的逻辑(reducer)从是store中分离。
首先,在store文件夹下新建一个 fruitReducer.js
,把无关store本身的业务逻辑
// 把action和reducer移至fruit.redux.js
// 导出异步操作
export const asyncFetch = (payload) => {
return dispatch => {
setTimeout(() => {
dispatch({ type: 'init', payload});
}, 1000);
};
};
// action-creater
export const init = (payload) => ({
type: 'init',
payload
})
export const addFruit = (payload) => ({
type: 'add',
payload
})
export default function(state = {
list: []
}, action) {
switch (action.type) {
case "init": // 初始化fruits
return { state, list: action.payload }
case "add": // 新增
return { ...state, list: [...state.list, action.payload] };
default:
return state;
}
}
把combineReducers引入,并作为createStore的第一个参数
代码语言:javascript复制// store/index.js
import { combineReducers } from "redux";
import fruitReducer from './fruitReducer';
const store = createStore(
// 此处设置别名`fruit`
combineReducers({ fruit: fruitReducer }),
applyMiddleware(logger, thunk)
);
这时候,在组件Hook.js中,可以以 state.fruit
调用状态:
// ReduxTest.js
import {addFruit,asyncFetch} from "../store/fruitReducer";
const mapStateToProps = state => ({
fruits: state.fruit.list
});