【轉(zhuǎn)】完全理解 redux(從零實現(xiàn)一個 redux)

本周在閱讀redux源碼時宜岛,發(fā)現(xiàn)一個文章,由簡至深功舀,甚好萍倡,原文地址在這里,學習一下辟汰。

但是本篇內(nèi)容并不是介紹redux API的列敲,redux的使用可以參考阮一峰老師的博客阱佛,或者示例

目錄

  1. 前言
  2. 狀態(tài)管理器
    • 簡單的狀態(tài)管理器
    • 有計劃的狀態(tài)管理器
  3. 多文件協(xié)作
    • reducer 的拆分和合并
    • state 的拆分和合并
  4. 中間件 middleware
  5. 完整的 redux
  6. 最佳實踐
    • 純函數(shù)
  7. 總結(jié)

前言

記得開始接觸 react 技術(shù)棧的時候,最難理解的地方就是 redux戴而。全是新名詞:reducer瘫絮、store、dispatch填硕、middleware 等等麦萤,我就理解 state 一個名詞。

網(wǎng)上找的 redux 文章扁眯,要不有一本書的厚度壮莹,要不很玄乎,晦澀難懂姻檀,越看越覺得難命满,越看越怕,信心都沒有了绣版!

花了很長時間熟悉 redux胶台,慢慢的發(fā)現(xiàn)它其實真的很簡單。本章不會把 redux 的各種概念杂抽,名詞解釋一遍诈唬,這樣和其他教程沒有任何區(qū)別,沒有太大意義缩麸。我會帶大家從零實現(xiàn)一個完整的 redux铸磅,讓大家知其然,知其所以然杭朱。

開始前阅仔,你必須知道一些事情:

  • redux 和 react 沒有關(guān)系,redux 可以用在任何框架中弧械,忘掉 react八酒。
  • connect 不屬于 redux,它其實屬于 react-redux刃唐,請先忘掉它羞迷,下一章節(jié),我們會介紹它唁桩。
  • 請一定先忘記 reducer闭树、store耸棒、dispatch荒澡、middleware 等等這些名詞。
  • redux 是一個狀態(tài)管理器与殃。

Let's Go单山!

狀態(tài)管理器

簡單的狀態(tài)管理器

redux 是一個狀態(tài)管理器碍现,那什么是狀態(tài)呢?狀態(tài)就是數(shù)據(jù)米奸,比如計數(shù)器中的 count昼接。

let state = {
  count: 1
}

我們來使用下狀態(tài)

console.log(state.count);

我們來修改下狀態(tài)

state.count = 2;

好了,現(xiàn)在我們實現(xiàn)了狀態(tài)(計數(shù))的修改和使用了悴晰。

讀者:你當我傻嗎慢睡?你說的這個誰不知道?捶你<g-emoji class="g-emoji" alias="fist_oncoming" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png" style="box-sizing: border-box; font-family: "Apple Color Emoji", "Segoe UI", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 1.4em; font-weight: 400; line-height: 20px; vertical-align: middle; font-style: normal !important; margin-right: 0px;">??</g-emoji>铡溪!

筆者:哎哎哎漂辐,別打我!有話好好說棕硫!redux 核心就是這個呀髓涯!我們一步一步擴展開來嘛!

當然上面的有一個很明顯的問題:修改 count 之后哈扮,使用 count 的地方不能收到通知纬纪。我們可以使用發(fā)布-訂閱模式來解決這個問題。

/*------count 的發(fā)布訂閱者實踐------*/
let state = {
  count: 1
};
let listeners = [];

/*訂閱*/
function subscribe(listener) {
  listeners.push(listener);
}

function changeCount(count) {
  state.count = count;
  /*當 count 改變的時候滑肉,我們要去通知所有的訂閱者*/
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i];
    listener();
  }
}

我們來嘗試使用下這個簡單的計數(shù)狀態(tài)管理器包各。

/*來訂閱一下,當 count 改變的時候靶庙,我要實時輸出新的值*/
subscribe(() => {
  console.log(state.count);
});

/*我們來修改下 state髓棋,當然我們不能直接去改 state 了,我們要通過 changeCount 來修改*/
changeCount(2);
changeCount(3);
changeCount(4);

現(xiàn)在我們可以看到惶洲,我們修改 count 的時候按声,會輸出相應(yīng)的 count 值。

現(xiàn)在有兩個新的問題擺在我們面前

  • 這個狀態(tài)管理器只能管理 count恬吕,不通用
  • 公共的代碼要封裝起來

我們嘗試來解決這個問題签则,把公共的代碼封裝起來

const createStore = function (initState) {
  let state = initState;
  let listeners = [];

  /*訂閱*/
  function subscribe(listener) {
    listeners.push(listener);
  }

  function changeState(newState) {
    state = newState;
    /*通知*/
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  function getState() {
    return state;
  }

  return {
    subscribe,
    changeState,
    getState
  }
}

我們來使用這個狀態(tài)管理器管理多個狀態(tài) counter 和 info 試試

let initState = {
  counter: {
    count: 0
  },
  info: {
    name: '',
    description: ''
  }
}

let store = createStore(initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(`${state.info.name}:${state.info.description}`);
});
store.subscribe(() => {
  let state = store.getState();
  console.log(state.counter.count);
});

store.changeState({
  ...store.getState(),
  info: {
    name: '前端九部',
    description: '我們都是前端愛好者!'
  }
});

store.changeState({
  ...store.getState(),
  counter: {
    count: 1
  }
});

到這里我們完成了一個簡單的狀態(tài)管理器铐料。

這里需要理解的是 createStore渐裂,提供了 changeStategetState钠惩,subscribe 三個能力柒凉。

本小節(jié)完整源碼見 demo-1

有計劃的狀態(tài)管理器

我們用上面的狀態(tài)管理器來實現(xiàn)一個自增,自減的計數(shù)器篓跛。

let initState = {
  count: 0
}
let store = createStore(initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(state.count);
});
/*自增*/
store.changeState({
  count: store.getState().count + 1
});
/*自減*/
store.changeState({
  count: store.getState().count - 1
});
/*我想隨便改*/
store.changeState({
  count: 'abc'
});

你一定發(fā)現(xiàn)了問題膝捞,count 被改成了字符串 abc,因為我們對 count 的修改沒有任何約束愧沟,任何地方蔬咬,任何人都可以修改鲤遥。

我們需要約束,不允許計劃外的 count 修改林艘,我們只允許 count 自增和自減兩種改變方式盖奈!

那我們分兩步來解決這個問題

  1. 制定一個 state 修改計劃,告訴 store狐援,我的修改計劃是什么钢坦。
  2. 修改 store.changeState 方法,告訴它修改 state 的時候啥酱,按照我們的計劃修改场钉。

我們來設(shè)置一個 plan 函數(shù),接收現(xiàn)在的 state懈涛,和一個 action逛万,返回經(jīng)過改變后的新的 state。

/*注意:action = {type:'',other:''}, action 必須有一個 type 屬性*/
function plan(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1
      }
    default:
      return state;
  }
}

我們把這個計劃告訴 store批钠,store.changeState 以后改變 state 要按照我的計劃來改宇植。

/*增加一個參數(shù) plan*/
const createStore = function (plan, initState) {
  let state = initState;
  let listeners = [];

  function subscribe(listener) {
    listeners.push(listener);
  }

  function changeState(action) {
    /*請按照我的計劃修改 state*/  
    state = plan(state, action);
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  function getState() {
    return state;
  }

  return {
    subscribe,
    changeState,
    getState
  }
}

我們來嘗試使用下新的 createStore 來實現(xiàn)自增和自減

let initState = {
  count: 0
}
/*把plan函數(shù)*/
let store = createStore(plan, initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(state.count);
});
/*自增*/
store.changeState({
  type: 'INCREMENT'
});
/*自減*/
store.changeState({
  type: 'DECREMENT'
});
/*我想隨便改 計劃外的修改是無效的!*/
store.changeState({
  count: 'abc'
});

到這里為止埋心,我們已經(jīng)實現(xiàn)了一個有計劃的狀態(tài)管理器指郁!

我們商量一下吧?我們給 plan 和 changeState 改下名字好不好拷呆?plan 改成 reducer闲坎,changeState 改成 dispatch!不管你同不同意茬斧,我都要換腰懂,因為新名字比較厲害(其實因為 redux 是這么叫的)!

本小節(jié)完整源碼見 demo-2

多文件協(xié)作

reducer 的拆分和合并

這一小節(jié)我們來處理下 reducer 的問題涛酗。啥問題揩尸?

我們知道 reducer 是一個計劃函數(shù),接收老的 state伦仍,按計劃返回新的 state娄蔼。那我們項目中怖喻,有大量的 state,每個 state 都需要計劃函數(shù)岁诉,如果全部寫在一起會是啥樣子呢锚沸?

所有的計劃寫在一個 reducer 函數(shù)里面,會導(dǎo)致 reducer 函數(shù)及其龐大復(fù)雜涕癣。按經(jīng)驗來說哗蜈,我們肯定會按組件維度來拆分出很多個 reducer 函數(shù),然后通過一個函數(shù)來把他們合并起來。

我們來管理兩個 state恬叹,一個 counter候生,一個 info同眯。

let state = {
  counter: {
    count: 0
  },
  info: {
    name: '前端九部',
    description: '我們都是前端愛好者绽昼!'
  }
}

他們各自的 reducer

/*counterReducer, 一個子reducer*/
/*注意:counterReducer 接收的 state 是 state.counter*/
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1
      }
    default:
      return state;
  }
}
/*InfoReducer,一個子reducer*/
/*注意:countReducer 接收的 state 是 state.info*/
function InfoReducer(state, action) {
  switch (action.type) {
    case 'SET_NAME':
      return {
        ...state,
        name: action.name
      }
    case 'SET_DESCRIPTION':
      return {
        ...state,
        description: action.description
      }
    default:
      return state;
  }
}

那我們用 combineReducers 函數(shù)來把多個 reducer 函數(shù)合并成一個 reducer 函數(shù)须蜗。大概這樣用

const reducer = combineReducers({
    counter: counterReducer,
    info: InfoReducer
});

我們嘗試實現(xiàn)下 combineReducers 函數(shù)

function combineReducers(reducers) {

  /* reducerKeys = ['counter', 'info']*/
  const reducerKeys = Object.keys(reducers)

  /*返回合并后的新的reducer函數(shù)*/
  return function combination(state = {}, action) {
    /*生成的新的state*/
    const nextState = {}

    /*遍歷執(zhí)行所有的reducers硅确,整合成為一個新的state*/
    for (let i = 0; i < reducerKeys.length; i++) {
      const key = reducerKeys[i]
      const reducer = reducers[key]
      /*之前的 key 的 state*/
      const previousStateForKey = state[key]
      /*執(zhí)行 分 reducer,獲得新的state*/
      const nextStateForKey = reducer(previousStateForKey, action)

      nextState[key] = nextStateForKey
    }
    return nextState;
  }
}

我們來嘗試下 combineReducers 的威力吧

const reducer = combineReducers({
  counter: counterReducer,
  info: InfoReducer
});

let initState = {
  counter: {
    count: 0
  },
  info: {
    name: '前端九部',
    description: '我們都是前端愛好者明肮!'
  }
}

let store = createStore(reducer, initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(state.counter.count, state.info.name, state.info.description);
});
/*自增*/
store.dispatch({
  type: 'INCREMENT'
});

/*修改 name*/
store.dispatch({
  type: 'SET_NAME',
  name: '前端九部2號'
});

本小節(jié)完整源碼見 demo-3

state 的拆分和合并

上一小節(jié)菱农,我們把 reducer 按組件維度拆分了,通過 combineReducers 合并了起來柿估。但是還有個問題循未, state 我們還是寫在一起的,這樣會造成 state 樹很龐大秫舌,不直觀的妖,很難維護。我們需要拆分足陨,一個 state嫂粟,一個 reducer 寫一塊。

這一小節(jié)比較簡單墨缘,我就不賣關(guān)子了星虹,用法大概是這樣(注意注釋)

/* counter 自己的 state 和 reducer 寫在一起*/
let initState = {
  count: 0
}
function counterReducer(state, action) {
  /*注意:如果 state 沒有初始值,那就給他初始值D魉稀宽涌!*/  
  if (!state) {
      state = initState;
  }
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1
      }
    default:    
      return state;
  }
}

我們修改下 createStore 函數(shù),增加一行 dispatch({ type: Symbol() })

const createStore = function (reducer, initState) {
  let state = initState;
  let listeners = [];

  function subscribe(listener) {
    listeners.push(listener);
  }

  function dispatch(action) {
    state = reducer(state, action);
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  function getState() {
    return state;
  }
  /* 注意5濉;ぬ恰!只修改了這里嚼松,用一個不匹配任何計劃的 type嫡良,來獲取初始值 */
  dispatch({ type: Symbol() })

  return {
    subscribe,
    dispatch,
    getState
  }
}

我們思考下這行可以帶來什么效果?

  1. createStore 的時候献酗,用一個不匹配任何 type 的 action寝受,來觸發(fā) state = reducer(state, action)
  2. 因為 action.type 不匹配,每個子 reducer 都會進到 default 項罕偎,返回自己初始化的 state很澄,這樣就獲得了初始化的 state 樹了。

你可以試試

/*這里沒有傳 initState 哦 */
const store = createStore(reducer);
/*這里看看初始化的 state 是什么*/
console.dir(store.getState());

本小節(jié)完整源碼見 demo-4

到這里為止,我們已經(jīng)實現(xiàn)了一個七七八八的 redux 啦甩苛!

中間件 middleware

中間件 middleware 是 redux 中最難理解的地方蹂楣。但是我挑戰(zhàn)一下用最通俗的語言來講明白它。如果你看完這一小節(jié)讯蒲,還沒明白中間件是什么痊土,不知道如何寫一個中間件,那就是我的鍋了墨林!

中間件是對 dispatch 的擴展赁酝,或者說重寫,增強 dispatch 的功能旭等!

記錄日志

我現(xiàn)在有一個需求酌呆,在每次修改 state 的時候,記錄下來 修改前的 state 搔耕,為什么修改了隙袁,以及修改后的 state。我們可以通過重寫 store.dispatch 來實現(xiàn)弃榨,直接看代碼

const store = createStore(reducer);
const next = store.dispatch;

/*重寫了store.dispatch*/
store.dispatch = (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

我們來使用下

store.dispatch({
  type: 'INCREMENT'
});

日志輸出為

this state { counter: { count: 0 } }
action { type: 'INCREMENT' }
1
next state { counter: { count: 1 } }

現(xiàn)在我們已經(jīng)實現(xiàn)了一個完美的記錄 state 修改日志的功能菩收!

記錄異常

我又有一個需求,需要記錄每次數(shù)據(jù)出錯的原因惭墓,我們擴展下 dispatch

const store = createStore(reducer);
const next = store.dispatch;

store.dispatch = (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('錯誤報告: ', err)
  }
}

這樣每次 dispatch 出異常的時候坛梁,我們都會記錄下來。

多中間件的合作

我現(xiàn)在既需要記錄日志腊凶,又需要記錄異常划咐,怎么辦?當然很簡單了钧萍,兩個函數(shù)合起來唄褐缠!

store.dispatch = (action) => {
  try {
    console.log('this state', store.getState());
    console.log('action', action);
    next(action);
    console.log('next state', store.getState());
  } catch (err) {
    console.error('錯誤報告: ', err)
  }
}

如果又來一個需求怎么辦?接著改 dispatch 函數(shù)风瘦?那再來10個需求呢队魏?到時候 dispatch 函數(shù)肯定龐大混亂到無法維護了!這個方式不可取呀万搔!

我們需要考慮如何實現(xiàn)擴展性很強的多中間件合作模式胡桨。

  1. 我們把 loggerMiddleware 提取出來

    const store = createStore(reducer);
    const next = store.dispatch;
    
    const loggerMiddleware = (action) => {
      console.log('this state', store.getState());
      console.log('action', action);
      next(action);
      console.log('next state', store.getState());
    }
    
    store.dispatch = (action) => {
      try {
        loggerMiddleware(action);
      } catch (err) {
        console.error('錯誤報告: ', err)
      }
    }
    
  2. 我們把 exceptionMiddleware 提取出來

    const exceptionMiddleware = (action) => {
      try {
        /*next(action)*/
        loggerMiddleware(action);
      } catch (err) {
        console.error('錯誤報告: ', err)
      } 
    }
    store.dispatch = exceptionMiddleware;
    
  3. 現(xiàn)在的代碼有一個很嚴重的問題,就是 exceptionMiddleware 里面寫死了 loggerMiddleware瞬雹,我們需要讓 next(action)變成動態(tài)的昧谊,隨便哪個中間件都可以

    const exceptionMiddleware = (next) => (action) => {
      try {
        /*loggerMiddleware(action);*/
        next(action);
      } catch (err) {
        console.error('錯誤報告: ', err)
      } 
    }
    /*loggerMiddleware 變成參數(shù)傳進去*/
    store.dispatch = exceptionMiddleware(loggerMiddleware);
    
  4. 同樣的道理,loggerMiddleware 里面的 next 現(xiàn)在恒等于 store.dispatch酗捌,導(dǎo)致 loggerMiddleware 里面無法擴展別的中間件了呢诬!我們也把 next 寫成動態(tài)的

    const loggerMiddleware = (next) => (action) => {
      console.log('this state', store.getState());
      console.log('action', action);
      next(action);
      console.log('next state', store.getState());
    }
    

到這里為止涌哲,我們已經(jīng)探索出了一個擴展性很高的中間件合作模式!

const store = createStore(reducer);
const next = store.dispatch;

const loggerMiddleware = (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

const exceptionMiddleware = (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('錯誤報告: ', err)
  }
}

store.dispatch = exceptionMiddleware(loggerMiddleware(next));

這時候我們開開心心的新建了一個 loggerMiddleware.js尚镰,一個exceptionMiddleware.js文件阀圾,想把兩個中間件獨立到單獨的文件中去。會碰到什么問題嗎狗唉?

loggerMiddleware 中包含了外部變量 store初烘,導(dǎo)致我們無法把中間件獨立出去。那我們把 store 也作為一個參數(shù)傳進去好了~

const store = createStore(reducer);
const next  = store.dispatch;

const loggerMiddleware = (store) => (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

const exceptionMiddleware = (store) => (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('錯誤報告: ', err)
  }
}

const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(logger(next));

到這里為止敞曹,我們真正的實現(xiàn)了兩個可以獨立的中間件啦账月!

現(xiàn)在我有一個需求综膀,在打印日志之前輸出當前的時間戳澳迫。用中間件來實現(xiàn)!

const timeMiddleware = (store) => (next) => (action) => {
  console.log('time', new Date().getTime());
  next(action);
}
...
const time = timeMiddleware(store);
store.dispatch = exception(time(logger(next)));

本小節(jié)完整源碼見 demo-6

中間件使用方式優(yōu)化

上一節(jié)我們已經(jīng)完全實現(xiàn)了正確的中間件剧劝!但是中間件的使用方式不是很友好

import loggerMiddleware from './middlewares/loggerMiddleware';
import exceptionMiddleware from './middlewares/exceptionMiddleware';
import timeMiddleware from './middlewares/timeMiddleware';

...

const store = createStore(reducer);
const next = store.dispatch;

const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
const time = timeMiddleware(store);
store.dispatch = exception(time(logger(next)));

其實我們只需要知道三個中間件橄登,剩下的細節(jié)都可以封裝起來!我們通過擴展 createStore 來實現(xiàn)讥此!

先來看看期望的用法

/*接收舊的 createStore拢锹,返回新的 createStore*/
const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore);

/*返回了一個 dispatch 被重寫過的 store*/
const store = newCreateStore(reducer);

實現(xiàn) applyMiddleware

const applyMiddleware = function (...middlewares) {
  /*返回一個重寫createStore的方法*/
  return function rewriteCreateStoreFunc(oldCreateStore) {
     /*返回重寫后新的 createStore*/
    return function newCreateStore(reducer, initState) {
      /*1\. 生成store*/
      const store = oldCreateStore(reducer, initState);
      /*給每個 middleware 傳下store,相當于 const logger = loggerMiddleware(store);*/
      /* const chain = [exception, time, logger]*/
      const chain = middlewares.map(middleware => middleware(store));
      let dispatch = store.dispatch;
      /* 實現(xiàn) exception(time((logger(dispatch))))*/
      chain.reverse().map(middleware => {
        dispatch = middleware(dispatch);
      });

      /*2\. 重寫 dispatch*/
      store.dispatch = dispatch;
      return store;
    }
  }
}

讓用戶體驗美好

現(xiàn)在還有個小問題萄喳,我們有兩種 createStore 了

/*沒有中間件的 createStore*/
import { createStore } from './redux';
const store = createStore(reducer, initState);

/*有中間件的 createStore*/
const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);
const newCreateStore = rewriteCreateStoreFunc(createStore);
const store = newCreateStore(reducer, initState);

為了讓用戶用起來統(tǒng)一一些卒稳,我們可以很簡單的使他們的使用方式一致,我們修改下 createStore 方法

const createStore = (reducer, initState, rewriteCreateStoreFunc) => {
    /*如果有 rewriteCreateStoreFunc他巨,那就采用新的 createStore */
    if(rewriteCreateStoreFunc){
       const newCreateStore =  rewriteCreateStoreFunc(createStore);
       return newCreateStore(reducer, initState);
    }
    /*否則按照正常的流程走*/
    ...
}

最終的用法

const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);

const store = createStore(reducer, initState, rewriteCreateStoreFunc);

本小節(jié)完整源碼見 demo-7

完整的 redux

退訂

不能退訂的訂閱都是耍流浪充坑!我們修改下 store.subscribe 方法,增加退訂功能

  function subscribe(listener) {
    listeners.push(listener);
    return function unsubscribe() {
      const index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

使用

const unsubscribe = store.subscribe(() => {
  let state = store.getState();
  console.log(state.counter.count);
});
/*退訂*/
unsubscribe();

中間件拿到的store

現(xiàn)在的中間件拿到了完整的 store染突,他甚至可以修改我們的 subscribe 方法捻爷,按照最小開放策略,我們只用把 getState 給中間件就可以了份企!因為我們只允許你用 getState 方法也榄!

修改下 applyMiddleware 中給中間件傳的 store

/*const chain = middlewares.map(middleware => middleware(store));*/
const simpleStore = { getState: store.getState };
const chain = middlewares.map(middleware => middleware(simpleStore));

compose

我們的 applyMiddleware 中,把 [A, B, C] 轉(zhuǎn)換成 A(B(C(next)))司志,是這樣實現(xiàn)的

const chain = [A, B, C];
let dispatch = store.dispatch;
chain.reverse().map(middleware => {
   dispatch = middleware(dispatch);
});

redux 提供了一個 compose 方式甜紫,可以幫我們做這個事情

const chain = [A, B, C];
dispatch = compose(...chain)(store.dispatch)

看下他是如何實現(xiàn)的

export default function compose(...funcs) {
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

當然 compose 函數(shù)對于新人來說可能比較難理解,你只需要他是做什么的就行啦骂远!

省略initState

有時候我們創(chuàng)建 store 的時候不傳 initState囚霸,我們怎么用?

const store = createStore(reducer, {}, rewriteCreateStoreFunc);

redux 允許我們這樣寫

const store = createStore(reducer, rewriteCreateStoreFunc);

我們僅需要改下 createStore 函數(shù)吧史,如果第二個參數(shù)是一個object邮辽,我們認為他是 initState唠雕,如果是 function,我們就認為他是 rewriteCreateStoreFunc吨述。

function craeteStore(reducer, initState, rewriteCreateStoreFunc){
    if (typeof initState === 'function'){
    rewriteCreateStoreFunc = initState;
    initState = undefined;
  }
  ...
}

2 行代碼的 replaceReducer

reducer 拆分后岩睁,和組件是一一對應(yīng)的。我們就希望在做按需加載的時候揣云,reducer也可以跟著組件在必要的時候再加載捕儒,然后用新的 reducer 替換老的 reducer。

const createStore = function (reducer, initState) {
  ...
  function replaceReducer(nextReducer) {
    reducer = nextReducer
    /*刷新一遍 state 的值邓夕,新來的 reducer 把自己的默認狀態(tài)放到 state 樹上去*/
    dispatch({ type: Symbol() })
  }
  ...
  return {
    ...
    replaceReducer
  }
}

我們來嘗試使用下

const reducer = combineReducers({
  counter: counterReducer
});
const store = createStore(reducer);

/*生成新的reducer*/
const nextReducer = combineReducers({
  counter: counterReducer,
  info: infoReducer
});
/*replaceReducer*/
store.replaceReducer(nextReducer);

replaceReducer 示例源碼見 demo-5

bindActionCreators

bindActionCreators 我們很少很少用到刘莹,一般只有在 react-redux 的 connect 實現(xiàn)中用到。

他是做什么的焚刚?他通過閉包点弯,把 dispatch 和 actionCreator 隱藏起來,讓其他地方感知不到 redux 的存在矿咕。

我們通過普通的方式來 隱藏 dispatch 和 actionCreator 試試抢肛,注意最后兩行代碼

const reducer = combineReducers({
  counter: counterReducer,
  info: infoReducer
});
const store = createStore(reducer);

/*返回 action 的函數(shù)就叫 actionCreator*/
function increment() {
  return {
    type: 'INCREMENT'
  }
}

function setName(name) {
  return {
    type: 'SET_NAME',
    name: name
  }
}

const actions = {
  increment: function () {
    return store.dispatch(increment.apply(this, arguments))
  },
  setName: function () {
    return store.dispatch(setName.apply(this, arguments))
  }
}
/*注意:我們可以把 actions 傳到任何地方去*/
/*其他地方在實現(xiàn)自增的時候,根本不知道 dispatch碳柱,actionCreator等細節(jié)*/
actions.increment(); /*自增*/
actions.setName('九部威武'); /*修改 info.name*/

我眼睛一看捡絮,這個 actions 生成的時候,好多公共代碼莲镣,提取一下

const actions = bindActionCreators({ increment, setName }, store.dispatch);

來看一下 bindActionCreators 的源碼福稳,超級簡單(就是生成了剛才的 actions)

/*核心的代碼在這里,通過閉包隱藏了 actionCreator 和 dispatch*/
function bindActionCreator(actionCreator, dispatch) {
  return function () {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

/* actionCreators 必須是 function 或者 object */
export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error()
  }

  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

bindActionCreators 示例源碼見 demo-8

大功告成

完整的示例源碼見 demo-9瑞侮,你可以和 redux 源碼做一下對比的圆,你會發(fā)現(xiàn),我們已經(jīng)實現(xiàn)了 redux 所有的功能了区岗。

當然略板,為了保證代碼的理解性,我們少了一些參數(shù)驗證慈缔。比如 createStore(reducer)的參數(shù) reducer 必須是 function 等等叮称。

最佳實踐

純函數(shù)

什么是純函數(shù)?

純函數(shù)是這樣一種函數(shù)藐鹤,即相同的輸入瓤檐,永遠會得到相同的輸出,而且沒有任何可觀察的副作用娱节。

通俗來講挠蛉,就兩個要素

  1. 相同的輸入,一定會得到相同的輸出
  2. 不會有 “觸發(fā)事件”肄满,更改輸入?yún)?shù)谴古,依賴外部參數(shù)质涛,打印 log 等等副作用
/*不是純函數(shù),因為同樣的輸入掰担,輸出結(jié)果不一致*/
function a( count ){
   return count + Math.random();
}

/*不是純函數(shù)汇陆,因為外部的 arr 被修改了*/
function b( arr ){
    return arr.push(1);
}
let arr = [1, 2, 3];
b(arr);
console.log(arr); //[1, 2, 3, 1]

/*不是純函數(shù),以為依賴了外部的 x*/
let x = 1;
function c( count ){
    return count + x;
}

我們的 reducer 計劃函數(shù)带饱,就必須是一個純函數(shù)毡代!

只要傳入?yún)?shù)相同,返回計算得到的下一個 state 就一定相同勺疼。沒有特殊情況教寂、沒有副作用,沒有 API 請求执庐、沒有變量修改酪耕,單純執(zhí)行計算。

總結(jié)

到了最后耕肩,我想把 redux 中關(guān)鍵的名詞列出來因妇,你每個都知道是干啥的嗎问潭?

  • createStore
    創(chuàng)建 store 對象猿诸,包含 getState, dispatch, subscribe, replaceReducer

  • reducer
    reducer 是一個計劃函數(shù),接收舊的 state 和 action狡忙,生成新的 state

  • action
    action 是一個對象梳虽,必須包含 type 字段

  • dispatch
    dispatch( action ) 觸發(fā) action,生成新的 state

  • subscribe
    實現(xiàn)訂閱功能灾茁,每次觸發(fā) dispatch 的時候窜觉,會執(zhí)行訂閱函數(shù)

  • combineReducers
    多 reducer 合并成一個 reducer

  • replaceReducer
    替換 reducer 函數(shù)

  • middleware
    擴展 dispatch 函數(shù)!

你再看 redux 流程圖北专,是不是大徹大悟了禀挫?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拓颓,隨后出現(xiàn)的幾起案子语婴,更是在濱河造成了極大的恐慌,老刑警劉巖驶睦,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砰左,死亡現(xiàn)場離奇詭異,居然都是意外死亡场航,警方通過查閱死者的電腦和手機缠导,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來溉痢,“玉大人僻造,你說我怎么就攤上這事憋他。” “怎么了髓削?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵举瑰,是天一觀的道長。 經(jīng)常有香客問我蔬螟,道長此迅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任旧巾,我火速辦了婚禮耸序,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鲁猩。我一直安慰自己坎怪,他們只是感情好,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布廓握。 她就那樣靜靜地躺著搅窿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪隙券。 梳的紋絲不亂的頭發(fā)上男应,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機與錄音娱仔,去河邊找鬼沐飘。 笑死,一個胖子當著我的面吹牛牲迫,可吹牛的內(nèi)容都是我干的耐朴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼盹憎,長吁一口氣:“原來是場噩夢啊……” “哼筛峭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起陪每,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤影晓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后奶稠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俯艰,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年锌订,在試婚紗的時候發(fā)現(xiàn)自己被綠了竹握。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡辆飘,死狀恐怖啦辐,靈堂內(nèi)的尸體忽然破棺而出谓传,到底是詐尸還是另有隱情,我是刑警寧澤芹关,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布续挟,位于F島的核電站,受9級特大地震影響侥衬,放射性物質(zhì)發(fā)生泄漏诗祸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一轴总、第九天 我趴在偏房一處隱蔽的房頂上張望直颅。 院中可真熱鬧,春花似錦怀樟、人聲如沸功偿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽械荷。三九已至,卻和暖如春虑灰,著一層夾襖步出監(jiān)牢的瞬間吨瞎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工瘩缆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留关拒,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓庸娱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谐算。 傳聞我的和親對象是個殘疾皇子熟尉,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容