從 Redux 設計理念到源碼分析

2021-02-20 React
前言

Redux 也是我列在 THE LAST TIME 系列中的一篇,由於現在正在著手探究關於我目前正在開發的業務中狀態管理的方案。所以,這裡打算先從 Redux 中學習學習,從他的狀態中取取經。畢竟,成功總是需要站在巨人的肩膀上不是。

話說回來,都 2020 年了還在寫 Redux 的文章,真的是有些過時了。不過呢,當時 Redux 孵化過程中一定也是回頭看了 Flux、CQRS、ES 等。

本篇先從 Redux 的設計理念到部分源碼分析。下一篇我們在注重說下 Redux的 Middleware工作機制。至於手寫,推薦磚家大佬的:完全理解 redux(從零實現一個 redux)

Redux

Redux 並不是什麼特別 Giao 的技術,但是其理念真的提的特別好。

說透了,它就是一個提供了 setter、getter 的大閉包,。外加一個 pubSub。。。另外的什麼 reducer、middleware 還是 action什麼的,都是基於他的規則和解決用戶使用痛點而來的,僅此而已。下面我們一點點說。。。

設計思想

在 jQuery 時代的時候,我們是「面向過程開發」,隨著 react 的普及,我們提出了狀態驅動 UI 的開發模式。我們認為:「Web 應用就是狀態與 UI 一一對應的關係」

但是隨著我們的 web 應用日趨的複雜化,一個應用所對應的背後的 state 也變的越來越難以管理。

「Redux 就是我們 Web 應用的一個狀態管理方案」

一一對應

如上圖所示,store 就是 Redux 提供的一個狀態容器。裡面存儲著 View 層所需要的所有的狀態(state)。每一個 UI 都對應著背後的一個狀態。Redux 也同樣規定。一個 state 就對應一個 View。只要 state 相同,View 就相同。(其實就是 state 驅動 UI)。

為什麼要使用 Redux

如上所說,我們現在是狀態驅動 UI,那麼為什麼需要 Redux 來管理狀態呢?react 本身就是 state drive view 不是。

原因還是由於現在的前端的地位已經愈發的不一樣啦,前端的複雜性也是越來越高。通常一個前端應用都存在大量複雜、無規律的交互。還伴隨著各種異步操作。

任何一個操作都可能會改變 state,那麼就會導致我們應用的 state 越來越亂,且被動原因愈發的模糊。我們很容易就對這些狀態何時發生、為什麼發生、怎麼發生而失去控制。

如上,如果我們的頁面足夠複雜,那麼view 背後state 的變化就可能呈現出這個樣子。不同的 component 之間存在著父子、兄弟、子父、甚至跨層級之間的通信。

而我們理想中的狀態管理應該是這個樣子的:

單純的從架構層面而言,UI 與狀態完全分離,並且單向的數據流確保了狀態可控。

而 Redux 就是做這個的!

下面簡單介紹下 Redux 中的幾個概念。其實初學者往往就是對其概念而困惑。

store❝

保存數據的地方,你可以把它看成一個容器,整個應用只能有一個Store。

❞State❝

某一個時刻,存儲著的應用狀態值

❞Action❝

View 發出的一種讓 state 發生變化的通知

❞Action Creator❝

可以理解為 Action 的工廠函數

❞dispatch❝

View 發出 Action 的媒介。也是唯一途徑

❞reducer❝

根據當前接收到的Action 和 State,整合出來一個全新的 State。注意是需要是純函數

❞三大原則

Redux 的使用,基於以下三個原則

單一數據源

單一數據源這或許是與 Flux 最大的不同了。在 Redux 中,整個應用的 state 都被存儲到一個object 中。當然,這也是唯一存儲應用狀態的地方。我們可以理解為就是一個 Object tree。不同的枝幹對應不同的 Component。但是歸根結底只有一個根。

也是受益於單一的 state tree。以前難以實現的「撤銷/重做」甚至回放。都變得輕鬆了很多。

State 只讀

唯一改變 state 的方法就是 dispatch 一個 action。action 就是一個令牌而已。normal Object。

任何 state 的變更,都可以理解為非 View 層引起的(網絡請求、用戶點擊等)。View 層只是發出了某一種意圖。而如何去滿足,完全取決於 Redux 本身,也就是 reducer。

store.dispatch({
type:'FETCH_START',
params:{
itemId:233333
}
})

使用純函數來修改

所謂純函數,就是你得純,別變來變去了。書面詞彙這裡就不做過多解釋了。而這裡我們說的純函數來修改,其實就是我們上面說的 reducer。

Reducer 就是純函數,它接受當前的 state 和 action。然後返回一個新的 state。所以這裡,state 不會更新,只會替換。

之所以要純函數,就是結果可預測性。只要傳入的 state 和 action 一直,那麼就可以理解為返回的新 state 也總是一樣的。

總結

Redux 的東西遠不止上面說的那麼些。其實還有比如 middleware、actionCreator 等等等。其實都是使用過程中的衍生品而已。我們主要是理解其思想。然後再去源碼中學習如何使用。

源碼分析❝

Redux 源碼本身非常簡單,限於篇幅,我們下一篇再去介紹compose、combineReducers、applyMiddleware

目錄結構

Redux 源碼本身就是很簡單,代碼量也不大。學習它,也主要是為了學習他的編程思想和設計範式。

當然,我們也可以從 Redux 的代碼裡,看看大佬是如何使用 ts 的。所以源碼分析裡面,我們還會去花費不少精力看下 Redux 的類型說明。所以我們從 type 開始看

src/types

看類型聲明也是為了學習Redux 的 ts 類型聲明寫法。所以相似聲明的寫法形式我們就不重複介紹了。

actions.ts

類型聲明也沒有太多的需要去說的邏輯,所以我就寫注釋上吧

// Action的接口定義。type 欄位明確聲明
export interface Action<T = any> {
  type: T
}
export interface AnyAction extends Action {
  // 在 Action 的這個接口上額外擴展的另外一些任意欄位(我們一般寫的都是 AnyAction 類型,用一個「基類」去約束必須帶有 type 欄位)
  [extraProps: string]: any
}
export interface ActionCreator<A> {
  // 函數接口,泛型約束函數的返回都是 A
  (...args: any[]): A
}
export interface ActionCreatorsMapObject<A = any> {
  // 對象,對象值為 ActionCreator
  [key: string]: ActionCreator<A>
}

reducers.ts
// 定義的一個函數,接受 S 和繼承 Action 默認為 AnyAction 的 A,返回 S
export type Reducer<S = any, A extends Action = AnyAction> = (
  state: S | undefined,
  action: A
) => S

// 可以理解為 S 的 key 作為ReducersMapObject的 key,然後 value 是  Reducer的函數。in 我們可以理解為遍歷
export type ReducersMapObject<S = any, A extends Action = Action> = {
  [K in keyof S]: Reducer<S[K], A>
}

上面兩個聲明比較簡單直接。下面兩個稍微麻煩一些

export type StateFromReducersMapObject<M> = M extends ReducersMapObject<
  any,
  any
>
  ? { [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never }
  : never
  
export type ReducerFromReducersMapObject<M> = M extends {
  [P in keyof M]: infer R
}
  ? R extends Reducer<any, any>
    ? R
    : never
  : never

上面兩個聲明,咱們來解釋其中第一個吧(稍微麻煩些)。

StateFromReducersMapObject 添加另一個泛型M約束M 如果繼承 ReducersMapObject<any,any>則走{ [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never }的邏輯{ [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never } 很明顯,這就是一個對象,key 來自 M 對象裡面,也就是ReducersMapObject裡面傳入的S。key 對應的 value 就是需要判斷 M[P]是否繼承自 Reducer。否則也啥也不是infer 關鍵字和 extends 一直配合使用。這裡就是指返回 Reducer 的這個 State 「的類型」其他

types 目錄裡面其他的比如 store、middleware都是如上的這種聲明方式,就不再贅述了,感興趣的可以翻閱翻閱。然後取其精華的應用到自己的 ts 項目裡面

src/createStore.ts不要疑惑上面函數重載的寫法~

可以看到,整個createStore.ts 就是一個createStore 函數。

createStore

三個參數:

reducer:就是 reducer,根據 action 和 currentState 計算 newState 的純 FunctionpreloadedState:initial Stateenhancer:增強store的功能,讓它擁有第三方的功能

createStore 裡面就是一些「閉包函數的功能整合」

INIT
// A extends Action
dispatch({ type: ActionTypes.INIT } as A)

這個方法是Redux保留用的,用來初始化State,其實就是dispatch 走到我們默認的 switch case default 的分支裡面獲取到默認的 State。

return
const store = ({
    dispatch: dispatch as Dispatch<A>,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  } as unknown) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext

ts 的類型轉換語法就不說了,返回的對象裡面包含dispatch、subscribe、getState、replaceReducer、[$$observable].

這裡我們簡單介紹下前三個方法的實現。

getState
  function getState(): S {
    if (isDispatching) {
      throw new Error(
        `我 reducer 正在執行,newState 正在產出呢!現在不行`
      )
    }

    return currentState as S
  }

方法很簡單,就是 return currentState

subscribe

subscribe的作用就是添加監聽函數listener,讓其在每次dispatch action的時候調用。

返回一個移除這個監聽的函數。

使用如下:

const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

unsubscribe();

function subscribe(listener: () => void) {
    // 如果 listenter 不是一個 function,我就報錯(其實 ts 靜態檢查能檢查出來的,但!那是編譯時,這是運行時)
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }
    // 同 getState 一個樣紙
    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/store#subscribelistener for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    // 直接將監聽的函數放進nextListeners裡
    nextListeners.push(listener)

    return function unsubscribe() {// 也是利用閉包,查看是否以訂閱,然後移除訂閱
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
          'See https://`Redux`.js.org/api/store#subscribelistener for more details.'
        )
      }

      isSubscribed = false//修改這個訂閱狀態

      ensureCanMutateNextListeners()
      //找到位置,移除監聽
      const index = nextListeners.indexOf(listener) 
      nextListeners.splice(index, 1)
      currentListeners = null
    }
  }

「一句話解釋就是在 listeners 數據裡面添加一個函數」

再來說說這裡面的ensureCanMutateNextListeners,很多 Redux 源碼都麼有怎麼提及這個方法的作用。也是讓我有點困惑。

這個方法的實現非常簡單。就是判斷當前的監聽數組裡面是否和下一個數組相等。如果是!則 copy 一份。

  let currentListeners: (() => void)[] | null = []
  let nextListeners = currentListeners
  
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

那麼為什麼呢?這裡留個彩蛋。等看完 dispatch 再來看這個疑惑。

dispatch
  function dispatch(action: A) {
  // action必須是個普通對象
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
        'Use custom middleware for async actions.'
      )
    }
  // 必須包含 type 欄位
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
        'Have you misspelled a constant?'
      )
    }
  // 同上
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      // 設置正在 dispatch 的 tag 為 true(解釋了那些判斷都是從哪裡來的了)
      isDispatching = true
      // 通過傳入的 reducer 來去的新的 state
      //  let currentReducer = reducer
      currentState = currentReducer(currentState, action)
    } finally {
    // 修改狀態
      isDispatching = false
    }
    
    // 將 nextListener 賦值給 currentListeners、listeners (注意回顧 ensureCanMutateNextListeners )
    const listeners = (currentListeners = nextListeners)
    // 挨個觸發監聽
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

方法很簡單,都寫在注釋裡了。這裡我們再回過頭來看ensureCanMutateNextListeners的意義

ensureCanMutateNextListeners
  let currentListeners: (() => void)[] | null = []
  let nextListeners = currentListeners
  
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  function subscribe(listener: () => void) {
    // ...
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null
    }
  }
  
  function dispatch(action: A) {
    // ... 
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    // ...
    return action
  }

從上,代碼看起來貌似只要一個數組來存儲listener 就可以了。但是事實是,我們恰恰就是我們的 listener 是可以被 unSubscribe 的。而且 slice 會改變原數組大小。

所以這裡增加了一個 listener 的副本,是為了避免在遍歷listeners的過程中由於subscribe或者unsubscribe對listeners進行的修改而引起的某個listener被漏掉了。

最後

限於篇幅,就暫時寫到這吧~

其實後面打算重點介紹的 Middleware,只是中間件的一種更規範,甚至我們可以理解為,它並不屬於 Redux 的。因為到這裡,你已經完全可以自己寫一份狀態管理方案了。

而 combineReducers也是我認為是費巧妙的設計。所以這些篇幅,就放到下一篇吧~

參考連結

相關焦點

  • 學習 redux 源碼整體架構,深入理解 redux 及其中間件原理
    這是學習源碼整體架構系列第八篇。整體架構這詞語好像有點大,姑且就算是源碼整體結構吧,主要就是學習是代碼整體結構,不深究其他不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。要是有人說到怎麼讀源碼,正在讀文章的你能推薦我的源碼系列文章,那真是太好了。
  • 【THE LAST TIME】從 Redux 源碼中學習它的範式
    而學習 Redux,也並非它的源碼有多麼複雜,而是他狀態管理的思想,著實值得我們學習。講真,標題真的是不好取,因為本文是我寫的 redux 的下一篇。兩篇湊到一起,才是完整的 Redux。上篇:從 Redux 設計理念到源碼分析本文續上篇,接著看 combineReducers、applyMiddleware和 compose 的設計與源碼實現至於手寫,其實也是非常簡單,說白了,「去掉源碼中嚴謹的校驗
  • redux原理解析,看這篇就夠了
    在實際開發中,常搭配React + React-redux使用。這代表了目前前端開發的一個基本理念,數據和視圖的分離。redux應運而生,當然還有其他的一些狀態管理庫,如Flux、Elm等,當然,我們這裡只對redux進行解析。
  • 你還在redux中寫重複囉嗦的樣板代碼嗎?rc-redux-model來了
    再次明確rc-redux-model 出發點在於解決繁瑣重複的工作,store 文件分散,state 類型和賦值錯誤的問題,為此,對於跟我一樣的用戶,提供了一個寫狀態管理較為[舒服]的書寫方式,大部分情況下兼容原先項目~為了解決[store 文件分散],參考借鑑了 dva 寫狀態管理的方式,一個 model 中寫所有的
  • Redux入坑進階之源碼解析
    即: 傳入的Object參數中,對象的key與value所代表的reducer function同名 各個reducer function的名稱和需要傳入該reducer的state參數同名 源碼標註解讀(省略部分): export default function combineReducers(reducers) {
  • 完全理解 redux(從零實現一個 redux)
    不管你同不同意,我都要換,因為新名字比較厲害(其實因為 redux 是這麼叫的)!本小節完整源碼見 demo-2多文件協作reducer 的拆分和合併這一小節我們來處理下 reducer 的問題。啥問題?我們知道 reducer 是一個計劃函數,接收老的 state,按計劃返回新的 state。
  • 從零開始手寫 redux
    這樣 getState 首次調用時,可以獲取到狀態的默認值。前面我們一步一步推演了 redux 的核心代碼,現在我們來回顧一下 redux 的設計思想:Redux 設計思想Redux 將整個應用狀態(state)存儲到一個地方(通常我們稱其為 store)當我們需要修改狀態時,必須派發(dispatch)一個 action( action 是一個帶有 type 欄位的對象)專門的狀態處理函數
  • 探尋 Redux useSelector 更新機制
    一個似乎無法實現的 hook 的內部工作原理深入研究當一個 react context 更新的時候,所有使用到該 context 的組件也會更新。即使我的組件只用到了不常更新的那部分數據,組件還是會在另一部分數據更新的時候進行重新 render。react-redux 的useSelector hook 就沒有這樣的問題——組件只會在其所選擇到的(selected)數據更新的情況下才會重新 render,即使在 store 中的其他數據被更新的情況下也是這樣。那麼這是什麼原理呢?
  • Redux數據狀態管理
    Redux基礎簡單幾行代碼,詮釋了Redux的設計理念// 定義計算規則,即reducerfunction counter(state = 0, action) { switch(action.type) { case "INCREMENT": return state + 1;
  • 從零開始手寫redux,8000字長文讓你徹底搞懂
    前面我們一步一步推演了 redux 的核心代碼,現在我們來回顧一下 redux 的設計思想:Redux 設計思想Redux 將整個應用狀態(state)存儲到一個地方(通常我們稱其為 store)當我們需要修改狀態時,必須派發(dispatch)一個 action( action 是一個帶有 type 欄位的對象)專門的狀態處理函數
  • 【重學React】動手實現一個react-redux
    react-redux 是什麼react-redux 是 redux 官方 React 綁定庫。它幫助我們連接UI層和數據層。本文目的不是介紹 react-redux 的使用,而是要動手實現一個簡易的 react-redux,希望能夠對你有所幫助。
  • Redux異步方案選型
    由於Redux的理念非常精簡,沒有追求大而全,這份架構上的優雅卻在某種程度上傷害了使用體驗:不能開箱即用,甚至是異步這種最常見的場景也要藉助社區方案。 如果你已經挑花了眼,或者正在挑但不知道是否適合,或者已經挑了但不知道會不會有坑,這篇文章應該適合你。
  • Vuex、Flux、Redux、Redux-saga、Dva、MobX
    比如 redux-thunk 或 redux-promise 。redux-thunk 和 redux-promise 剛好就是代表這兩個面。redux-thunk 和 redux-promise 的具體使用就不介紹了,這裡只聊一下大概的思路。大部分簡單的異步業務場景,redux-thunk 或者 redux-promise 都可以滿足了。
  • 從<琅琊榜>學 Redux
    import {combineReducers} from 'redux'import {ADD_TODO} from './actions'import { createStore } from 'redux'import rootReducer from '.
  • React-redux數據傳遞是如何實現的?
    主要內容React數據傳遞reduxReact-redux其他學習目標第一節 react數據傳遞react 中組件之間數據傳遞1. 父傳子2. 子傳父(狀態提升)3.兄弟之間傳遞需要把數據上傳到共有的父級身上,然後再通過父級向下傳,傳到指定的子級上本節作業兄弟元素之間數據傳遞兩個具有共同祖先級的元素之間數據傳遞第二節 redux1. 介紹:Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理。
  • React-Redux 使用 Hooks
    快速搭建 redux 環境    create-react-app redux-app    yarn  add react-redux reduxTips: 確保 react-redux 版本 在 v7.1.0 以上
  • 3-6-react-redux
    我們先來觀察一下我們需要用props傳入的3個參數,在之前我們單獨使用react的時候,我們是通過react自身的state來控制狀態數據的,而現在有的redux,我們可以發現,其實我們需要的value就是redux目前返回的state,另外兩個事件函數increment和decrement其實也就對應著同種類型action的執行。也就是說,目前的這3個參數,我們都可以直接從redux當中獲取到。
  • React + Redux + React-router 全家桶
    StoreStore是由redux的createStore函數生成。所有的狀態,保存在對象Store裡面,整個應用只有唯一一個Store。import thunk from 'redux-thunk'const store = createStore(reducer, applyMiddleware(thunk))react-redux
  • React Native 從入門到源碼分析-了解RN百態
    本文投稿人bestswifter:博客:https://bestswifter.com/react-native/本文所講知識點React Native問世解決的痛點React Native運行原理React Native源碼分析React Native優缺點分析
  • Redux 入門教程(三):React-Redux 的用法
    import { connect } from 'react-redux'const VisibleTodoList = connect()(TodoList);上面代碼中,TodoList是 UI 組件,VisibleTodoList就是由 React-Redux 通過connect方法自動生成的容器組件。