0%

对redux的理解以及结合react的使用

1.Redux是什么?

官方定义是:对于JavaScript应用而已,Redux是一个可预测状态的“容器”。

设计哲学:

1.数据来源单一性。不论是像计数器一样,还是复杂的聊天系统,我们都使用一个JavaScript对象来表述整个状态机的全部状态,并存储到store当中,而这个store是唯一的。获取状态:let state = store.getState();
2.状态是只读的。即store.getState()返回的结果是只读的,不允许改变它。当页面需要新的数据状态时再生成一颗全新的状态数据树,使得store.getState()返回一个全新的JavaScript对象。Redux规定:当页面需要展现新的数据状态时,我们只需要dispatch一个action即可。
3.使用reducer函数来接收action,并执行页面状态数据树的变更。经过reducer函数处理之后,store.getState()就会返回新页面的数据状态。当然,经过reducer函数根据处理后,返回一个新的JavaScript对象,而不会对原返回值进行更改,因此reducer是一个纯函数。reducer并不直接更改页面的状态数据树,而是根据action产生一颗新的页面状态数,并把它应用在store.getState()当中。
reducer和action需要开发者编写,reducer接收两个参数:当前的页面数据状态和被派发的action
(previousState,action) => newState

2.Redux基本使用和实践

store

store是一个可预测状态的“容器”,保存着整个页面状态数据树,提供了重要的API。
API:

  • dispatch(action):派发action。
  • subscribe(listener):订阅页面数据状态,即store中state的变化。
  • getState:获取当前页面状态数据树,即store中的state。
  • replaceReducer(nextReducer):一般用不到…我也不知道用来干嘛的?

创建store

1
2
3
4
5
import { createStore } from 'redux';
const store = createStore(reducer,preloadedState,enhancer);
// reducer:为开发者编写的reducer函数,必需。
// preloadedState:页面数据状态数的初始数据,可选。
// enhancer:增强器,函数类型,可选。一般接入中间件applyMiddleware(middleware)

action

action描述了状态变更的信息,也就是需要页面做出的变化。这是由开发者定义并借助store.dispatch()派发的。action也是一个对象,Redux规定:action对象需要有一个type属性,作为确定这个action的名称。
action构造器一般为:

1
2
3
4
const actionCreator = data => {
type:'ACTION_TYPE',
data
}

然后使用dispatch派发action,dispatch来自store对象暴露的方法,负责派发action,这个action将作为dispatch的参数。
store.dispatch(actionCreator('这里是数据')); // 派发上面的action

reducer

真正执行action的是reducer(),它必须是一个纯函数,以保证数据变化的可预测性。
reducer一般为:

1
2
3
4
5
6
7
8
9
10
11
12
const updateStateTree = (previousState = {}, action) => {
switch(action.type) {
case 'case1':
return newState1;
case 'case2':
return newState2;
default:
return previousState;
}
}
// previousState为原状态,这里设置为默认值是一个空对象。
// 当无法匹配到的时候,返回原状态。

当多个reducer时,应考虑进行合理拆分。Redux提供了一个工具函数:combineReducers,它接收一个JavaScript对象类型的参数,这个对象的键值分别为页面数据状态分片和子reducer函数,最后合并返回一个reducer。
let resultReducer = combineReducer({reducer1,reducer2});
多个reducer时,往往将reducer命名为其处理的页面状态数据树中的键值,例如有下面的状态数据树:

1
2
3
4
5
const state = {
data1: { ... },
data2: { ... },
data3: { ... }
}

然后reducer命名为:

1
2
3
const data1 = (state.data1,action) => { ... };
const data2 = (state.data2,action) => { ... };
const data3 = (state.data3,action) => { ... };

最后合并reducer:
const resultReducer = combineReducers({data1,data2,data3});

总结

当通过Redux的createStore()创建一个store实例后,我们便可以使用store.dispatch()派发一个action,这个action需要开发者结合自身业务去编写。同时在执行store.dispatch()之后,Redux会“自动”帮我们执行处理变化并更新数据的reducer函数。从store.dispatch()到reducer这个过程可以认为是Redux内部处理的,但具体的action以及reducer需要开发者编写,以完成应用的需求。那么当页面数据状态得到更新之后,实际上就需要store.subscribe(callbackFn)方法订阅数据的更新,并完成UI更新。
avatar

3.Redux开发基础

在Redux架构下保证reducer的不可变性

对于reducer是要保证它不可被修改的,不应该直接更改原有对象或者数组的值,因为他们是引用类型。

1).数组操作

1.增加一项:
显然push()不能满足需求,它会改变原来的数组,可以考虑用**concat()**,它返回一个新的数组。

1
2
3
4
5
6
7
let arr = [1,2,3];
const addArrReducer = (arr,action) => {
return arr.concat(action.data);
}
let newArr = addArrReducer(arr,{type: 'ADD', data:[4]});
console.log(arr); // [1,2,3]
console.log(newArr); // [1,2,3,4]

2.删除一项:
对于删除数组某一项,splice也不能满足需求,它会改变原来的数组,可以考虑用slice()。

1
2
3
4
5
6
7
const rmArrReducer = (arr,index) => {
return [
...arr.slice(0,index),
...arr.slice(index+1)
]
}
// 一般来讲index参数往往出现在action的负载数据中,如action.payload

3.更新一项:
同上面一致使用slice()来更新

1
2
3
4
5
6
7
8
9
const insertOneReducer = (arr,index) => {
return [
...arr.slice(0,index),
arr[index] + 1,
...arr.slice(index+1)
]
}
// 一般来讲index参数往往出现在action的负载数据中,如action.payload
// 某一项加1操作

2).对象操作

1.更新一项
直接修改会违背纯函数原则,一般选择ES Next新特性的Object.assign()来更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let item = {
id:0,
book:'JavaScript',
readed:false
}
const setReaded = (item) => {
return Object.assign({},item,{
readed:true
});
}
// 或者使用对象扩展运算符(推荐)
const setReaded = (item) => {
return {
...item,
readed:true
}
}

2.增加一项
与上面同理,一般使用对象扩展运算符。
3.删除一项

1
2
3
4
5
6
7
8
9
10
11
12
let item = {
id:0,
book:'JavaScript',
readed:false,
note:13
}
const newItem = Object.keys(item).reduce((obj,key) => {
if(key !== 'note') {
return { ...obj, [key] : item[key]}
}
return obj
}, {});

4.深入拷贝嵌套数据
需要注意的是:Object.assign()以及扩展运算符等都是浅操作。如果在item外在嵌套一层:

1
2
3
4
5
6
7
8
9
10
11
12
13
let data = {
item1:{
id:0,
book:'javascript',
readed:false
}
}
// 应该需要手动分开所有层分别拷贝,实现一种深拷贝
let item1 = Object.assign({}, data.item1);
let newDate = Object.assign({}, {item1});
newData.item1.readed = true; // 修改内层对象某一项
console.log(data.item1.readed); // false
console.log(newData.item1.readed); // true

当然,也可以自己实现一个深拷贝函数。

Redux中间件和异步

初始Redux中间件

中间件可以在派发任何一个action和执行reducer这两步之间,添加自定扩展功能,例如 异步请求、日志打印等等。
流程如图:
avatar
中间件可以在action到达reducer之前进行日志记录、中断action触发,甚至修改action,或者不进行处理。可以接入多个中间件。
使用:

1
2
3
4
5
6
7
8
9
import { createStore,applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
applyMiddleware(thunk,logger) // 注意这里的logger要放在所有中间件最后方可生效!
)
// 这里省略了第二个参数,如果有初始化数据,那么applyMiddleware(...arg)将作为第三个参数

Redux的异步处理

dispatch()派发的默认参数只能是一个JavaScript对象,如果要异步处理,则dispatch()接收一个函数为参数,在函数体内进行异步操作,并在异步完成后在派发相应的action。
而redux-thunk中间件正是解决了异步处理问题!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 引入redux创建store和应用中间件等代码省略...
store.dispatch(fetchNewBook('learnRedux'));
function fetchNewBook(book) {
return (dispatch) => {
// 即将请求,可在对应reducer处理渲染“转圈圈”的UI
dispatch({
type:'START_FETCH_NEW_BOOK',
data:book
})
// ajax请求
ajax({
url:`/api/${book}.json`,
type:'POST',
data:{}
}).then(res => {
// 请求成功后reducer根据action.data(即res)来进行页面状态更新
dispatch({
type:'FETCH_NEW_BOOK_SUCCESS',
data:res
})
});
}
}

整体的过程如图:
avatar
redux-thunk对于异步处理的关键在于:使dispatch能够接收异步函数,我们完全可以控制dispatch响应action的时机。

4.结合react使用redux

使用react-redux库

react-redux是对React和Redux进行了连接,对Redux的方法进行了封装和增强,使得使用起来非常方便。
容器组件:指数据状态和逻辑的容器。它并不负责展示,而是只维护内部状态,进行数据分发和处理派发action。因此容器组件对Reudx是感知的,可以使用Redux的API,比如dispatch()等。
展示组件:只负责接收相应的数据,完成页面展示,它本身并不维护数据和状态。实际上,为了渲染页面,展示组件所需要的所有数据都由容器组件通过props层层传递。
avatar

描述 展示组件 容器组件
目的 展示内容 处理数据和逻辑
是否感知Redux 不感知 感知
数据来源 从props获取 从Redux state订阅获取
改变数据 通过回调props派发action 直接派发action
由谁编写 开发者 由react-redux库生产

react-redux有个最重要的方法:connect()
connect([mapStateToProps],[mapDispatchToProps],[mergeProps],[options]);
connect()是用来连接容器组件和展示组件。它的核心是将开发者定义的组件,包装转换生成容器组件。所生成的容器组件能使用Redux store中的那些数据,全由connect()的参数来确定。
一般用法:

1
connect(mapStateToProps,mapDispatchToProps)(presentationalComponent);

第一次执行的第一个参数是一个函数,其作用是给返回的组件注入props,这个props来自Redux store中的状态。所以这个函数一定要返回一个纯JavaScript对象。第一次的第二个参数可以是一个函数也可以是一个对象,如果是一个函数,则这个函数接收dispatch()以及容器的props作为参数,最终也返回一个对象;如果是一个对象,那么兼键值应该是一个函数,用来描述action的生成。第二次执行的参数是接收一个正常的展示组件,并在这基础上返回一个容器组件。
mapStateToProps一般编写形式如下:

1
2
3
4
5
const mapStateToProps = (state) => {
return {
value:state
}
}

它完成从store中选取数据并通过props传递给将要创建的容器组件。

mapDispatchToProps一般编写形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 接收函数
const mapDispatchToProps = (dispatch,ownProps) => {
return {
onAct:() => dispatch({
type:'CLICK_ACTION',
data:ownProps.data
})
}
}
// 接收对象
const mapDispatchToProps = {
onAct:(data) => {
type:'CLICK_ACTION',
data:data
}
}

mapStateToProps和mapDispatchToProps定义了展示组件需要用到的store内容。其中mapStateToProps负责输入逻辑,就是将状态数据映射到展示组件的参数(props)上;后者负责输出逻辑,即将用户对展示组件的操作映射成action。
两者个参数是可选的。当只有前者的参数时,默认情况下,dispatch()会注入最后返回的容器组件的props中,而只有后者则把前者填上null。两个都忽略时,Redux store的状态数据无法传递下来,因此返回的容器组件就不会监听store的任何变化。
最后,react-redux提供了Provider组件,一般用法是需要将Provider作为整个应用的根组件,并获取store为其prop,以便后续进行下发处理。
所以,在开发中,借助于react-redux的基本模式如下:

1
2
3
4
5
6
let App = connect(mapStateToProps,mapDispatchToProps)(presentationalComponent);
ReactDom.render(
<Provider store={store}>
<App />
</Provider>
);

注:读完《React状态管理与同构实战》第三章的理解