redux 源碼分析

redux 源碼分析

原文鏈接

簡介

Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理。

三大原則

  1. 單一數據源,創建一個 Redux store 來以存放應用中所有的 state。應用中應有且僅有一個 store。
  2. State 是隻讀的,唯一改變 state 的方法就是觸發 action,action.type 作爲邏輯說明。
  3. 使用純函數來執行修改,reducers 來描述 action 如何改變 state tree,reducers 應該是一個純函數。

對比 mobx

  • 第一條,一般 redux 以 reducer 爲拆分維度,然後 combineReducers 合成一個總 reducer 來創建 store,然後傳給 React context,全局訪問。
    而 mobx 一般對於一個頁面或頁面級別的組件創建 store,然後在一個總文件中導入,作爲一個大對象導出給 context。
  • 第二條,redux 把 reducer 生產的 state 閉包在內部,無法直接訪問,只能 dispatch action 驅動 reducer 函數來改變 state,邏輯集中在 reducer 內,可預測維護。
    而 mobx @observable state 可以隨意改變,甚至邏輯分散在視圖 view 層,例如 onClick={() => (state.status = 1)}。雖然官方建議把邏輯封裝
    在 action 中,例如 @action updata() {},但不是一種強約束。
  • 第三條,這裏是唯一一個 redux 難以強約束用戶的地方,例如 reducer 中 state.xx = 1; return state,應該是 return { ...state, xx: 1}
    而鑑於 mobx 的機制, mobx 更別說了。

所以常說,mobx 常用中小型應用,redux 更適合大型應用。

概覽

./src
├── applyMiddleware.ts
├── bindActionCreators.ts
├── combineReducers.ts
├── compose.ts
├── createStore.ts
└── index.ts
  • index.ts,核心的 5 個文件名對應 redux 暴露的關鍵的 5 個函數。
export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
}
  • createStore,主函數用來創建 redux 實例,利用閉包隱藏數據,暴露接口操作內部數據。使用訂閱發佈模式,當數據改變時發佈通知。
  • combineReducers,組合多個 reducer 函數,reducer 就是一個創建、修改重新創建數據的工廠。數據可以交給多個 reducer 小車間創建。
  • bindActionCreators,當需要隱藏 dispatch 或 store 時用到,往往是傳給下一個組件時,讓組件無感知 redux 的存在。
  • compose,工具函數
  • applyMiddleware,利用 compose 來實現,store.dispatch 傳給最後一箇中間件函數,前一箇中間件函數能調用後一箇中間件函數。

使用

例子都是摘錄來的,不想看的可以跳過。

  • createStore
import { createStore } from 'redux';

function reducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text]);
    default:
      return state
  }
}
// createStore 至少接收一個參數、最多三個。第二個是初始化 state 對象,必須是一個純對象
let store = createStore(reducer, ['Use Redux']);

store.dispatch({
  type: 'ADD_TODO',
  text: 'Read the docs',
});

console.log(store.getState());
// [ 'Use Redux', 'Read the docs' ]
  • combineReducers
// reducers/todos.js
export default function todos(state = [], action) {
  switch (action.type) {
  case 'ADD_TODO':
    return state.concat([action.text]);
  default:
    return state
  }
}
// reducers/counter.js
export default function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  default:
    return state
  }
}
// reducers/index.js
import { combineReducers } from 'redux';
import todos from './todos';
import counter from './counter';

// 這裏 combineReducers 會根據傳入的 reducer 名稱設置爲 state 的屬性名
// 可以設置別名 combineReducers({ cnt: counter }); 最終 state = { cnt: 0 }
export default combineReducers({
  todos,
  counter
})
// App.js
import { createStore } from 'redux';
import reducer from './reducers/index';

let store = createStore(reducer);
console.log(store.getState());
// {
//   counter: 0,
//   todos: []
// }

store.dispatch({
  type: 'ADD_TODO',
  text: 'Use Redux'
});
console.log(store.getState());
// {
//   counter: 0,
//   todos: [ 'Use Redux' ]
// }
  • bindActionCreators
// actionCreators.js
// action 函數
export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  };
}

export function removeTodo(id) {
  return {
    type: 'REMOVE_TODO',
    id
  };
}
// App.js
const bindAction = bindActionCreators({ addTodo, removeTodo }, dispatch);
<Child {...bindAction} />

// Child.js
const {addTodo, removeTodo} = props.bindAction;
addTodo('text'); 
// 相當於 dispatch( addTodo('text') ); 子組件不知道 redux 的存在,卻驅動了數據的更新。
  • applyMiddleware,當有多個參數的時,底層用 compose 來組合。
// dispatch 只接收一個純對象 
function actionCreator( name ) { return { type: 'ActionName', name } }
store.dispatch(
  actionCreator( '張三' )
);

// 但要根據用戶id 異步獲取姓名怎麼辦?

// 使用 redux-thunk 支持 dispatch 接收一個函數,
// 把 store 上 dispatch, getState 的控制權傳給該函數
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import * as reducers from './reducers';

const reducer = combineReducers(reducers);
// applyMiddleware 爲 createStore 注入了 middleware:
const store = createStore(reducer, applyMiddleware(thunk));

function asyncAction(id) {
  return (dispatch, getState) => ajax(id).then(name => dispatch( actionCreator(name) ));
}

// thunk 打破了 dispatch 只能接收純對象的約定
store.dispatch(
  asyncAction(123)
);

深入 redux

createStore

  • createStore 概覽,典型的發佈訂閱模式
function createStore(reducer, preloadedState, enhancer) {
  // preloadedState、enhancer 不能同爲函數,enhancer 爲函數時,第四個參數不能爲函數
  if (typeof preloadedState === 'function' && typeof enhancer === 'function' || typeof enhancer === 'function' && typeof arguments[3] === 'function') {
    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.');
  }
  // preloadedState、enhancer 同爲函數時,preloadedState 複製給 enhancer,preloadedState 置爲 undefined
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState;
    preloadedState = undefined;
  }
  // 增強函數 enhancer 不爲 undefined 時,執行增強函數,這個主要用來實現中間件的連接
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.');
    }
    // 增強函數
    return enhancer(createStore)(reducer, preloadedState);
  }
  // reducer 不是函數拋錯
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.');
  }

  let currentReducer = reducer; // 存儲當前 reducer,也用於 replaceReducer 替換
  let currentState = preloadedState; // 初始化 state
  let currentListeners = []; // 訂閱的回調數組
  let nextListeners = currentListeners; // 避免在發佈通知調用用戶回調函數時拋錯影響了數組
  let isDispatching = false; // 防止在 reducer 中執行,getState、dispatch、subscribe、unsubscribe
  
  // currentListeners 數組淺拷貝,防止 dispatch 階段執行用戶回調拋錯
  // 導致 dispatch 中斷了,卻影響了原來的數組結構 
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice();
    }
  }
  
  // 獲取 currentState
  function getState() {}
  
  // 訂閱消息,即 listener 入 currentListeners 隊列
  function subscribe(listener) {}
  
  // 根據 action.type 執行 reducer 函數,觸發 state 更新,讓後發佈通知即遍歷 nextListeners 執行
  function dispatch(action) {}
  
  // 動態更換 reducer,然後根據新的 reducer 初始化一次 state
  function replaceReducer(nextReducer) {}
  
  // 爲了支持未來 Observable 新特性,還在提案階段,不討論
  // function observable() {}
  
  // type 爲一個隨機字符串不會和 reducer 中的任何 type 相匹配
  // 如果 reducer 沒有默認匹配,則初始化的 state 爲 preloadedState
  dispatch({ type: ActionTypes.INIT });
  
  const store = {
    getState,
    subscribe,
    dispatch,
    replaceReducer,
  };
  return store;
}
  • 解釋下 isDispatching 的作用,例如你在 reducer 中嘗試以下操作,都會拋錯
function reducer(state = [], action) {
  // reducer 中執行 getState、dispatch、subscribe、unsubscribe
  action.store && action.store.getState();
  action.store && action.store.dispatch({
    type: 'hello',
    text: 'world',
  });
  action.store && action.store.subscribe(() => {});
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text]);
    default:
      return state
  }
}
  • getState
function getState() {
  if (isDispatching) {
    throw new Error('reducer 函數中不能執行 getState、dispatch、subscribe、unsubscribe');
  }

  return currentState; // 閉包隱藏內部 state,只能通過暴露 api 獲取
}
  • subscribe
function subscribe(listener) {
  if (typeof listener !== 'function') {
    throw new Error('Expected the listener to be a function.');
  }

  if (isDispatching) {
    throw new Error('reducer 函數中不能執行 getState、dispatch、subscribe、unsubscribe');
  }

  let isSubscribed = true; // 註冊憑證
  ensureCanMutateNextListeners(); // 確保原回調隊列在代碼拋錯時受到干擾
  
  nextListeners.push(listener); // 發佈訂閱,必不可少的入隊列操作
  
  // 返回取消訂閱函數
  return function unsubscribe() {
    if (!isSubscribed) { // 防止 unsubscribe 被多次執行
      return;
    }

    if (isDispatching) {
      throw new Error('reducer 函數中不能執行 getState、dispatch、subscribe、unsubscribe');      
    }

    isSubscribed = false; // 一旦執行 unsubscribe 就註銷掉註冊憑證
    ensureCanMutateNextListeners();
    // 找到隊列中的函數,剔除
    const index = nextListeners.indexOf(listener);
    nextListeners.splice(index, 1);
    currentListeners = null; // currentListeners 會在 dispatch 中恢復
  };
}
  • dispatch
function dispatch(action) {
  if (!isPlainObject(action)) { // 什麼是純對象?1. 字面量定義對象:const obj = {}; 2. const obj = new Object();
    throw new Error('action 必須是個純對象,異步操作需要自定義中間件');
  }

  if (typeof action.type === 'undefined') {
    throw new Error('必不可少的 type ');
  }

  if (isDispatching) {
    throw new Error('reducer 函數中不能執行 getState、dispatch、subscribe、unsubscribe');
  }

  try { // 防止用戶 reducer 拋錯
    isDispatching = true; // isDispatching 用來控制 reducer 執行階段
    currentState = currentReducer(currentState, action);
  } finally {
    isDispatching = false;
  }
  // 發佈通知
  const listeners = (currentListeners = nextListeners); // 在 unsubscribe 時 null,這裏被恢復
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i];
    listener();
  }

  return action;
}
  • replaceReducer
function replaceReducer(nextReducer) {
  if (typeof nextReducer !== 'function') {
    throw new Error('reducer 必須是個函數');
  }
  currentReducer = nextReducer; // 替換閉包中的 reducer

  // type: ActionTypes.REPLACE 是個獨一無二的隨機字符
  // 所有 reducer 中,最好有個默認 case
  dispatch({ type: ActionTypes.REPLACE });

  return store;
}

到此最重要的 createStore 函數就實現完畢。接下來分析兩個輔助函數 bindActionCreators,combineReducers,最後分析實現中間件功能的 compose、applyMiddleware。

bindActionCreators

function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') { // actionCreators 可以是單個函數
    return bindActionCreator(actionCreators, dispatch);
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error("bindActionCreators 得是個對象或函數");
  }

  let boundActionCreators = {};

  // 也可以是多個函數的導出,例如 export { actionCreators1, actionCreators2, ... }
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key];

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

  return boundActionCreators;
}

// 實際上就是形成閉包對外隱藏 actionCreator, dispatch。
// 返回的函數接受 actionCreator 的參數,然後 dispatch。
function bindActionCreator(actionCreator, dispatch) {
  return function (...args) {
    return dispatch(actionCreator.apply(this, args));
  };
}

combineReducers

function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers);
  const finalReducers = {};
  // reducers 是一個包含多個 reducer 函數的對象
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i];

    // 這裏會做很多判斷,爲了簡便刪了
    // 例如你不能在 reducer 函數裏使用 redux 內部自定義的 ActionTypes.INIT
    // reducer 函數默認都應該返回一個非 undefined 的值
    // reducer 函數用戶明確知道不需要一個默認值,也應該用 null 代替 undefined
    // 等等
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key];
    }
  }
  const finalReducerKeys = Object.keys(finalReducers);

  // 返回的是一個組合後的 reducer 函數
  return function combination(state = {}, action) {
    let hasChanged = false;
    const nextState = {};
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i];
      const reducer = finalReducers[key];
      const previousStateForKey = state[key];
      
      // 遍歷執行所有的 reducer
      const nextStateForKey = reducer(previousStateForKey, action);
      
      if (typeof nextStateForKey === 'undefined') {
        throw new Error('reducer 不應該給個 undefined');
      }
      nextState[key] = nextStateForKey;
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    // 如果其中有一個 reducer 返回的新的 state 有變更,則最終返回一個新的 state
    hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length;

    return hasChanged ? nextState : state
  }
}

compose

  • 輔助函數,效果:compose(fn1, fn2, fn3, …) fn1( fn2( fn3(…) ) ); 即 fn3 函數執行的返回值傳給 fn2,接着 fn2 的返回值傳給 fn1 執行。
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)));
}

applyMiddleware

function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, ...args) => {
    const store = createStore(reducer, ...args);
    let dispatch = () => {
      throw new Error('中間件生成函數不能執行 dispatch 操作');
    };

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    };

    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    dispatch = compose(...chain)(store.dispatch);

    return {
      ...store,
      dispatch
    };
  }
}

thunk 中間件

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
  • 我們來串聯下,中間件是如何進行的
  1. const store = createStore(reducer, preloadedState, enhancer = applyMiddleware(thunk));
  2. 當 enhancer 存在時,執行 createStore 其實是執行 enhancer(createStore)(reducer, preloadedState);
  3. 所以 applyMiddleware 執行返回的是一個接受 createStore 的函數,返回一個新的 createStore 函數,接收 reducer, preloadedState;
  4. 最終返回一個 store 對象,看看中間件是如何接手管轄權的。
    1. 中間件需要是一個接收 store 對象的函數,遍歷執行中間件一遍生成 chain 這樣每個中間件就能閉包住 store;
    2. 接着在 compose 中執行了一次,最後一箇中間件通過 next 參數接收了 store.dispatch,返回了一個接受 action 的函數賦值給 store;
    3. 此時只有一個 thunk 中間件,如果傳一個函數的話,控制權就給了用戶,通過參數可以拿到 dispatch, getState。如果 action 是對象,此時 next === 內部的 store.dispatch ,就像原來一樣。
    4. 如果有多箇中間件,那麼 applyMiddleware(a, b),a 中的 next 就是 b 中返回的 action => {} 函數,b 中的 next 就是 store.dispatch。這樣就串起來了。
  • 所以一般中間件的書寫的套路是 store => next => action => {};

總結

完。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章