從 Redux 設計理念到源碼分析

前言

Redux 也是我列在 THE LAST TIME 系列中的一篇,因爲如今正在着手探究關於我目前正在開發的業務中狀態管理的方案。因此,這裏打算先從 Redux 中學習學習,從他的狀態中取取經。畢竟,成功老是須要站在巨人的肩膀上不是。前端

話說回來,都 2020 年了還在寫 Redux 的文章,真的是有些過期了。不過呢,當時 Redux 孵化過程當中必定也是回頭看了 FluxCQRSES 等。react

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

Redux

Redux 並非什麼特別 Giao 的技術,可是其理念真的提的特別好。github

說透了,它就是一個提供了 settergetter 的大閉包,。外加一個 pubSub。。。另外的什麼 reducermiddleware 仍是 action什麼的,都是基於他的規則和解決用戶使用痛點而來的,僅此而已。下面咱們一點點說。。。web

設計思想

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

可是隨着咱們的 web 應用日趨的複雜化,一個應用所對應的背後的 state 也變的愈來愈難以管理。redux

Redux 就是咱們 Web 應用的一個狀態管理方案api

一一對應
一一對應

如上圖所示,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 就是作這個的!

  • 每個 State 的變化可預測
  • 動做和狀態統一管理

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

store

保存數據的地方,你能夠把它當作一個容器,整個應用只能有一個Store

State

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

Action

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

Action Creator

能夠理解爲 Action 的工廠函數

dispatch

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

reducer

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

三大原則

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

單一數據源

單一數據源這或許是與 Flux 最大的不一樣了。在 Redux 中,整個應用的 state 都被存儲到一個object 中。固然,這也是惟一存儲應用狀態的地方。咱們能夠理解爲就是一個 Object tree。不一樣的枝幹對應不一樣的 Component。可是歸根結底只有一個根。

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

State 只讀

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

任何 state 的變動,均可以理解爲非 View 層引發的(網絡請求、用戶點擊等)。View 層只是發出了某一中意圖。而如何去知足,徹底取決於 Redux 自己,也就是 reducer。

store.dispatch({
  type:'FETCH_START',
  params:{
    itemId:233333
  }
})
複製代碼

使用純函數來修改

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

Reducer 就是純函數,它接受當前的 stateaction。而後返回一個新的 state。因此這裏,state 不會更新,只會替換。

之因此要純函數,就是結果可預測性。只要傳入的 stateaction 一直,那麼就能夠理解爲返回的新 state 也老是同樣的。

總結

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

源碼分析

Redux 源碼自己很是簡單,限於篇幅,咱們下一篇再去介紹composecombineReducersapplyMiddleware

目錄結構
目錄結構

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 }的邏輯
  • 不然就是 never。啥也不是
  • { [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never } 很明顯,這就是一個對象, key 來自 M 對象裏面,也就是 ReducersMapObject裏面傳入的 Skey 對應的 value 就是須要判斷 M[P]是否繼承自 Reducer。不然也啥也不是
  • infer 關鍵字和 extends 一直配合使用。這裏就是指返回 Reducer 的這個 State 的類型

其餘

types 目錄裏面其餘的好比 storemiddleware都是如上的這種聲明方式,就再也不贅述了,感興趣的能夠翻閱翻閱。而後取其精華的應用到本身的 ts 項目裏面

src/createStore.ts

不要疑惑上面函數重載的寫法~
不要疑惑上面函數重載的寫法~

能夠看到,整個createStore.ts 就是一個createStore 函數。

createStore

三個參數:

  • reducer:就是 reducer,根據 action 和 currentState 計算 newState 的純 Function
  • preloadedState:initial State
  • enhancer:加強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 的類型轉換語法就不說了,返回的對象裏面包含dispatchsubscribegetStatereplaceReducer[$$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或者unsubscribelisteners進行的修改而引發的某個listener被漏掉了。

最後

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

其實後面打算重點介紹的 Middleware,只是中間件的一種更規範,甚至咱們能夠理解爲,它並不屬於 Redux 的。由於到這裏,你已經徹底能夠本身寫一份狀態管理方案了。

combineReducers也是我認爲是費巧妙的設計。因此這些篇幅,就放到下一篇吧~

參考連接

學習交流

  • 關注公衆號【全棧前端精選】,每日獲取好文推薦
  • 添加微信號:is_Nealyang(備註來源) ,入羣交流
公衆號【全棧前端精選】 我的微信【is_Nealyang】
相關文章
相關標籤/搜索