redux从零到入门笔记

为什么需要redux

学过react的都知道,react用stateprops控制组件的渲染情况,而对于JavaScript单页面日趋复杂的今天,JavaScript需要管理越来越多的state,而这些state包括着各种乱七八糟途径来的数据。甚至有的应用的state会关系到另一个组件的状态。所以为了方便对这些state的管理以及对state变化的可控性。这个时候Redux这个东西就出来了,它可以让state的变化变得可预测。

Redux的基本概念

什么是redux?这里非权威的解释:就是一个应用的state管理库,甚至可以说是前端数据库。更包括的是管理数据。

state

state是整个应用的数据,本质上是一个普通对象。
state决定了整个应用的组件如何渲染,渲染的结果是什么。可以说,State是应用的灵魂,组件是应用的肉体。
所以,在项目开发初期,设计一份健壮灵活的State尤其重要,对后续的开发有很大的帮助。
但是,并不是所有的数据都需要保存到state中,有些属于组件的数据是完全可以留给组件自身去维护的。

action

数据state已经有了,那么我们是如何实现管理这些state中的数据的呢?那就是action,什么是action?按字面意思解释就是动作,也可以理解成,一个可能!改变state的动作包装。就这么简单。。。。
只有当某一个动作发生的时候才能够触发这个state去改变,那么,触发state变化的原因那么多,比如这里的我们的点击事件,还有网络请求,页面进入,鼠标移入。。。所以action的出现,就是为了把这些操作所产生或者改变的数据从应用传到store中的有效载荷。 需要说明的是,action是state的唯一来源。它本质上就是一个JavaScript对象,但是约定的包含type属性,可以理解成每个人都要有名字一般。除了type属性,别的属性,都可以.
那么这么多action一个个手动创建必然不现实,一般我们会写好actionCreator,即action的创建函数。调用actionCreator,给你返回一个action。这里我们可以使用 redux-actions,嗯呢,我们下文有介绍。
比如有一个counter数量加减应用,我们就有两个action,一个decrement,一个increment。 所以这里的action creator写成如下:

1
2
3
4
5
6
7
8
9
10
11
export function decrement() {
return{
type:DECREMENT_COUNTER
}
}

export function increment(){
return{
type:INCREMENT_COUNTER
}
}

那么,当action创建完成了之后呢,我们怎么触发这些action呢,这时我们是要利用dispatch,比如我们执行count增减减少动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function incrementIfOdd(){
return(dispatch,getState)=>{
const {counter} = getState();
if(counter%2==0) {
return;
}
dispatch(increment());
}
}

export function incrementAsync() {
return dispatch => {
setTimeout(() => {
dispatch(increment());
}, 1000);
};
}

为了减少样板代码,我们使用单独的模块或文件来定义 action type 常量

1
2
export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';

这么做不是必须的,在大型应用中把它们显式地定义成常量还是利大于弊的。

reducer

既然这个可能改变state的动作已经包装好了,那么我们怎么去判断并且对state做相应的改变呢?对,这就是reducer干的事情了。
reducer是state最终格式的确定。它是一个纯函数,也就是说,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
reducer对传入的action进行判断,然后返回一个通过判断后的state,这就是reducer的全部职责。如我们的counter应用:

1
2
3
4
5
6
7
8
9
10
11
12
import {INCREMENT_COUNTER,DECREMENT_COUNTER} from '../actions';

export default function counter(state = 0, action) {
switch (action.type){
case INCREMENT_COUNTER:
return state+1;
case DECREMENT_COUNTER:
return state-1;
default:
return state;
}
}

这里我们就是对增和减两个之前在action定义好的常量做了处理。
对于一个比较大一点的应用来说,我们是需要将reducer拆分的,最后通过redux提供的combineReducers方法组合到一起。 如此项目上的:

1
2
3
4
const rootReducer = combineReducers({
counter
});
export default rootReducer;

每个reducer只负责管理全局state中它负责的一部分。每个reducerstate参数都不同,分别对应它管理的那部分state数据。combineReducers()所做的只是生成一个函数,这个函数来调用你的一系列reducer,每个reducer根据它们的key来筛选出state中的一部分数据并处理, 然后这个生成的函数再将所有reducer的结果合并成一个大的对象。

store

store是对之前说到一个联系和管理。具有如下职责

  • 维持应用的state
  • 提供getState()方法获取 state
  • 提供dispatch(action)方法更新 state;
  • 通过subscribe(listener)注册监听器;
  • 通过subscribe(listener)返回的函数注销监听器。
    强调一下 Redux 应用只有一个单一的store。当需要拆分数据处理逻辑时,你应该使用reducer组合,而不是创建多个storestore的创建通过reduxcreateStore方法创建,这个方法还需要传入reducer,很容易理解:毕竟我需要dispatch一个action来改变state嘛。 应用一般会有一个初始化的state,所以可选为第二个参数,这个参数通常是有服务端提供的,传说中的Universal渲染。后面会说。。。 第三个参数一般是需要使用的中间件,通过applyMiddleware传入。
    说了这么多,actionstoreactionCreatorreducer关系就是这么如下的简单明了:

redux

结合react-redux的使用

react-reduxreduxreact的桥梁工具。
react-redux将组建分成了两大类,UI组建component和容器组建container。 简单的说,UI组建负责美的呈现,容器组件负责来帮你盛着,给你”力量”。
UI 组件有以下几个特征:

  • 只负责 UI 的呈现,不带有任何业务逻辑
  • 没有状态(即不使用this.state这个变量)
  • 所有数据都由参数(this.props)提供
  • 不使用任何 Redux 的 API
    如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    export default class Counter extends Component{
    render(){
    const { counter, increment, decrement, incrementIfOdd, incrementAsync } = this.props;
    return(
    <p>
    Clicked:{counter} times
    <button onClick={increment}>+</button>
    <button onClick={decrement}>-</button>
    <button onClick={incrementIfOdd}>increment if Odd</button>
    <button onClick={incrementAsync}>increment async</button>
    </p>
    )
    }
    }

容器组件特性则恰恰相反:

  • 负责管理数据和业务逻辑,不负责 UI 的呈现
  • 带有内部状态
  • 使用 Redux 的 API
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class App extends Component{
    render(){
    const { counter, increment, decrement, incrementIfOdd, incrementAsync } = this.props;
    return(
    <Counter
    counter={counter}
    increment={increment}
    decrement={decrement}
    incrementIfOdd={incrementIfOdd}
    incrementAsync={incrementAsync}/>
    )
    }
    }

    export default connect(
    state=>({ counter: state.counter }),
    ActionCreators
    )(App);

connect方法接受两个参数:mapStateToPropsmapDispatchToProps。它们定义了UI组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props), 后者负责输出逻辑,即将用户对 UI 组件的操作映射成Action。因为作为组件,我们只要能拿到值,能发出改变值得action就可以了,所以mapStateToPropsmapDispatchToProps正是满足这个需求的。

redux-thunk

一个比较流行的redux的action中间件,它可以让actionCreator暂时不返回action对象,而是返回一个函数,函数传递两个参数(dispatch, getState),在函数体内进行业务逻辑的封装,比如异步操作,我们至少需要触发两个action,这时候我们可以通过redux-thunk将这两个action封装在一起,如下:

1
2
3
4
5
6
7
8
const fetchDataAction = (querys) => (dispatch, getState) => {
const setLoading = createAction('SET_LOADING');
dispatch(setLoading(true)); // 设置加载中。。。
return fetch(`${url}?${querys}`).then(r => r.json()).then(res => {
dispatch(setLoading(false)); // 设置取消加载中。。。
dispatch(createAction('DATA_DO_SOMETHIN')(res))
})
}

这里我们的createCreator返回的是一个fetch对象,我们下文会介绍,我们通过dispatch触发改action

1
dispatch(fetchDataAction(querys))

在请求数据之前,通过redux-thunk我们可以先触发加载中的action,等请求数据结束之后我们可以在次触发action,使得加载中状态取消,并处理请求结果。

redux-promise

既然说到了异步action,我们可以使用redux-promise,它可以让actionCreator返回一个Promise对象。
第一种做法,我们可以参考redux-thunk的部分。
第二种做法,action对象的payload属性(相当于我们的diy参数,action里面携带的其他参数)是一个Promise对象。这需要从redux-actions模块引入createAction方法,并且写法也要变成下面这样。

1
2
3
4
5
6
7
8
9
10
import { createAction } from 'redux-actions';
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
// 发出异步 Action
dispatch(createAction(
'FETCH_DATA',
fetch(`url`).then(res => res.json())
));
}

其实redux-actionscreateAction的源码是拿到fetch对象的payload结果之后又触发了一次action

redux-actions

当我们的在开发大型应用的时候,对于大量的action,我们的reducer需要些大量的swich来对action.type进行判断。redux-actions可以简化这一烦琐的过程,它可以是actionCreator,也可以用来生成reducer,其作用都是用来简化actionreducer
主要函数有createActioncreateActionshandleActionhandleActionscombineActions

createAction

创建action,参数如下

1
2
3
4
5
6
import { createAction } from 'redux-actions';
createAction(
type, // action类型
payloadCreator = Identity, // payload数据 具体参考Flux教程
?metaCreator // 具体我也没深究是啥
)

例子如下:

1
2
3
4
5
6
7
export const increment = createAction('INCREMENT')
export const decrement = createAction('DECREMENT')

increment() // { type: 'INCREMENT' }
decrement() // { type: 'DECREMENT' }
increment(10) // { type: 'INCREMENT', payload: 10 }
decrement([1, 42]) // { type: 'DECREMENT', payload: [1, 42] }

createActions

创建多个action

1
2
3
4
5
import { createActions } from 'redux-actions';
createActions(
actionMap,
?...identityActions,
)

第一个参数actionMap为一个对象,以action type为键值,值value有三种形式,

  • 函数,该函数参数传入的是action创建的时候传入的参数,返回结果会作为到生成的actionpayload的value。
  • 数组,长度为二,第一个值为一个函数,前面的一样,返回payload的值,第二个值也为一个函数,返回meta的值,不知道有什么用。
  • 一个 actionMap对象,递归作用吧。
    例子如下
    1
    2
    3
    4
    5
    6
    7
    createActions({
    ADD_TODO: todo => ({ todo })
    REMOVE_TODO: [
    todo => ({ todo }), // payloa
    (todo, warn) => ({ todo, warn }) // meta
    ]
    });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const actionCreators = createActions({
APP: {
COUNTER: {
INCREMENT: [
amount => ({ amount }),
amount => ({ key: 'value', amount })
],
DECREMENT: amount => ({ amount: -amount }),
SET: undefined // given undefined, the identity function will be used
},
NOTIFY: [
(username, message) => ({ message: `${username}: ${message}` }),
(username, message) => ({ username, message })
]
}
});

expect(actionCreators.app.counter.increment(1)).to.deep.equal({
type: 'APP/COUNTER/INCREMENT',
payload: { amount: 1 },
meta: { key: 'value', amount: 1 }
});
expect(actionCreators.app.counter.decrement(1)).to.deep.equal({
type: 'APP/COUNTER/DECREMENT',
payload: { amount: -1 }
});
expect(actionCreators.app.counter.set(100)).to.deep.equal({
type: 'APP/COUNTER/SET',
payload: 100
});
expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
type: 'APP/NOTIFY',
payload: { message: 'yangmillstheory: Hello World' },
meta: { username: 'yangmillstheory', message: 'Hello World' }
});

第二个参数identityActions,可选参数,也是一个action type吧,官方例子没看懂,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const { actionOne, actionTwo, actionThree } = createActions({
// function form; payload creator defined inline
ACTION_ONE: (key, value) => ({ [key]: value }),

// array form
ACTION_TWO: [
(first) => [first], // payload
(first, second) => ({ second }) // meta
],

// trailing action type string form; payload creator is the identity
}, 'ACTION_THREE');

expect(actionOne('key', 1)).to.deep.equal({
type: 'ACTION_ONE',
payload: { key: 1 }
});

expect(actionTwo('first', 'second')).to.deep.equal({
type: 'ACTION_TWO',
payload: ['first'],
meta: { second: 'second' }
});

expect(actionThree(3)).to.deep.equal({
type: 'ACTION_THREE',
payload: 3,
});

handleAction

字面意思理解,处理action,那就是一个reducer,包裹返回一个reducer,处理一种类型的action type

1
2
3
4
5
6
7
import { handleAction } from 'redux-actions';

handleAction(
type, // action类型
reducer | reducerMap = Identity
defaultState // 默认state
)

当第二个参数为一个reducer处理函数时,形式如下,处理传入的state并返回新的state

1
2
3
handleAction('APP/COUNTER/INCREMENT', (state, action) => ({
counter: state.counter + action.payload.amount,
}), defaultState);

当第二个参数为reducerMap时,也为处理state并返回新的state,只是必须传入key值为nextthrow的两个函数,分别用来处理state和异常如下:

1
2
3
4
handleAction('FETCH_DATA', {
next(state, action) {...},
throw(state, action) {...},
}, defaultState);

官方推荐使用reducerMap形式,因为与ES6的generator类似。

handleActions

handleAction不同,handleActions可以处理多个action,也返回一个reducer

1
2
3
4
5
6
import { handleActions } from 'redux-actions';

handleActions(
reducerMap,
defaultState
)

reducerMapaction type为key,value与handleAction的第二个参数一致,传入一个reducer处理函数或者一个只有nextthrow两个键值的对象。
另外,键值key也可以使用createAction创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { createActions, handleActions } from 'redux-actions';

const { increment, decrement } = createActions({
'INCREMENT': amount => ({ amount: 1 }),
'DECREMENT': amount => ({ amount: -1 })
});

const reducer = handleActions({
[increment](state, { payload: { amount } }) {
return { counter: state.counter + amount }
},
[decrement](state, { payload: { amount } }) {
return { counter: state.counter + amount }
}
}, defaultState);

combineActions

将多个action或者actionCreator结合起来,看起来很少用,具体例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { increment, decrement } = createActions({
INCREMENT: amount => ({ amount }),
DECREMENT: amount => ({ amount: -amount })
});

const reducer = handleActions({
[combineActions(increment, decrement)](state, { payload: { amount } }) {
return { ...state, counter: state.counter + amount };
}
}, { counter: 10 });

expect(reducer({ counter: 5 }, increment(5))).to.deep.equal({ counter: 10 });
expect(reducer({ counter: 5 }, decrement(5))).to.deep.equal({ counter: 0 });
expect(reducer({ counter: 5 }, { type: 'NOT_TYPE', payload: 1000 })).to.equal({ counter: 5 });
expect(reducer(undefined, increment(5))).to.deep.equal({ counter: 15 });

redux-actions说到这里,大概是这样,有什么不了解看看官方文档吧。

reselect

Reselect用来记忆selectors的库,我们定义的selectors是作为函数获取state的某一部分。使用记忆能力,我们可以组织不必要的衍生数据的重渲染和计算过程,由此加速了我们的应用。具体细节大概是在mapStateToProps的时候,讲state的某一部分交给reselectselectors来管理,使用selectors的记忆功能让组件的props尽量不变化,引起不必要的渲染。
下面我们以一个todolist为例子。
当我们没有reselect的时候,我们是直接通过mapStateToProps把数据传入组件内,如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}

const mapStateToProps = (state, props) => {
return {
todolist: getVisibleTodos(state, props)
}
}

这个代码有一个潜在的问题。每当state tree改变时,selector都要重新运行。当state tree特别大,或者selector计算特别耗时,那么这将带来严重的运行效率问题。为了解决这个问题,reselect为selector设置了缓存,只有当selector的输入改变时,程序才重新调用selector函数。
这时我们把state转化为props的数据交给reselect来处理,我们重写mapStateToProps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const getVisibilityFilter = state => state.todo.showStatus

const getTodos = state => state.todo.todolist

const getVisibleTodos = createSelector([getVisibilityFilter, getTodos], (visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_COMPLETED':
return todos.filter(todo => todo.completed)
case 'SHOW_ACTIVE':
return todos.filter(todo => !todo.completed)
default:
return todos
}
})
const mapStateToProps = (state, props) => {
const todolist = getVisibleTodos(state, props);
return {
todolist
}
}

我们使用createSelector包裹起来,将组件内需要的两个props包裹起来,然后在返回一个获取数据的函数getVisibleTodos,这样返回的todolist就不会受到一些不必要的state的变化而变化引起冲渲染。

最后

总结了那么多的用法,其实也是redux的基本用法,然后自己写了半天的todolist,把上面说到的技术都用了,这是 github地址,上面的内容如有错误,勿喷,毕竟入门级别。。。