yacan8 / blog

杨溜溜的个人博客,个人学习知识总结
https://github.com/yacan8/blog
826 stars 118 forks source link

Redux从零到入门笔记 #7

Open yacan8 opened 4 years ago

yacan8 commented 4 years ago

为什么需要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写成如下:

export function decrement() {
    return{
        type:DECREMENT_COUNTER
    }
}

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

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

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 常量

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应用:

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方法组合到一起。 如此项目上的:

const rootReducer = combineReducers({
    counter
});
export default rootReducer;

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

store

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

redux

结合react-redux的使用

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

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

`connect`方法接受两个参数:`mapStateToProps`和`mapDispatchToProps`。它们定义了UI组件的业务逻辑。前者负责输入逻辑,即将`state`映射到 UI 组件的参数(props), 后者负责输出逻辑,即将用户对 UI 组件的操作映射成`Action`。因为作为组件,我们只要能拿到值,能发出改变值得action就可以了,所以`mapStateToProps`和`mapDispatchToProps`正是满足这个需求的。
## redux-thunk
一个比较流行的redux的action中间件,它可以让`actionCreator`暂时不返回`action`对象,而是返回一个函数,函数传递两个参数`(dispatch, getState)`,在函数体内进行业务逻辑的封装,比如异步操作,我们至少需要触发两个`action`,这时候我们可以通过`redux-thunk`将这两个`action`封装在一起,如下:
```js
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

dispatch(fetchDataAction(querys))

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

redux-promise

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

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,参数如下

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

例子如下:

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

import { createActions } from 'redux-actions';
createActions(
  actionMap,
  ?...identityActions,
)

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

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`吧,官方例子没看懂,如下:
```js
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

import { handleAction } from 'redux-actions';

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

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

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

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

handleAction('FETCH_DATA', {
  next(state, action) {...},
  throw(state, action) {...},
}, defaultState);

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

handleActions

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

import { handleActions } from 'redux-actions';

handleActions(
  reducerMap,
  defaultState
)

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

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结合起来,看起来很少用,具体例子如下:

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把数据传入组件内,如下。

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

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地址,上面的内容如有错误,勿喷,毕竟入门级别。。。