Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redux从零到入门笔记 #7

Open
yacan8 opened this issue May 13, 2020 · 0 comments
Open

Redux从零到入门笔记 #7

yacan8 opened this issue May 13, 2020 · 0 comments

Comments

@yacan8
Copy link
Owner

yacan8 commented May 13, 2020

为什么需要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是对之前说到一个联系和管理。具有如下职责

  • 维持应用的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
    如:
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
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封装在一起,如下:

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有三种形式,

  • 函数,该函数参数传入的是action创建的时候传入的参数,返回结果会作为到生成的actionpayload的value。
  • 数组,长度为二,第一个值为一个函数,前面的一样,返回payload的值,第二个值也为一个函数,返回meta的值,不知道有什么用。
  • 一个 actionMap对象,递归作用吧。
    例子如下
createActions({
  ADD_TODO: todo => ({ todo })
  REMOVE_TODO: [
    todo => ({ todo }), // payloa
    (todo, warn) => ({ todo, warn }) // meta
  ]
});
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吧,官方例子没看懂,如下:

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant