hawx1993 / tech-blog

📦My personal tech blog,not regularly update
http://sf.gg/u/trigkit4/articles
339 stars 30 forks source link

漫谈Redux & React-Redux 设计精髓 #21

Open hawx1993 opened 6 years ago

hawx1993 commented 6 years ago

漫谈Redux & React-Redux 设计精髓

Redux 设计精髓

Redux 是为 Javascript 应用而生的可预估的状态容器,而React-Redux是针对React应用而提供的可预测状态管理机制。

1514515687136

Redux 由三部分组成:action,store,reducer。action顾名思义是你发起的一个操作,然后丢给reducer,reducer接收一个之前的state和action参数,然后返回一个新的 state 给 store(state 只允许在 reducer 中进行改变)。Store是一个容器,state 存储在这个容器中,redux规定Store仅能有唯一一个,而mobx可以有多个Store。

Store

在redux中,Store 管理着应用程序的状态,我们无法直接修改Store,唯一的方法是通过reducer,而唯一可以触发reducer的是通过Store去dispatch一个action(包含状态变更的信息)。因此,要改变数据,我们需要dispatch一个action。所以,redux应用数据的改变为:

const action = {
  type: 'COMPLETE_TODO',
  payload: 0
}
1.store.dispatch(action)

2.newState = reducer(previousState, action)
// createStore
const store = createStore(
  rootReducer,
  initialState,// initialState = {}
  compose(
    applyMiddleware(...middleware)
  )
)

Redux使用“Store”将应用程序的整个状态存储在一个地方。因此,所有组件的状态都存储在Store中,并且它们从Store本身接收更新。单状态树使跟踪 随时间的变化以及调试或检查应用程序变得更容易。 593627-20160418100236976-196339185

reducer

一个reducer是一个纯函数:(previousState, action) => newState,用来执行根据指定 action 来更新 state 的逻辑。reducer决定应用数据的改变

纯函数即只要参数相同,相同的输入永远会有相同的输出,纯函数不能修改previousState。reducer每次都要返回一个新状态,新状态由旧状态和action决定。类似的:

import { createStore } from 'redux'

const MathReducer = (state = {count: 0}, action) => {
  switch (action.type){
    case 'INCREASE': return {count: state.count + 1};
    case 'DECREASE': return {count: state.count - 1};
    default: return state;
  }
}
const store = createStore(MathReducer);// createStore(rootReducer,initState)

store.subscribe(() => {
  console.log(store.getState())
})

const actions = {
  increase: () => ({type: 'INCREASE'}),
  decrease: () => ({type: 'DECREASE'})
}

store.dispatch(actions.increase()) ;// { count: 1}
store.dispatch(actions.decrease());// { count: 0}

Store中的数据通过dispatch action,但action只是提供了actionType和payload,并没有决定Store应如何更新。

reducer 接收actionType和payload,然后switch actionType,在相关条件下进行数据整合,最后结合原Store 的state和action对象生成新的state给Store,进行Store的更新

action 类型常量

其实action就是传递的数据(对象的形式),Redux将所有事件抽象为action。当dispatch action后,Store 收到了 Action 必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。

在大型项目中,为了项目规范,一般把action type定义为常量,单独放一个文件。

因此,redux缺点也是显而易见的,为了一个功能,既要写reducer,还要写action,还要单独写一个文件定义action type

源码简单分析

createStore(reducer, preloadedState, enhancer)

createStore对外暴露了dispatch,subscribe,getState和replaceReducer方法。

function dispatch(action) {
    // ...省略一堆数据判断,错误捕获
    // 将当前和下一监听函数赋值给listeners
    const listeners = (currentListeners = nextListeners)
    // 遍历所有监听函数
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener();// 执行每一个监听函数
    }
    return action;//返回传入的action对象
}
function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }
    if (isDispatching) {
      throw new Error(/* some error msg*/)
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(/* some error msg*/)
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }
function getState() {
  return currentState
}

compose.js: 从右向左来组合多个单参函数

compose调用了reduce方法,将形如fn(arg1)(arg2)(arg3)...的柯里化函数按照顺序执行。

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

compose(f, g, h)形如(...args) => f(g(h(...args))),将函数h(...args)得到的结果作为参数传给函数g,以此类推。例如:

// eg.1
function add(x){
  return function(y){
    return x + y;
  }
}
compose(add(2)(3));// 5

// eg.2
function add1(x){
  return x + 10
}
function add2(a){
  return a * a;
}
compose(add1(add2(2)));// 14

applyMiddleware.js

applyMiddleware接收中间件为参数,并返回一个createStore为参数的函数。中间件会在每次dispatch的时候执行。

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error('some err msg')
    }
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

applyMiddleware把中间件放在一个chain数组中,通过compose方法,让每个中间件按照顺序依次传入dispatch参数执行,再组合出新的dispatch

React-Redux

React-Redux提供了两个重要的对象,<Provider store>connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])。redux提供一个全局的Store, react 从 store 拿去数据,store 发生变化,react 重新进行渲染。

redux与react-redux关系图

redux

Provider

Provider 是在原有的 APP 上面包一层,然后接收 store 作为 props,然后给 connect 用。connect 接收 store 提供的 state 和 action,返回给我们的 react 组件

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp);

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

Provider内的组件要使用state中的数据,需要使用connect方法进行连接:

class MyComp extends Component {
  // content...
}

const Comp = connect(...args)(MyComp);
connect

connect函数是将React组件连接到Redux Store,允许我们将 store 中的数据作为 props 绑定到组件上。用法如下:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

配合babel,使用es7 decorator:

@connect(
  (state) => ({
    increase: state.MathReducer.increase,
    // 店铺详情页数据
    decrease: state.MathReducer.decrease,
})

class Calculate extends  React.Component{
    // content...
}

decorator是一个对类进行处理的函数,第一个参数就是所要修饰的类。

对于多个reducer,redux也提供了combineReducers方法,可以把多个 reducer 合并成一个 root reducer,最后再将该reducer丢给createStore()

// root-reducers.js
const rootReducer = combineReducers({
    MathReducer: MathReducer,
    otherReducer: otherReducer
})
export default rootReducer

// store.js
import rootReducer from './root-reducers'
import {createStore} from 'redux'

let store =  createStore(rootReducer);
export default store
Redux 数据流动

第一步:调用store.dispatch(action)

如果在React项目中使用react-redux,则通过connect方法,可以将dispatch方法映射到props上。可以通过:

// increase.js
import {IncreaseActions } from '../actions/increase'
this.props.dispatch(IncreaseActions())

// increase.js
import { START_INCREASE,FINISH_INCREASE } from '../actionTypes'
const startCalculate = () => ({ type: START_INCREASE })
const finishCalculate = (payload) => {
  return {
    type: FINISH_INCREASE,
    payload: payload
  }
}
export const IncreaseActions = createFetchAction(riderSearchApi,startAddServer,finishAddServer)

第二步:Redux Store调用rootReducer

redux 收到 action 过后,调用根 reducer 并返回最新的状态数据。

第三步:接收新状态并publish给订阅者

reducer函数负责处理数据,返回新的state(数据),state变化,触发store.subscribe(),所有订阅 store.subscribe(listener)的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state。

jiajianrong commented 6 years ago

赞楼主,不过最后一段,

reducer函数负责处理数据,返回新的state(数据)

不一定是新的state哦,如果dispatch了一个无效action,state返回的还是上一个(===比较为true)