Redux原始碼初探

Alan丶發表於2019-03-11

1、本文不涉及redux的使用方法,因此可能更適合使用過 redux 的同學閱讀 2、當前redux版本為4.0.1

Redux作為大型React應用狀態管理最常用的工具。雖然在平時的工作中很多次的用到了它,但是一直沒有對其原理進行研究。最近看了一下原始碼,下面是我自己的一些簡單認識,如有疑問歡迎交流指正。

1、createStore

結合使用場景我們首先來看一下createStore方法。

// 這是我們平常使用時建立store
const store = createStore(reducers, state, enhance);
複製程式碼

以下原始碼為去除異常校驗後的原始碼,


export default function createStore(reducer, preloadedState, enhancer) {

// 如果有傳入合法的enhance,則通過enhancer再呼叫一次createStore
 if (typeof enhancer !== 'undefined') {
   if (typeof enhancer !== 'function') {
     throw new Error('Expected the enhancer to be a function.')
   }
   return enhancer(createStore)(reducer, preloadedState) // 這裡涉及到中介軟體,後面介紹applyMiddleware時在具體介紹
 }

 let currentReducer = reducer     //把 reducer 賦值給 currentReducer
 let currentState = preloadedState   //把 preloadedState 賦值給 currentState
 let currentListeners = []     //初始化監聽函式列表
 let nextListeners = currentListeners   //監聽列表的一個引用
 let isDispatching = false  //是否正在dispatch

 function ensureCanMutateNextListeners() {}

 function getState() {}

 function subscribe(listener) {}
 
 function dispatch(action) {}

 function replaceReducer(nextReducer) {}
 
// 在 creatorStore 內部沒有看到此方法的呼叫,就不講了
 function observable() {}
 //初始化 store 裡的 state tree
 dispatch({ type: ActionTypes.INIT })

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

我們可以看到creatorStore方法除了返回我們常用的方法外,還做了一次初始化過程dispatch({ type: ActionTypes.INIT });那麼dispatch幹了什麼事情呢?

/**
  * dispath action。這是觸發 state 變化的惟一途徑。
  * @param {Object} 一個普通(plain)的物件,物件當中必須有 type 屬性
  * @returns {Object} 返回 dispatch 的 action
  */
 function dispatch(action) {
 // 判斷 dispahch 正在執行,Reducer在處理的時候又要執行 dispatch
   if (isDispatching) {
     throw new Error('Reducers may not dispatch actions.')
   }

   try {
     //標記 dispatch 正在執行
     isDispatching = true
     //執行當前 Reducer 函式返回新的 state
     currentState = currentReducer(currentState, action)
   } finally {
     isDispatching = false
   }
   const listeners = (currentListeners = nextListeners)
   //遍歷所有的監聽函式
   for (let i = 0; i < listeners.length; i++) {
     const listener = listeners[i]
     listener() // 執行每一個監聽函式
   }
   return action
 }
複製程式碼

這裡dispatch主要做了二件事情

  • 通過reducer更新state
  • 執行所有的監聽函式,通知狀態的變更

那麼reducer是怎麼改變state的呢?這就涉及到下面的combineReducers

2、combineReducers

回到上面建立store的引數reducers,

// 兩個reducer
const todos = (state = INIT.todos, action) => {
  // ....
};
const filterStatus = (state = INIT.filterStatus, action) => {
  // ...
};

const reducers = combineReducers({
  todos,
  filterStatus
});
// 這是我們平常使用時建立store
const store = createStore(reducers, state, enhance);
複製程式碼

下面我們來看combineReducers做了什麼

export default function combineReducers(reducers) {
   // 第一次篩選,引數reducers為Object
  // 篩選掉reducers中不是function的鍵值對
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)
  
  // 二次篩選,判斷reducer中傳入的值是否合法(!== undefined)
  // 獲取篩選完之後的所有key
  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  return function combination(state = {}, action) {
    let hasChanged = false
    const nextState = {}
    // 遍歷所有的key和reducer,分別將reducer對應的key所代表的state,代入到reducer中進行函式呼叫
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      // 這裡就是reducer function的名稱和要和state同名的原因,傳說中的黑魔法
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // 將reducer返回的值填入nextState
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 發生改變了返回新的nextState,否則返回原先的state
    return hasChanged ? nextState : state
  }
}
複製程式碼
  • 這裡 reducer(previousStateForKey, action)執行的就是我們上面定義的todosfilterStatus方法。通過這二個reducer改變state值。
  • 以前我一直很奇怪我們的reducer裡的state是怎麼做到取當前reducer對應的資料。看到const previousStateForKey = state[key]這裡我就明白了。
  • 這裡還有一個疑問點就是combineReducers的巢狀,最開始也我不明白,看了原始碼才知道combineReducers()=> combination(state = {}, action),這裡combineReducers返回的combination也是接受(state = {}, action)也就是一個reducer所以可以正常巢狀。

看到這初始化流程已經走完了。這個過程我們認識了dispatchcombineReducers;接下來我們來看一下我們自己要怎麼更新資料。

使用者更新資料時,是通過createStore後暴露出來的dispatch方法來觸發的。dispatch方法,是 store物件提供的更改 currentState 這個閉包變數的唯一建議途徑(注意這裡是唯一建議途徑,不是唯一途徑,因為通過getState獲取到的是state的引用,所以是可以直接修改的。但是這樣就不能更新檢視了)。 正常情況下我們只需要像下面這樣

//action creator
var addTodo = function(text){
    return {
        type: 'add_todo',
        text: text
    };
};
function TodoReducer(state = [], action){
    switch (action.type) {
        case 'add_todo':
            return state.concat(action.text);
        default:
            return state;
    }
};

// 通過 store.dispatch(action) 來達到修改 state 的目的
// 注意: 在redux裡,唯一能夠修改state的方法,就是通過 store.dispatch(action)
store.dispatch({type: 'add_todo', text: '讀書'});// 或者下面這樣
// store.dispatch(addTodo('讀書'));
複製程式碼

也就是說dispatch接受一個包含type的物件。框架為我們提供了一個建立Action的方法bindActionCreators

3、bindActionCreators

下面來看下原始碼

// 核心程式碼,並通過apply將this繫結起來
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

export default function bindActionCreators(actionCreators, dispatch) {
// 如果actionCreators是一個函式,則說明只有一個actionCreator,就直接呼叫bindActionCreator
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }
  // 遍歷物件,然後對每個遍歷項的 actionCreator 生成函式,將函式按照原來的 key 值放到一個物件中,最後返回這個物件
  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的作用就是使用dispatchaction creator包裹起來,這樣我們就可以直接呼叫他們了。這個在平常開發中不常用。

4、applyMiddleware

最後我們回頭來看一下之前調到的中介軟體,

import thunkMiddleware from 'redux-thunk';
// 兩個reducer
const todos = (state = INIT.todos, action) => {
  // ....
};
const filterStatus = (state = INIT.filterStatus, action) => {
  // ...
};

const reducers = combineReducers({
  todos,
  filterStatus
});
// 這是我們平常使用時建立store
const store = createStore(reducers, state, applyMiddleware(thunkMiddleware));
複製程式碼

為了下文好理解這個放一下redux-thunk的原始碼

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
  };
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
複製程式碼

可以看出thunk返回了一個接受({ dispatch, getState })為引數的函式 下面我們來看一下applyMiddleware原始碼分析

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    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.`
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 每個 middleware 都以 middlewareAPI 作為引數進行注入,返回一個新的鏈。此時的返回值相當於呼叫 thunkMiddleware 返回的函式: (next) => (action) => {} ,接收一個next作為其引數
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 並將鏈代入進 compose 組成一個函式的呼叫鏈
    // compose(...chain) 返回形如(...args) => f(g(h(...args))),f/g/h都是chain中的函式物件。
    // 在目前只有 thunkMiddleware 作為 middlewares 引數的情況下,將返回 (next) => (action) => {}
    // 之後以 store.dispatch 作為引數進行注入注意這裡這裡的store.dispatch是沒有被修改的dispatch他被傳給了next;
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

// 定義一個程式碼組合的方法
// 傳入一些function作為引數,返回其鏈式呼叫的形態。例如,
// compose(f, g, h) 最終返回 (...args) => f(g(h(...args)))
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  } else {
    const last = funcs[funcs.length - 1]
    const rest = funcs.slice(0, -1)
    return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
  }
}
複製程式碼

我第一眼看去一臉悶逼,咋這麼複雜。但是靜下心來看也就是一個三級柯里化的函式,我們從頭來分析一下這個過程

// createStore.js
if (typeof enhancer !== 'undefined') {
  if (typeof enhancer !== 'function') {
    throw new Error('Expected the enhancer to be a function.')
  }
  return enhancer(createStore)(reducer, preloadedState)
}
複製程式碼

也就是說,會變成這樣

applyMiddleware(thunkMiddleware)(createStore)(reducer, preloadedState)
複製程式碼
  • applyMiddleware(thunkMiddleware) applyMiddleware接收thunkMiddleware作為引數,返回形如(createStore) => (...args) => {}的函式。
  • applyMiddleware(thunkMiddleware)(createStore)createStore 作為引數,呼叫上一步返回的函式(...args) => {}
  • applyMiddleware(thunkMiddleware)(createStore)(reducer, preloadedState)(reducer, preloadedState)為引數進行呼叫。 在這個函式內部,thunkMiddleware被呼叫,其作用是監測typefunctionaction

因此,如果dispatch的action返回的是一個function,則證明是中介軟體,則將(dispatch, getState)作為引數代入其中,進行action 內部下一步的操作。否則的話,認為只是一個普通的action,將通過next(也就是dispatch)進一步分發


也就是說,applyMiddleware(thunkMiddleware)作為enhance,最終起了這樣的作用:

dispatch呼叫的action進行檢查,如果action在第一次呼叫之後返回的是function,則將(dispatch, getState)作為引數注入到action返回的方法中,否則就正常對action進行分發,這樣一來我們的中介軟體就完成嘍~

因此,當action內部需要獲取state,或者需要進行非同步操作,在操作完成之後進行事件呼叫分發的話,我們就可以讓action 返回一個以(dispatch, getState)為引數的function而不是通常的Object,enhance就會對其進行檢測以便正確的處理

到此redux原始碼的主要部分講完了,如有感興趣的同學可以去看一下我沒講到的一些東西轉送門redux;

相關文章