Redux其實很簡單(原理篇)

在上一篇文章中,咱們經過一個示例頁面,瞭解到Redux的使用方法以及各個功能模塊的做用。若是還不清楚Redux如何使用,能夠先看看Redux其實很簡單(示例篇),而後再來看本文,理解起來會更加輕鬆。git

那麼在這一篇文章中,筆者將帶你們編寫一個完整的Redux,深度剖析Redux的方方面面,讀完本篇文章後,你們對Redux會有一個深入的認識。github

核心API

這套代碼是筆者閱讀完Redux源碼,理解其設計思路後,自行總結編寫的一套代碼,API的設計遵循與原始一致的原則,省略掉了一些沒必要要的API。redux

createStore

這個方法是Redux核心中的核心,它將全部其餘的功能鏈接在一塊兒,暴露操做的API供開發者調用。數組

const INIT = '@@redux/INIT_' + Math.random().toString(36).substring(7)

export default function createStore (reducer, initialState, enhancer) {
  if (typeof initialState === 'function') {
    enhancer = initialState
    initialState = undefined
  }

  let state = initialState
  const listeners = []
  const store = {
    getState () {
      return state
    },
    dispatch (action) {
      if (action && action.type) {
        state = reducer(state, action)
        listeners.forEach(listener => listener())
      }
    },
    subscribe (listener) {
      if (typeof listener === 'function') {
        listeners.push(listener)
      }
    }
  }

  if (typeof initialState === 'undefined') {
    store.dispatch({ type: INIT })
  }

  if (typeof enhancer === 'function') {
    return enhancer(store)
  }

  return store
}

在初始化時,createStore會主動觸發一次dispach,它的action.type是系統內置的INIT,因此在reducer中不會匹配到任何開發者自定義的action.type,它走的是switch中default的邏輯,目的是爲了獲得初始化的狀態。app

固然咱們也能夠手動指定initialState,筆者在這裏作了一層判斷,當initialState沒有定義時,咱們纔會dispatch,而在源碼中是都會執行一次dispatch,筆者認爲沒有必要,這是一次多餘的操做。由於這個時候,監聽流中沒有註冊函數,走了一遍reducer中的default邏輯,獲得新的state和initialState是同樣的。dom

第三個參數enhancer只有在使用中間件時纔會用到,一般狀況下咱們搭配applyMiddleware來使用,它能夠加強dispatch的功能,如經常使用的logger和thunk,都是加強了dispatch的功能。異步

同時createStore會返回一些操做API,包括:函數

  • getState:獲取當前的state值
  • dispatch:觸發reducer並執行listeners中的每個方法
  • subscribe:將方法註冊到listeners中,經過dispatch來觸發

applyMiddleware

這個方法經過中間件來加強dispatch的功能。性能

在寫代碼前,咱們先來了解一下函數的合成,這對後續理解applyMiddleware的原理大有裨益。this

函數的合成

若是一個值要通過多個函數,才能變成另一個值,就能夠把全部中間步驟合併成一個函數,這叫作函數的合成(compose)

舉個例子

function add (a) {
  return function (b) {
    return a + b
  }
}

// 獲得合成後的方法
let add6 = compose(add(1), add(2), add(3))

add6(10) // 16

下面咱們經過一個很是巧妙的方法來寫一個函數的合成(compose)。

export 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)))
}

上述代碼巧妙的地方在於:經過數組的reduce方法,將兩個方法合成一個方法,而後用這個合成的方法再去和下一個方法合成,直到結束,這樣咱們就獲得了一個全部方法的合成函數。

有了這個基礎,applyMiddleware就會變得很是簡單。

import { compose } from './utils'

export default function applyMiddleware (...middlewares) {
  return store => {
    const chains = middlewares.map(middleware => middleware(store))
    store.dispatch = compose(...chains)(store.dispatch)

    return store
  }
}

光看這段代碼可能有點難懂,咱們配合中間件的代碼結構來幫助理解

function middleware (store) {
  return function f1 (dispatch) {
    return function f2 (action) {
      // do something
      dispatch(action)
      // do something
    }
  }
}

能夠看出,chains是函數f1的數組,經過compose將所欲f1合併成一個函數,暫且稱之爲F1,而後咱們將原始dispatch傳入F1,通過f2函數一層一層地改造後,咱們獲得了一個新的dispatch方法,這個過程和Koa的中間件模型(洋蔥模型)原理是同樣的。

爲了方便你們理解,咱們再來舉個例子,有如下兩個中間件

function middleware1 (store) {
  return function f1 (dispatch) {
    return function f2 (action) {
      console.log(1)
      dispatch(action)
      console.log(1)
    }
  }
}

function middleware2 (store) {
  return function f1 (dispatch) {
    return function f2 (action) {
      console.log(2)
      dispatch(action)
      console.log(2)
    }
  }
}

// applyMiddleware(middleware1, middleware2)

你們猜一猜以上的log輸出順序是怎樣的?

好了,答案揭曉:1, 2, (原始dispatch), 2, 1。

爲何會這樣呢?由於middleware2接收的dispatch是最原始的,而middleware1接收的dispatch是通過middleware1改造後的,我把它們寫成以下的樣子,你們應該就清楚了。

console.log(1)

/* middleware1返回給middleware2的dispatch */
console.log(2)
dispatch(action)
console.log(2)
/* end */

console.log(1)

三個或三個以上的中間件,其原理也是如此。

至此,最複雜最難理解的中間件已經講解完畢。

combineReducers

因爲Redux是單一狀態流管理的模式,所以若是有多個reducer,咱們須要合併一下,這塊的邏輯比較簡單,直接上代碼。

export default function combineReducers (reducers) {
  const availableKeys = []
  const availableReducers = {}

  Object.keys(reducers).forEach(key => {
    if (typeof reducers[key] === 'function') {
      availableKeys.push(key)
      availableReducers[key] = reducers[key]
    }
  })

  return (state = {}, action) => {
    const nextState = {}
    let hasChanged = false

    availableKeys.forEach(key => {
      nextState[key] = availableReducers[key](state[key], action)

      if (!hasChanged) {
        hasChanged = state[key] !== nextState[key]
      }
    })

    return hasChanged ? nextState : state
  }
}

combineReucers將單個reducer塞到一個對象中,每一個reducer對應一個惟一鍵值,單個reducer狀態改變時,對應鍵值的值也會改變,而後返回整個state。

bindActionCreators

這個方法就是將咱們的action和dispatch鏈接起來。

function bindActionCreator (actionCreator, dispatch) {
  return function () {
    dispatch(actionCreator.apply(this, arguments))
  }
}

export default function bindActionCreators (actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  const boundActionCreators = {}

  Object.keys(actionCreators).forEach(key => {
    let actionCreator = actionCreators[key]

    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  })

  return boundActionCreators
}

它返回一個方法集合,直接調用來觸發dispatch。

中間件

在本身動手編寫中間件時,你必定會驚奇的發現,原來這麼好用的中間件代碼居然只有寥寥數行,卻能夠實現這麼強大的功能。

logger

function getFormatTime () {
  const date = new Date()
  return date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds() + ' ' + date.getMilliseconds()
}

export default function logger ({ getState }) {
  return next => action => {
    /* eslint-disable no-console */
    console.group(`%caction %c${action.type} %c${getFormatTime()}`, 'color: gray; font-weight: lighter;', 'inherit', 'color: gray; font-weight: lighter;')
    // console.time('time')
    console.log(`%cprev state`, 'color: #9E9E9E; font-weight: bold;', getState())
    console.log(`%caction    `, 'color: #03A9F4; font-weight: bold;', action)

    next(action)

    console.log(`%cnext state`, 'color: #4CAF50; font-weight: bold;', getState())
    // console.timeEnd('time')
    console.groupEnd()
  }
}

thunk

export default function thunk ({ getState }) {
  return next => action => {
    if (typeof action === 'function') {
      action(next, getState)
    } else {
      next(action)
    }
  }
}

這裏要注意的一點是,中間件是有執行順序的。像在這裏,第一個參數是thunk,而後纔是logger,由於假如logger在前,那麼這個時候action多是一個包含異步操做的方法,不能正常輸出action的信息。

心得體會

到了這裏,關於Redux的方方面面都已經講完了,但願你們看完可以有所收穫。

可是筆者其實還有一個擔心:每一次dispatch都會從新渲染整個視圖,雖然React是在虛擬DOM上進行diff,而後定向渲染須要更新的真實DOM,可是咱們知道,通常使用Redux的場景都是中大型應用,管理龐大的狀態數據,這個時候整個虛擬DOM進行diff可能會產生比較明顯的性能損耗(diff過程其實是對象和對象的各個字段挨個比較,若是數據達到必定量級,雖然沒有操做真實DOM,也可能產生可觀的性能損耗,在小型應用中,因爲數據較少,所以diff的性能損耗能夠忽略不計)。

本文源碼地址:https://github.com/ansenhuang/redux-demo

相關文章
相關標籤/搜索