全面剖析 Redux 原始碼

大Y發表於2019-08-01

個人Blog

程式設計模式

e.g.有一個公式,求連續自然數的平方和:

s = 1² + 2² + 3² + 4² + ... + N²
複製程式碼

指令式程式設計

用命令式是這麼解決問題的:

function squares(arr){
  var i, sum = 0, squares = []
  for(i = 0; i < arr.length; i++){
    squares.push(arr[i] * arr[i])
  }
  for(i = 0; i < squares.length; i++){
    sum += squares[i]
  }
  return sum
}
console.log(squares([1, 2, 3, 4, 5])) //55
複製程式碼

雖然說現在的你不會寫出這樣的程式碼,但以前肯定寫過類似的。別說是同事,就算是自己過半個月回來也要熟悉一會親手寫的邏輯程式碼。

也稱“業務型”程式設計,指的是用一步步下達命令最終去實現某個功能。行為過程不直觀,只關心下一步應該怎麼、然後再怎麼、最後幹什麼,卻對效能、易讀性、複用性漠不關心。

因為需求的差異化、定製化太過嚴重,依賴於後端互動、並且函數語言程式設計過於抽象,導致無法用函數語言程式設計做到高效率開發,所以現在業務的實現,大多數都偏向於指令式程式設計。但是也帶來很大的一個問題,過於重複,有位大佬(不知道誰)說過:“DRY(Don't Repeat YouSelf)”。最典型的情況莫在於產品讓你寫若干個後臺列表篩選頁面,每個頁面只是欄位不一樣而已。有些要篩選框、下拉框、搜尋建議、評論等,而有些只要輸入框,即使高階元件面對這種情況也不能做到太多複用效果。

函數語言程式設計

函數語言程式設計是宣告式的一種 —— 最經典的Haskell(老讀成HaSaKi)。近幾年大量庫所應用。和生態圈的各類元件中,它們的標籤很容易辨認 —— 不可變資料(immutable)、高階函式(柯里化)、尾遞迴、惰性序列等... 它最大的特點就是專一、簡潔、封裝性好。

用函數語言程式設計解決這個問題:

function squares(arr){
  return arr.map(d=>Math.pow(d,2))
  .reduce((p,n)=>p+n,0)
}
console.log(squares([1,2,3,4,5])) //55
複製程式碼

它不僅可讀性更高,而且更加簡潔,在這裡,我們不用去關心for迴圈和索引,我們只關心兩件事:

1.取出每個數字計算平方(map,Math.pow)

2.累加(reduce)

邏輯式程式設計

屬於稀有動物,有點像初中數學的命題推論和 Node 裡的 asset 斷言,通過一系列事實和規則,利用數理邏輯來推導或論證結論。但並不適合理論上的教學,所以沒有被廣泛採用。

差異

  • 函數語言程式設計關心資料是如何被處理的,類似於自動流水線。

  • 而指令式程式設計關心的是怎麼去做?就像是手工,先這樣做,再這樣做,然後再這樣,如果這樣,就這樣做 ...

  • 邏輯式程式設計是通過一定的規則和資料,推匯出結論,類似於asset,使用極少

他們幾個有什麼區別?這個問題對於一個非專出身有點難以理解。

函數語言程式設計關心資料的對映,指令式程式設計關心解決問題的步驟。

瞭解到這裡,相信大概的概念你也能領悟到。 引入主題,redux是函數語言程式設計很好的一門不扯皮了,我們開始幹正事

整體機制流程

全面剖析 Redux 原始碼

從入口開始

// src/redux/index.js
import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'

/*
 * This is a dummy function to check if the function name has been altered by minification.
 * If the function has been minified and NODE_ENV !== 'production', warn the user.
 */
function isCrushed() {}

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    'You are currently using minified code outside of NODE_ENV === "production". ' +
      'This means that you are running a slower development build of Redux. ' +
      'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
      'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' +
      'to ensure you have the correct code for your production build.'
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}
複製程式碼

首先會判斷在非生產環境下 isCrushed 表示在生產環境下壓縮,因為使用壓縮過後的 redux 會降低效能,這裡建立一個空函式在入口處判斷警告開發者。 三個條件:

  • 非生產環境。
  • 函式有name , IE 不支援 Function.name。所以先要用 typeof 判斷下
  • 但是名稱已經被改變 isCrushed。 壓縮後isCrushed.name !== 'isCrushed';

這裡為什麼要用 typeof isCrushed.nametypeof 有容錯保護機制,保證不會程式崩潰。

alt

對外暴露5個常用的API。 __DO_NOT_USE__ActionTypes。顧名思義不要用這裡面的幾個ActionTypes。但是隨機數的方法為什麼不用symbol防止重新命名有待思考。

// src/redux/utils/actionTypes.js
// 生成隨機數,大概輸出sqrt(36*(7-1)) = 46656次後看到重複,一般程式事件觸發不到這個次數
const randomString = () =>
  Math.random()
    .toString(36)
    .substring(7)
    .split('')
    .join('.')

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`, //用來redux內部傳送一個預設的dispatch, initialState
  REPLACE: `@@redux/REPLACE${randomString()}`, // store.replaceReducers替換當前reducer觸發的內部Actions
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}
複製程式碼

PROBE_UNKNOWN_ACTION 則是redux內部隨機檢測combineReducers合併所有reducer預設情況下觸發任何Action判斷是否返回了相同的資料。

createStore

createStore(reducer:any,preloadedState?:any,enhancer?:middleware),最終返回一個 state tree 例項。可以進行getStatesubscribe 監聽和 dispatch 派發。

createStore 接收3個引數

  • reducer: Function。給定當前state tree和要執行的action,返回下一個state tree
  • preloadedState?: any,initial state tree
  • enhancer?:middle, 增強器,若干個中介軟體可以通過 applymiddleware 產生一個增強器enhancer,多個增強器可以通過 compose 函式合併成一個增強器。
// src/redux/createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    // 檢測是否傳入了多個compose函式,丟擲錯誤,提示強制組合成一個enhancer
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function.'
    )
  }
  // 直接傳enhancer的情況
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
    // 校驗enhancer
      throw new Error('Expected the enhancer to be a function.')
    }
    // 返回建立增強後的store
    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    // 校驗reducer
    throw new Error('Expected the reducer to be a function.')
  }
  
  //60 --------------
}
複製程式碼

60行暫停一會,return enhancer(createStore)(reducer, preloadedState) 。如果傳入了 enhancer 增強器的狀態

// src/store/index.js
const logger = store => next => action => {
  console.log('logger before', store.getState())
  const returnValue = next(action)
  console.log('logger after', store.getState())
  return returnValue
}
export default function configStore(preloadedState){
  const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
  const logEnhancer = applyMiddleware(logger);// 應用中介軟體生成的增強器
  const store = createStore(
    ShopState,
    preloadedState,
    composeEnhancer(logEnhancer) // compose可以將多個增強器合併成一個增強器Plus
  )
  return store;
}
複製程式碼

最終建立store後的狀態樣子應該是

// enhancer = composeEnhancer(applyMiddleware(logger)))
enhancer(createStore)(reducer, preloadedState)
 ||
\||/
 \/
composeEnhancer(applyMiddleware(logger)))(createStore)(reducer, preloadedState)
複製程式碼

看起來是不是很複雜,沒事,我們一步一步來,先看下compose函式做了什麼。

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。這裡的compose程式碼其實只有一行,通過迭代器生成組合迭代函式。

funcs.reduce((a, b) => (...args) => a(b(...args)))
複製程式碼

其他都是做相容。最終會將compose(f,g,h...)轉化成compose(f(g(h(...))))

不同於柯里化,compose引數無限收集一次性執行,而科裡化是預先設定引數長度等待執行。而且compose(f(g(h(...))))等價於compose(h(g(f(...)))),我們來看個Demo

const a = str => str + 'a'
const b = str => str + 'b'
const c = str => str + 'c'

const compose = (...funcs) => {
    return funcs.reduce((a,b)=>(...args)=>a(b(...args)))
}
compose(a,b,c)('開始迭代了') // 開始迭代了cba
複製程式碼

compose的入參現在只有一個,直接返回自身,可以被忽略,我們可以試試傳入多個 enhancer

const enhancer = applyMiddleware(logger)
compose(enhancer,enhancer,enhancer) // 前後將會列印6次logger
複製程式碼

瞭解完了compose,我們再看applyMiddleware(logger)

applyMiddleware原始碼

// src/redux/applyMiddleware.js

import compose from './compose'

export default function applyMiddleware(...middlewares) {
   // 接受若干個中介軟體引數
   // 返回一個enhancer增強器函式,enhancer的引數是一個createStore函式。等待被enhancer(createStore)
  return createStore => (...args) => {
    // 先建立store,或者說,建立已經被前者增強過的store
    const store = createStore(...args)
    // 如果還沒有改造完成,就先被呼叫直接丟擲錯誤
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }
    // 暫存改造前的store
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 遍歷中介軟體 call(oldStore),改造store,得到改造後的store陣列
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 組合中介軟體,將改造前的dispatch傳入,每個中介軟體都將得到一個改造/增強過後的dispatch。
    dispatch = compose(...chain)(store.dispatch)

    // 最終返回一個加強後的createStore()函式
    return {
      ...store,
      dispatch
    }
  }
}
複製程式碼

能實現錯誤也是一種學習。有時候這種錯誤反而能帶來一些更直觀的感受,知道原因,在可見的未來完全可以去避免。上面丟擲錯誤的情況只有一種

function middleware(store) {
 // 監聽路由的時候 dispatch(action),由於當前還未改造完,會拋錯
  history.listen(location => { store.dispatch(updateLocation(location)) })

  return next => action => {
      if (action.type !== TRANSITION) {
        return next(action)
      }
      const { method, arg } = action
      history[method](arg)
    }
}
複製程式碼

當在map middlewares的期間,dispatch 將要在下一步應用,但是目前沒應用的時候,通過其他方法去呼叫了原生 dispatch 的某個方法,這樣很容易造成混淆,因為改變的是同一個 store ,在你 middlewares 數量多的時候,你很難去找到原因到底為什麼資料不符合預期。

核心方法是 dispatch = compose(...chain)(store.dispatch) ,現在看是不是與上面Demo的 compose(a,b,c)('開始迭代了') 看起來一模一樣?我們繼續把上面的邏輯捋一遍。假如我們有兩個中介軟體,被applyMiddleware應用,

// src/store/index.js
const logger = store => next => action => { // 列印日誌
  console.log('logger before', store.getState())
  const returnValue = next(action)
  console.log('logger after', store.getState())
  return returnValue
}

const handlerPrice = store => next => action => { // 給每次新增的商品價格補小數位
  console.log('action: ', action);
  action = {
    ...action,
    data:{
      ...action.data,
      shopPrice:action.data.shopPrice + '.00'
    }
  }
  const returnValue = next(action)
  return returnValue
}

export default function configStore(){
  const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
  const store = createStore(
    ShopState,
    undefined,
    composeEnhancer(applyMiddleware(logger,handlerPrice))) ------ enhancer
  return store;
}
複製程式碼

enhancer 最終會返回一個增強函式,我們再看一遍applyMiddleware的原始碼,得出applyMiddleware(logger,handlerPrice) 執行後將會得到一個增強器。

const logger = store => next => action => { console.log(store); next(action) }
const handlerPrice = store => next => action => { console.log(store); next(action) }
middlewares = [logger, handlerPrice]
enhancer = (createStore) => (reducer, preloadedState, enhancer) => {
    // 初始化store
    var store = createStore(reducer, preloadedState, enhancer)
    // 儲存初始化的dispatch指標
    var dispatch = store.dispatch
    var chain = []
      // 暫存改造前的store
    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    // 將store傳入,等待 logger(store) 返回的 next => action => next(action)
    // 通過閉包,每個中介軟體得到的都是同一個store即middlewareAPI。這樣就保證了資料的迭代變化
    chain = [logger, handlerPrice].map(middleware => middleware(middlewareAPI))
    /* 每次middleware(middlewareAPI) 應用中介軟體,都相當於 logger(store)一次,store也隨之改變,返回兩個next形參函式
    * [next => action => { console.log(store); next(action) },// logger
    *  next => action => { console.log(store); next(action) }] // handlerPrice
    * 隨之兩個中介軟體等待被compose, 每個都可以單獨訪問next/dispatch前後的store
    */
    dispatch = compose(...chain)(store.dispatch)
    // 先將所有的中介軟體compose合併,然後將store.dispatch作為next形數傳入,得到每個action => store.dispatch(action)
    // 也就行上文的 next(action) === store.dispatch(action)
    // 最終丟擲一個compose後的增強dispatch與store
    // 返回改造後的store
    return {
      ...store,
      dispatch
    }
}
複製程式碼

實現邏輯是通過next(action) 處理和傳遞 action 直到 redux 原生的 dispatch 接收處理。

我們回到之前的 src/redux/createStore.jsreturn enhancer(createStore)(reducer, preloadedState) ,如果看不懂的話這裡可以分解成兩步

1.const enhancedCreateStore = enhancer(createStore) //----增強的createStore函式
2.return enhancedCreateStore(reducer, preloadedState)
複製程式碼

此時將createStore傳入,enhancer(createStore)後得到一個enhancedCreateStore()生成器。

也就是上文中的 {...store,dispatch}

enhancerStore = (reducer, preloadedState, enhancer) =>{
    // ... 省略若干程式碼
  return {
    ...store,
    dispatch
  }
}
複製程式碼

此時執行第2步再將enhancerStore(reducer, preloadedState)傳入............

alt

然後就通過呼叫此時的dispatch達到一樣的效果,上面已經介紹的很詳細了,如果不熟悉的話,建議多看幾遍。

三番四次扯到中介軟體,到底是什麼東西?

中介軟體

中介軟體說起來也不陌生,至於什麼是中介軟體,維基百科的解釋大家自行查詢,本來只有一個詞不懂,看了 Wiki 變成七八個詞不懂。

在 JavaScript 裡不管是前端還是 Node,都涉及頗廣

Ps:Reduxmiddlewarekoa 流程機制不完全一樣。具體的區別可以參考 Perkin 的 Redux,Koa,Express之middleware機制對比,本段 koa 內容已隱藏,同學們可選擇性去了解。

首先了解下 Reduxmiddleware ,正常流程上來說,和 koa 是一致的,但是如果在某個正在執行的 middleware 裡派發 action,那麼將會立即“中斷” 並且重置當前 dispatch

alt
栗子:

const logger = store =>{
  return next => action => {
    console.log(1)
    next(action)
    console.log(2)
  }
}

const handlerPrice = store => next => action => {
  console.log(3)
  // 禁止直接呼叫原生store.dispatch,在知道副作用的情況下加條件執行,否則程式將崩潰
  // 如果你想派發其他的任務,可以使用next(),此時next等價於dispatch
  store.dispatch({type: 'anything' })
  next(action)
  console.log(4)
}

const enhancer = applyMiddleware(logger, handlerPrice)
const store = createStore(
    ShopState,
    null,
    composeEnhancer(enhancer,handlerPrice))
// 結果無限迴圈的1和3
1
3
1
3
...
複製程式碼

這是怎麼做到的?我們來看,在 store.dispatch({type: 'anything' }) 的時候,此時的 store 表面子上看還是原生的,但實際上 store === middlewareAPI // false ,Why ?

// src/redux/applyMiddleware.js
let dispatch = () => {
  throw new Error(
    'Dispatching while constructing your middleware is not allowed. ' +
      'Other middleware would not be applied to this dispatch.'
  )
}
// 暫存改造前的store
const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args) //--- 儲存了dispatch的引用
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch) // --- dispatch被改變
複製程式碼

dispatch 最後的引用就是 compose(...chain)(store.dispatch) ,換句話說 store.dispatch 就是一次 middleWare Loop ...

這樣就能解釋上面的程式碼了,store.dispatch({type:'anything'})其實就是從頭又調了一遍中介軟體...

接下來是Koa的栗子,你可以瞭解或者跳過

借Koa程式碼一閱,在 SandBoxCode 上手動嘗試

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next)=>{
    console.log(1)
    await next();
    console.log(2)
});

app.use(async (ctx, next) => {
    console.log(3)
    await next();
    console.log(4)
})

app.use(async (ctx, next) => {
    console.log(5)
})

app.listen(3000);
複製程式碼
1
3
5
4
2
複製程式碼

alt

上圖被稱為 洋蔥模型,很清晰的表明了一個請求是如何經過中介軟體最後生成響應。

舉一個實際的例子,你每天回到家,假設家門是個中介軟體,你的臥室門也是個中介軟體,你是一個請求。那麼你必須先進家門,再進臥室的門,你想再出去就必須先出臥室的門,再出家門。需要遵守的是,你必須原路倒序返回。 A->B->C->B->A。不能瞎蹦躂跳窗戶出去(如果你家是一樓可以走後門當我沒說)

那麼再看上面的的例子就非常簡單了。koa 通過 use 方法新增中介軟體,每個 async 函式就是你的要經過的門,而 next() 就表示你進門的動作。這不同於JavaScript執行機制中棧,更像是

        +----------------------------------------------------------------------------------+
        |                                                                                  |
        |                                 middleware 1                                     |
        |                                                                                  |
        |          +--------------------------next()---------------------------+           |
        |          |                                                           |           |
        |          |                      middleware 2                         |           |
        |          |                                                           |           |
        |          |            +-------------next()--------------+            |           |
        |          |            |         middleware 3            |            |           |
        | action   |  action    |                                 |    action  |   action  |
        | 001      |  002       |                                 |    005     |   006     |
        |          |            |   action              action    |            |           |
        |          |            |   003      next()     004       |            |           |
        |          |            |                                 |            |           |
+---------------------------------------------------------------------------------------------------->
        |          |            |                                 |            |           |
        |          |            |                                 |            |           |
        |          |            +---------------------------------+            |           |
        |          +-----------------------------------------------------------+           |
        +----------------------------------------------------------------------------------+
複製程式碼

最後再次提示:KoaReduxmiddleware 機制除了特殊狀態下是一致的,特殊狀態:在某個 middleware 內呼叫 dispatch

dispatch的妙用

回到主題,我們看61行之後的

let currentReducer = reducer // 當前reducer物件
let currentState = preloadedState  // 當前state物件
let currentListeners = [] // 當前的listeners訂閱者集合, 使用subscribe進行訂閱
let nextListeners = currentListeners // currentListeners 備份
let isDispatching = false // dispatch狀態

function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
}
//  @returns {any} ,獲取state唯一方法,如果當前正在dispatch,就丟擲一個錯誤,告訴
function getState() {
    if (isDispatching) {
      throw new Error(
          'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }
    return currentState
}
複製程式碼

這裡的錯誤也很直觀了,懂一些邏輯或英語的人基本都能明白,很無語的是這種錯誤開發過程中基本沒人遇到過,但是在18年底很多用chrome redux擴充套件程式的人遭了殃。原因應該是在初始化 的時候沒有排除在INIT階段的 dispatching===true 就直接去取資料,這裡的報錯復現只要在dispatch的時候去呼叫一次 getState() 就行了。

// src/App.js
const addShop = async () => {
    dispatch({
      type:'ADD_SHOP',
      data:{
        ...newShop,
        fn:()=> getState() // -----新增函式準備在dispatch的期間去執行它
      }
    })
}

// src/store/index
//...other
case 'ADD_SHOP': //新增商品
    newState = {
      ...newState,
      shopList:newState.shopList.concat(action.data)
    }
    action.data.fn() //----- 在這裡執行
複製程式碼

或者非同步去中介軟體獲取也會得到這個錯誤。先來分析什麼時候 isDispatching === true

  function dispatch(action) {
    // dispatch只接受一個普通物件
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }
    // action type為有效引數
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // 如果當前正在dispatch,丟擲警告,可能不會被派發出去,因為store還沒有被change完成
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      // INIT 和 dispatch 都會觸發這一步
      // 將當前的 reducer 和 state 以及 action 執行以達到更新State的目的
      currentState = currentReducer(currentState, action)
    } finally {
      // 無論結果如何,先結束dispatching狀態,防止阻塞下個任務
      isDispatching = false
    }
    // 更新訂閱者,通知遍歷更新核心資料
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener() // 將下文的subscribe收集的訂閱者通知更新
    }

    return action // 將 action 返回,在react-redux中要用到
  }
  // ... other 省略100行
  dispatch({ type: ActionTypes.INIT }) //INIT store 會觸發dispatch

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
複製程式碼

當然是在 dispatch 的時候,這是觸發 state change 的唯一方法。首先會通過遞迴原型鏈頂層是否為null來區分普通物件。

 export default function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  // 遞迴物件的原型  終點是否為null
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}
複製程式碼

這種檢測方式和 lodash 幾乎差不多,為什麼不直接用toString.call呢?原因我認為toString的雖然可行,但是隱患太多,react想讓開發者以字面量的方式建立Action,杜絕以new方式去建立action,就比如下面這種建立方式

var obj = {}
Object.getPrototypeOf(obj) === Object.prototype // true
Object.getPrototypeOf(Object.prototype) === null // true

Object.prototype.toString.call({}) // [object Object]
// but
Object.prototype.toString.call(new function Person(){}) // [object Object]
複製程式碼

看起來也沒有多難,但是我們看下redux倉庫isPlainObject的測試用例

import expect from 'expect'
import isPlainObject from '../../src/utils/isPlainObject'
import vm from 'vm'

describe('isPlainObject', () => {
  it('returns true only if plain object', () => {
    function Test() {
      this.prop = 1
    }
    const sandbox = { fromAnotherRealm: false }
    // vm.runInNewContext (沙箱) 可以在Node環境中建立新的上下文環境執行一段 js
    vm.runInNewContext('fromAnotherRealm = {}', sandbox)

    expect(isPlainObject(sandbox.fromAnotherRealm)).toBe(true)
    expect(isPlainObject(new Test())).toBe(false) // ---
    expect(isPlainObject(new Date())).toBe(false)
    expect(isPlainObject([1, 2, 3])).toBe(false)
    expect(isPlainObject(null)).toBe(false)
    expect(isPlainObject()).toBe(false)
    expect(isPlainObject({ x: 1, y: 2 })).toBe(true)
  })
})
複製程式碼

還有iframe、程式碼並非只在一個環境下執行,所以要考慮到比較多的因素,而lodash的考慮的因素更多——2.6w行測試用例...謹慎開啟,但是

alt

subscribe 新增訂閱者

可能有些同學不太清楚訂閱者模式和監聽者模式的區別

訂閱(subscribe)者模式

redux中就是使用 subscribe (譯文訂閱) , 打個比方,A告訴B,說你每次吃完飯就通知我一聲,我去洗碗,被動去請求得到對方的同意,這是訂閱者。B收集訂閱者的時候可以去做篩選是否通知A。

監聽(listen)者

A不去得到B的同意,每次B吃完飯自動去洗碗,B不管他。最典型的莫過於windowaddEventListener。B無法拒絕,只能通過A主動解綁。

程式碼分析

function subscribe(listener) {
    // 校驗訂閱函式
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }
    // 如果當前派發的時候新增訂閱者,丟擲一個錯誤,因為可能已經有部分action已經dispatch掉。不能保證通知到該listener
    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
      )
    }

    // ...other
}
複製程式碼

要復現這個問題只需要阻塞 dispatch 函式中的 currentState = await currentReducer(currentState, action),不改原始碼你可以通過上文的方法也能做到

// App.js

const addShop = () => {
    dispatch({
      type:'ADD_SHOP',
      data:{
        ...newShop, // ...商品資料
        fn:() => subscribe(() => { // -----新增函式準備在dispatch的期間去執行它
          console.log('我現在要再新增監聽者') // Error
        })
      }
    })
}
複製程式碼

然後在reducer change state 的時候去執行它,

// src/store/reducer.js
export default (state = ShopState, action)=>{
  let newState = {...state}
  switch(action.type){
    case 'ADD_SHOP': //新增商品
    newState = {
      ...newState,
      shopList:newState.shopList.concat(action.data)
    }
    action.data.fn() //---- 執行,報錯
    break
    default:
    break
  }
  return newState
}
複製程式碼

或者在中介軟體裡呼叫 subscribe 新增訂閱者也能達到相同的效果

當然,通過返回的函式你可以取消訂閱

function subscribe(listen){
    // ...other

    let isSubscribed = true // 訂閱標記

    ensureCanMutateNextListeners() // nextListener先拷貝currentListeners儲存一次快照
    nextListeners.push(listener) // 收集此次訂閱者,將在下次 dispatch 後更新該listener

    return function unsubscribe() {
      if (!isSubscribed) { // 多次解綁,已經解綁就沒有必要再往下走了
        return
      }
      // 同樣,在dispatch的時候,禁止 unsubscribed 當前listener
      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
        )
      }

      isSubscribed = false // 標記為已經 unSubscribed
      // 每次unsubscribe都要深拷貝一次 currentListeners 好讓nextListener拿到最新的 [listener] ,
       ensureCanMutateNextListeners() // 再次儲存一份快照,
      // 再對 nextListeners(也就是下次dispatch) 取消訂閱當前listener。
      const index = nextListeners.indexOf(listener)  
      nextListeners.splice(index, 1)
      currentListeners = null // 防止汙染 `ensureCanMutateNextListeners` 儲存快照,使本次處理掉的listener被重用
    }
}

  function ensureCanMutateNextListeners() {
    // 在 subscribe 和 unsubscribe 的時候,都會執行
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()  // 只有相同情況才儲存快照
    }
  }
  
複製程式碼

什麼是快照,假如現在有3個listener [A,B,C], 遍歷執行,當執行到B的時候(此時下標為1),B 的內部觸發了unsubscribe 取消訂閱者B,導致變成了[A,C],而此時下標再次變為2的時候,原本應該是C的下標此時變成了1,導致跳過C未執行。快照的作用是深拷貝當前listener,在深拷貝的listener上做事件subscribe與unSubscribe。不影響當前執行佇列

// 所以在dispatch的時候,需要明確將要釋出哪些listener
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
   const listener = listeners[i]
   listener()
}
複製程式碼

每次 dispatch() 呼叫之前都會儲存一份快照。當你在正在呼叫監聽器 listener 的時候訂閱 subscribe 或者去掉訂閱 unsubscribe,都會對當前佇列[A,B,C]沒有任何影響,你影響的只有下次 dispatch 後的listener。

currentListeners 為當前的 listener, nextListeners 為下次 dispatch 後才釋出的訂閱者集合

我們模擬下使用場景

const cancelSub = subscribe(()=>{
    if(getState().shopList.length>10) cancelSub() // 商品數量超過10個的時候,放棄訂閱更新
})
複製程式碼

首先,假設目前有0個商品,

  • 我們先通過ensureCanMutateNextListeners更新現有 currentListenernextListener(下回合的[listeners]),
  • subscribe 訂閱的事件收集到nextListeners,不影響當前 CurrentListener 的釋出更新,
  • 我們得到一個cancelSub:unsubscribe 閉包函式,該函式可以取消訂閱
  • 前10次正常釋出更新,
  • 在第11次執行的時候,商品數量增加到了11個
  • 邏輯命中 cancelSub:unsubscribe 函式被呼叫,isSubscribed被標記為0,表示當前事件已經被unSubscribed
  • 再次儲存一份快照,nextListener 為下次 dispatch 後的[listeners]
  • nextListener 上將當前 listener 移除。
  • 置空 currentListeners ,清除快取,防止汙染 ensureCanMutateNextListeners 儲存快照,使本次處理的listener被重用

replaceReducer 動態注入

原始碼

// 計算reducer,動態注入
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer

    // This action has a similiar effect to ActionTypes.INIT.
    // Any reducers that existed in both the new and old rootReducer
    // will receive the previous state. This effectively populates
    // the new state tree with any relevant data from the old one.
    dispatch({ type: ActionTypes.REPLACE })
  }
複製程式碼

結合路由能做到按需載入reducers,在專案工程較小的時候體驗不到這種優化,但是如果工程龐大的時候,initialStatedispatch 其實是很耗效能的一件事,幾十個 Reducer 包含了成百上千個 switch ,難道一個個去case?

多個dispatch 上千次case 的情景你可以想象一下。無從下手的效能優化或許可以在這上面幫你一把。今天就帶你瞭解一下 reducer 的“按需載入”,官方稱它為動態注入。

通常用來配合 webpack 實現 HMR hot module replacement

// src/store/reducers.js
import { combineReducers } from 'redux'
import { connectRouter } from 'connect-react-router'
import userReducer from '@/store/user/reducer'
const rootReducer = history => combineReducers({
    ...userReducer,
    router:connectRouter(history)
})

export default rootReducer

// src/store/index.js
import RootReducer from './reducers'
export default function configureStore(preloadState){
    const store = createStore(RootReducer,preloadState,enhancer)
    if(module.hot){
        module.hot.accpet('./reducers',()=>{ // 熱替換 reducers.js
            const hotRoot = RootReducer(history) // require語法引入則需要加.default
            store.replaceReducer(hotRoot)
        })
    }
    return store
}
複製程式碼

實現reducer按需載入

關於路由按需載入reducer,可以參考如下思路,寫了個Demo,可以在SandBoxCode上嘗試效果,去掉了其他程式碼,功能簡潔,以說明思路和實現功能為主

全面剖析 Redux 原始碼

  • Home、Index等同名檔案為新增的 storeviews 關聯
  • injectAsyncReducer封裝動態替換方法,供 PrivateRoute 呼叫,
  • reducers.js CombineReducers
  • ProviteRoute Code Spliting 與 執行生成 AsyncReducers 替換動作
// src/store/reducer.js 合併Reducers
import { combineReducers } from 'redux';
import publicState from 'store/Public';

export default function createReducer(asyncReducers) {
  return combineReducers({
   public: publicState, 
    ...asyncReducers // 非同步Reducer
  });
}
複製程式碼
// src/store/index.js
import { createStore } from '../redux/index.js';
import createReducer from './reducers';

export default function configStore(initialState) {
  const store = createStore(createReducer(),initialState);
  store.asyncReducers = {}; //  隔離防止對store其他屬性的修改
  // 動態替換方法
  function injectAsyncReducer(store, name, asyncReducer) {
    store.asyncReducers[name] = asyncReducer;
    store.replaceReducer(createReducer(store.asyncReducers));
  }
  
  return {
    store,
    injectAsyncReducer
  };
}
複製程式碼
// src/router/PrivateRoute.js
import React, { lazy, Suspense } from 'react';
import loadable from '@loadable/component'; // Code-spliting 也可以使用Suspense+lazy
import { Route, Switch } from 'react-router-dom';

const PrivateRoute = (props) => {
  const { injectAsyncReducer, store } = props;

  const withReducer = async (name) => {
    // 規定views和store關聯檔案首字母大寫
    const componentDirName = name.replace(/^\S/, s => s.toUpperCase()); 
    const reducer = await import(`../store/${componentDirName}/index`);// 引入reducer
    injectAsyncReducer(store, name, reducer.default);// 替換操作
    return import(`../views/${componentDirName}`); // 返回元件
  };
  return (
    <Suspense fallback={<div>loading...</div>}>
      <Switch>
        <Route {...props} exact path='/' name='main' component={lazy(() => withReducer('main'))} />
        <Route {...props} exact path='/home' name='home' component={lazy(() => withReducer('home'))}/>
        <Route {...props} exact path='/user' name='user' component={lazy(() => withReducer('user'))}/>
        <Route {...props} exact path='/shopList' name='shopList' component={lazy(() => withReducer('shopList'))}/>
      </Switch>
    </Suspense>
  );
};
export default PrivateRoute;
複製程式碼

這只是一個按需提供reducer的demo。最後的效果

全面剖析 Redux 原始碼

observable

function observable() {
    const outerSubscribe = subscribe;
    return {
      subscribe(observer) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.');
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState()); // 將資料同步返回
          }
        }

        observeState();
        const unsubscribe = outerSubscribe(observeState);  
        return { unsubscribe }; // 解綁事件
      },

      [$$observable]() { // 通過symbol-observable建立全域性唯一的觀察者
        return this;
      }
    };
}
複製程式碼

這個方法是為Rxjs準備的, 用來觀察物件做出相應的響應處理。

observable原本在ReactiveX中,一個觀察者(Observer)訂閱一個可觀察物件(Observable)。觀察者對Observable發射的資料或資料序列作出響應。這種模式可以極大地簡化併發操作,因為它建立了一個處於待命狀態的觀察者哨兵,在未來某個時刻響應Observable的通知,不需要阻塞等待Observable發射資料。

在實際業務中並未使用到,如果有興趣的可以參考

至此,createStore.js完結,大哥大都走過了,還有幾個小菜雞你還怕麼?

combineReducers

combineReducers用來將若干個reducer合併成一個reducers,使用方式:

combineReducers({
    key:(state = {}, action)=>{
        return state
    },
    post:(state = {}, action)=>{
        return state
    }
})
複製程式碼

176行原始碼碼大半部分全都是用來校驗資料、拋錯。

首當其衝是兩個輔助函式,用來 “友好” 的丟擲提示資訊

function getUndefinedStateErrorMessage(key, action) {
    // 如果任意一個 reducer 返回的state undefined 會踩到這個雷
  const actionType = action && action.type;
  const actionDescription =
    (actionType && `action "${String(actionType)}"`) || 'an action';
    // 即使沒有值應該返回null,而不要返回undefined
  return (
    `Given ${actionDescription}, reducer "${key}" returned undefined. ` +
    `To ignore an action, you must explicitly return the previous state. ` +
    `If you want this reducer to hold no value, you can return null instead of undefined.`
  );
}
function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  const reducerKeys = Object.keys(reducers);
  // 辨認此次操作來源是來自內部初始化還是外部呼叫,大部分都是後者
  const argumentName = action && action.type === ActionTypes.INIT  
    ? 'preloadedState argument passed to createStore'
    : 'previous state received by the reducer';

  if (reducerKeys.length === 0) { // 合併成空的reducers也會報錯
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    );
  }

  if (!isPlainObject(inputState)) { // state必須是個普通物件
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    );
  }
  // 過濾 state 與 finalReducers(也就是combineReducer定義時的有效 reducers),
  // 拿到 state 多餘的key值,比如 combineReducer 合併2個,但最後返回了3個物件
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  );
  // 標記警告這個值
  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true;
  });
  
  // 辨別來源,replaceReducers表示設定此次替代Reducer,可以被忽略
  if (action && action.type === ActionTypes.REPLACE) {
    return
    ;
  }
  // 告訴你有什麼值是多出來的,會被忽略掉
  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    );
  }
}
複製程式碼

還有一個輔助函式 assertReducerShape 用來判斷初始化和隨機狀態下返回的是不是 undefined

function assertReducerShape(reducers) {
  Object.keys(reducers).forEach(key => {
    // 遍歷 reducer
    const reducer = reducers[key];
    // 初始化該 reducer,得到一個state值
    const initialState = reducer(undefined, { type: ActionTypes.INIT });
    // 所以一般reducer寫法都是 export default (state={},action)=>{ return state}

    // 如果針對INIT有返回值,其他狀態沒有仍然是個隱患
    // 再次傳入一個隨機的 action ,二次校驗。判斷是否為 undefined
    const unknown = reducer(undefined, { type: ActionTypes.PROBE_UNKNOWN_ACTION() });

    // 初始化狀態下 state 為 undefined => 踩雷
    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
          `If the state passed to the reducer is undefined, you must ` +
          `explicitly return the initial state. The initial state may ` +
          `not be undefined. If you don't want to set a value for this reducer, ` +
          `you can use null instead of undefined.`
      );
    }
    // 隨機狀態下 為 undefined  => 踩雷
    if (typeof unknown === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
          `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
          `namespace. They are considered private. Instead, you must return the ` +
          `current state for any unknown actions, unless it is undefined, ` +
          `in which case you must return the initial state, regardless of the ` +
          `action type. The initial state may not be undefined, but can be null.`
      );
    }
  });
}
複製程式碼

輔助打野都解決了,切輸出吧。

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers);
  const finalReducers = {};// 收集有效的reducer
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i];

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        // 這個reducerKey 的 reducer是 undefined
        warning(`No reducer provided for key "${key}"`);
      }
    }

    if (typeof reducers[key] === 'function') {
      // reducer必須是函式,無效的資料不會被合併進來
      finalReducers[key] = reducers[key];
    }
  }
  // 所有可用reducer
  const finalReducerKeys = Object.keys(finalReducers);

  // This is used to make sure we don't warn about the same
  // keys multiple times.
  let unexpectedKeyCache; // 配合getUnexpectedStateShapeWarningMessage輔助函式過濾掉多出來的值
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {};
  }

  let shapeAssertionError;
  try {
    assertReducerShape(finalReducers);//校驗reducers是否都是有效資料
  } catch (e) {
    shapeAssertionError = e; // 任何雷都接著
  }
  // 返回一個合併後的 reducers 函式,與普通的 reducer 一樣
  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError;
    }

    if (process.env.NODE_ENV !== 'production') {
      // 開發環境下校驗有哪些值是多出來的
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      );
      if (warningMessage) {
        warning(warningMessage);
      }
    }

    let hasChanged = false; // mark值是否被改變
    const nextState = {};
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]; // reducerKey
      const reducer = finalReducers[key]; // 對應的 reducer
      const previousStateForKey = state[key]; // 改變之前的 state
      // 對每個reducer 做 dispatch,拿到 state 返回值
      const nextStateForKey = reducer(previousStateForKey, action);
      if (typeof nextStateForKey === 'undefined') { // 如果state是undefined就準備搞事情
        const errorMessage = getUndefinedStateErrorMessage(key, action);
        throw new Error(errorMessage);
      }
      nextState[key] = nextStateForKey; // 收錄這個reducer
      // 檢測是否被改變過
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    // 如果沒有值被改變,就返回原先的值,避免效能損耗
    return hasChanged ? nextState : state;
  };
}
複製程式碼

由於這部分較於簡單,就直接過吧。

bindActionCreators

bindActionCreators 由父元件申明,傳遞給子元件直接使用,讓子元件感受不到redux的存在,當成普通方法呼叫。

// 以import * 傳入的
import * as TodoActionCreators from './ActionCreators'
const todoAction = bindActionCreators(TodoActionCreators, dispatch) //繫結TodoActionCreators上所有的action

// 普通狀態
import { addTodoItem, removeTodoItem } from './ActionCreators'
const todoAction = bindActionCreators({ addTodoItem, removeTodoItem }, dispatch)

// 呼叫方法
todoAction.addTodoItem(args) //直接呼叫
todoAction.removeTodoItem(args)
複製程式碼

翻到原始碼,除去註釋就只有30行不到

function bindActionCreator(actionCreator, dispatch) {
  // 用apply將action進行this顯示繫結
  return function() {
    return dispatch(actionCreator.apply(this, arguments));
  };
}
export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    // 如果是函式直接繫結this
    return bindActionCreator(actionCreators, dispatch);
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) { // 校驗 action
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? 'null' : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    );
  }

  const boundActionCreators = {};
  // 如果是以import * as actions 方式引入的
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key];
    if (typeof actionCreator === 'function') {
      // 就遍歷成一個普通物件,其action繼續處理this顯示繫結
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
    }
  }
  return boundActionCreators; // 將繫結後的actions返回
}
複製程式碼

後話

在這裡分享一些遇到的問題和技巧

怎麼管理大量的reducer、action、constants?

在一般中型專案中通常會遇到這種問題: 程式碼裡存在大量的 constants 常量和 actions 冗餘程式碼

全面剖析 Redux 原始碼

然後又跑到 shop/reducers 又定義一遍,這點量還是少的,要是遇到大型專案就蛋疼了,reducer action constants 三個檔案來回切,兩個螢幕都不夠切的。雖然可以用 import * as types 方式全部引入,但是在業務元件裡還是得這樣寫

import bindActionCreators from '../redux/bindActionCreators';
import * as shop from 'store/ShopList/actionCreators'; // 所有的action

function mapDispatchToProps(dispatch) {
  return bindActionCreators(shop, dispatch);
}
複製程式碼

優雅是要靠犧牲可讀性換來的,問題迴歸到本質,為什麼要這麼做呢? 明確分工?利於查詢?統一管理?協同規範?跟隨主流?

只要利於開發,利於維護,利於協同就夠了。

所以從業務關聯的reducer入手,將 reduceraction 合併起來,每個業務單獨作為一個 reducer 檔案管理。每個 reducer 只針對一個業務。形式有點像“按需載入”。

alt

shop>store 內的負責所有 reduceraction 的建立。每個檔案單獨負責一塊內容業務。

import { 
  ADD_SHOP_BEGIN, 
  ADD_SHOP_FAIL, 
  ADD_SHOP_SUCCESS, 
  ADD_SHOP_FINALLY
} from '../constants';

export const addShopBegin = (payload) => ({
  type: ADD_SHOP_BEGIN,
  payload
});
export const addShopSuccess = (payload) => ({
  type: ADD_SHOP_SUCCESS,
  payload
});
export const addShopFail = (payload) => ({
  type: ADD_SHOP_FAIL,
  payload
});
export const addShopFinally = (payload) => ({
  type: ADD_SHOP_FINALLY,
  payload
});

export function reducer (state = { }, action) {
  let newState = { ...state };
  switch (action.type) {
  case ADD_SHOP_BEGIN:
    newState = {
      ...newState,
      hasLoaded: !newState.hasLoaded
    };
    // begin doSomething
    break;
  case ADD_SHOP_SUCCESS:
    // successful doSomething
    break;
  case ADD_SHOP_FAIL:
    // failed doSomething
    break;
  case ADD_SHOP_FINALLY:
    // whether doSomething
    break;
  default:
    break;
  }
  return newState;
}
複製程式碼

這樣做的好處是不用在兩個檔案間來回切換,業務邏輯比較清晰,方便測試。

actions

shop 模組子業務的 actions.js 則負責整合所有的 action 匯出。

export { addShopBegin, addShopSuccess, addShopFail, addShopFinally } from './store/add';
export { deleteShopBegin, deleteShopSuccess, deleteShopFail, deleteShopFinally } from './store/delete';
export { changeShopBegin, changeShopSuccess, changeShopFail, changeShopFinally } from './store/change';
export { searchShopBegin, searchShopSuccess, searchShopFail, searchShopFinally } from './store/search';
複製程式碼

constants

仍然負責上面所有的常量管理,但只在業務子模組的store 內被引入

reducers

整合該業務模組的所有 reducer,建立核心 reducer 進行遍歷,這裡核心的一點是怎麼去遍歷所有的reducer。上程式碼

import { reducer as addShop } from './store/add';
import { reducer as removeShop } from './store/delete';
import { reducer as changeShop } from './store/change';
import { reducer as searchShop } from './store/search';

const shopReducer = [ // 整合reducer
  addShop, 
  removeShop, 
  changeShop, 
  searchShop
];

let initialState = {
  hasLoaded: false 
};

export default (state = initialState, action) => {
  let newState = { ...state }; 
  // 對所有reducer進行迭代。類似於compose
  return shopReducer.reduce((preReducer, nextReducer) => {
            return nextReducer(preReducer, action)
          , newState);
};
複製程式碼

store/reducers.js

在全域性store內的reducers直接引用就可以了

import { combineReducers } from '../redux';
import { connectRouter } from 'connected-react-router';
import history from 'router/history';
import publicState from 'store/Public';
import shopOperation from './Shop/reducers';

export default function createReducer(asyncReducers) {
  return combineReducers({
    router: connectRouter(history),
    shop: shopOperation,
    public: publicState,
    ...asyncReducers// 非同步Reducer
  });
}
複製程式碼

業務元件內呼叫

業務元件內和正常呼叫即可。

import React from 'react';
import { addShopBegin } from 'store/Shop/actions';
import { connect } from 'react-redux';
import { bindActionCreators } from '../redux/index';

const Home = (props) => {
  const { changeLoaded } = props;
  return (
    <div>
      <h1>Home Page</h1>
      <button onClick={() => changeLoaded(false)}>changeLoaded</button>
    </div>
  );
};

function mapDispatchToProps(dispatch) {
  return bindActionCreators({ changeLoaded: addShopBegin }, dispatch);
}
export default connect(null, mapDispatchToProps)(Home);
複製程式碼

react 如何檢視每個元件渲染效能

你可能會用chrome performance的火焰圖去檢視整個網站的渲染時機和效能,網上教程也一大堆。

alt

  • 紫色的代表style樣式計算/layout
  • 黃色代表js操作
  • 藍色代表html解析
  • 灰色代表其他操作
  • 紅色框裡代表這個地方存在 強制迴流 、long task、FPS低、CPU 佔用過多

雖然知道總體效能,但是沒有更詳細的元件渲染週期,你不知道有哪些元件被多次重渲染,佔用主執行緒過長,是否存在效能。這時候,你可以點選上圖左側的Timings。

全面剖析 Redux 原始碼

通過這個,你能知道那些元件被重渲染哪些被掛載、銷燬、重建及更新。合理運用 Time Slicing + Suspense 非同步渲染。

Chrome 獨有的原生Api requestIdleCallback。可以在告訴瀏覽器,當你不忙(Cpu佔用較低)的時候執行這個回撥函式,類似於script標籤的async 。 如果要考慮相容性的話還是用web Worker來做一些優先順序較低的任務。

現在 Chrome Mac 版本 React Devtools 也有自己的performance了 官方傳送門

why-did-you-update

用React剛開始寫的元件基本不合規範,尤其是元件巢狀使用的時候,同級元件更新引起的不必要元件更新,導致無意義的 render ,當然,使用React Hooks的時候這個問題尤其嚴重,效能可行的情況下視覺看不出來差異,當元件複雜度量級化時候,效能損耗就體現出來了。

只需要在主檔案裡呼叫,建議加上環境限制,會有點卡

import React from 'react'
import whyDidYouUpdate from 'why-did-you-update'
if (process.env.NODE_ENV !== 'production') {
  whyDidYouUpdate(React);
}
複製程式碼

alt

它會提示你前後值是否相同,是否改變過。是不是很神奇?

其大致原理是將 React.Component.prototype.componentDidUpdate 覆蓋為一個新的函式,在其中進行了每次渲染前後的 props 的深度比較,並將結果以友好直觀的方式呈現給使用者。但它有一個明顯的缺陷——如果某一元件定義了 componentDidUpdate 方法, why-did-you-update 就失效了。參考文獻

拿到結果,分析原因,合理使用 memo/PureComponent 優化純元件,將元件進一步細分。 useMemo/reselect 快取計算結果。對於一些可以非同步載入的元件可以使用 React.lazy@loadable/component code Spliting 。 避免不必要的 render 效能損耗。

這也是 Immutable 因而誕生的一點,通過不可變資料結構,避免了資料流被更改無所謂的觸發changed。

至此 Redux 原始碼完整版刨析完畢。

由於 react-redux 增加了hooks等功能,後續會出另一篇文章,持續學習。共勉!

文中所有 原始碼備註倉庫

參考文獻

相關文章