banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

Redux學習之路(一)Redux三原則、createStore原理及實現

開新坑…… 記錄一下學習使用 Redux 的歷程,主要來自Redux 中文官網Redux 入門系列視頻及其裡面的教程裡的筆記和轉錄

包括了 Redux 三原則、Reducer、getState、dispatch、subscribe 原理及實現

首先要明確一點,雖然 Redux 是一個很不錯的管理狀態工具,但還是要考慮下它是否適合你的場景。

不要僅僅因為有人說過應該使用 Redux 而使用,而是應該花一些時間來了解使用它的潛在好處和取捨

這篇文章是下面這個教學視頻裡的 1-8 期視頻,闡述了 Redux 三原則和 Redux 中的 Reducer、(getState、dispatch、subscribe)以及 createStore 的原理及實現,並且實現了一個簡易的計數器。看完基本上對 Redux 就有了一個大致的了解

為什麼學捏,主要是因為最近看的項目或多或少都有 Redux 的使用,不學壓根看不懂

Redux 入門 —— 系列視頻
Redux 的創建者 Dan Abramov 在 30 個短片(2-5 分鐘)中展示了各種概念。鏈接的 Github 倉庫包含視頻的筆記和轉錄。
Redux 入門系列視頻
筆記和轉錄

簡介#

什麼是 Redux

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

可以讓你開發出行為穩定可預測的應用,運行於不同的環境(客戶端、伺服器、原生應用),並且易於測試。不僅於此,它還提供超爽的開發體驗,比如有一個時間旅行調試器可以編輯後實時預覽。

Redux 除了和 React 一起用外,還支持其它界面庫。它體小精悍(只有 2kB,包括依賴),卻有很強大的插件擴展生態。

什麼需要使用 Redux#

首先要明確一點,雖然 Redux 是一個很不錯的管理狀態工具,但還是要考慮下它是否適合你的場景。不要僅僅因為有人說過應該使用 Redux 而使用 - 應該花一些時間來了解使用它的潛在好處和取捨

當在自己的項目中遇到如下問題時,建議開始使用 Redux:

  • 你有很多數據隨時間而變化
  • 你希望狀態有一個唯一確定的來源(single source of truth)
  • 你發現將所有狀態放在頂層組件中管理已不可維護

Redux 三原則#

原則 1:單一不可變狀態樹#

Redux: The Single Immutable State Tree from @dan_abramov on @eggheadio

單一不可變狀態樹(The Single Immutable State Tree),注意幾個關鍵詞:單一不可變狀態樹

Redux 的第一個原則就是:應用程序的整個狀態將由一個 JavaScript 對象來表示,無論這個應用是簡單的小應用還是擁有大量 UI 和狀態變化的複雜應用程序。而這個 JavaScript 對象就稱之為狀態樹。

這是一個 Todo 應用程序中可能具備的狀態:

"current_state:"
[object Object] {
  todos: [[object Object] {
    completed: true,
    id: 0,
    text: "hey",
  }, [object Object] {
    completed: false,
    id: 1,
    text: "ho",
  }],
  visibilityFilter: "SHOW_ACTIVE"
}

原則 2:狀態樹只讀#

Redux: Describing State Changes with Actions from @dan_abramov on @eggheadio

Redux 的第二個原則是狀態樹是只讀的。我們是沒有辦法直接修改或寫入它的,只能通過 **“發起 Action”** 這個行為來對其進行修改。

Action 是描述更改的一個普通 JS 對象,它是對該數據所做的更改的最小表示形式,它的結構完全取決於我們自己,唯一的要求是它必須有一個綁定的屬性 type,(通常使用字符串,因為它是可序列化的)。

dispatch an action#

例如:Todo 應用程序中,顯示 Todo 的組件並不知道如何將項目添加到列表中,他們知道的只是他們需要分派(dispatch)一個 action,它具有一個類型 type:"add todo",以及一個 todo 的文本和一個序號。

如果切換一個任務,同樣,組件不知道它是怎麼發生的。他們所需要做的就是分派一個具有 type 的 action,切換 todo,並傳入想要切換到的 todo 的 ID。

如上可以看出,狀態是只讀的,並且只能通過 dispatch 操作進行修改。

原則 3:要描述狀態變化,必須編寫一個純函數(Reducer)#

Redux: Pure and Impure Functions | egghead.io

Redux: The Reducer Function | egghead.io

而 Redux 的第三個原則就是:要描述狀態變化,必須編寫一個純函數,該純函數採用應用的先前狀態(previous state)發起的 action(the action being dispatched),然後返回應用的下一個狀態(next state )。而這個純函數稱為Reducer

理解純函數與非純函數#

首先我們要理解什麼是純函數 / 非純函數,因為 Redux 有時候需要我們編寫純函數。

在之前的博客裡也提到過:【第二屆青訓營 - 寒假前端場】- 「跟著月影學 JavaScript」筆記,這裡再重溫一遍。

純函數是指一個函數的返回結果只依賴於它的參數,並且在執行過程中沒有任何副作用,這也意味著,它不會對外界產生任何影響。

可以非常確定的說,使用相同的參數集調用純函數,將得到相同的返回值。他們是可預測的。

此外,純函數不會修改傳遞給它們的值。例如,接受數組的 squareAll 函數不會覆蓋此數組內的項。相反,它通過使用項目映射返回一個新的數組。

純函數示例:

function square(x) {
    return x*x;
}
function squareAll(items) {
    return items.map(square);	// 注意,這裡是生成了一個新的數組而非直接return items
}

非純函數示例:

function square(x) {
  updateXInDatabase(x);	// 對數據庫中的x也產生了影響
  return x * x;
}
function squareAll(items) {
  for (let i = 0; i < items.length; i++) {
    items[i] = square(items[i]);	// 並且直接修改了items...
  }
}

之前在學習 React 的時候就了解過了,不可變這個概念,對於很多場景下都有幫助,如 React 官方文檔井字棋實現中的 時間旅行 功能,倘若每一步都在原來的對象上進行變換,撤銷等工作就會變得非常複雜。

Reducer#

React 開創了這樣一種觀點:當 UI 層被描述為應用程序狀態的純函數時,它是最可預測的。

Redux 用另一個想法補充了這種方法:應用中的狀態突變必須由純函數描述,該函數採用上個狀態和正在調度的操作,並返回應用程序的下一個狀態。

即使在大型應用程序中,仍然只需要一個函數來計算應用程序的新狀態。它根據整個應用程序的先前狀態和正在調度的操作來執行此操作。

但是,這個操作不一定很慢。如果狀態的某些部分沒有更改,則其引用可以保持原樣。這就是使 Redux 快速的原因。

下面是一個完整示例

初始狀態#

初始 state 中,todos 為空,過濾器為顯示全部

請添加圖片描述

添加 Todo#

變化如圖: 一開始的 state 中,todos 沒有內容,過濾器為顯示全部。發起 action 之後的 state 中 todos 多了個 todo,過濾視圖未變化

請添加圖片描述

完成 Todo#

點擊一個 todo 將其置為完成,可以看到發起這個 action 的時候,todos 的文本沒有變化,狀態 complete 被置為完成了……
請添加圖片描述

更改過濾視圖#

再添加一個 todo 後點擊過濾器 Active,觀察前後 state,可以發現,只是 visibilityFilter 狀態由 "SHOW_ALL" 改變為 "SHOW_ACTIVE" 了,todos 的內容還是沒有變化的(abcd 並沒有被刪掉)

請添加圖片描述

編寫一個帶有測試的計數器 Reducer#

Redux: Writing a Counter Reducer with Tests | egghead.io

我們要編寫的第一個函數是針對計數器示例的減速器。我們還將使用 expect 來進行斷言。

eggheadio-projects/getting-started-with-redux: null - Plunker (plnkr.co)

const counter = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

expect(
  counter(0, { type: 'INCREMENT' })
).toEqual(1);

expect(
  counter(1, { type: 'INCREMENT' })
).toEqual(2);

expect(
  counter(2, { type: 'DECREMENT' })
).toEqual(1);

expect(
  counter(1, { type: 'DECREMENT' })
).toEqual(0);

expect(
  counter(1, { type: 'SOMETHING_ELSE' }) 
).toEqual(1);

expect(
  counter(undefined, {})
).toEqual(0);

如上,counter 這個 Reducer 設置了兩個可識別的 type(INCREMENT、DECREMENT),分別表示計數 + 1,-1。在寫入 Reducer 時,如果傳入的 state 是未定義的,則需要返回一個表示初始狀態的對象(initstate)。在這個計數器的例子中,我們返回 0,因為我們的計數從 0 開始。如果傳入的 action 不是 Reducer 所能識別的(SOMETHING_ELSE),我們只返回當前state

存儲方法:getState ()、dispatch () 和 subscribe ()#

Redux: Store Methods: getState(), dispatch(), and subscribe() | egghead.io

本節使用了 Redux 中內置的函數。我們使用 ES6 析構語法引入了createStore .

const counter = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

const { createStore } = Redux; // Redux CDN import syntax
// import { createStore } from 'redux' // npm module syntax

const store = createStore(counter);

createStore 創建一個 Redux store 來以存放應用中所有的 state。應用中應有且僅有一個 store

參數

  1. reducer (Function): 接收兩個參數,分別是當前的 state 樹和要處理的 action返回新的 state 樹
  2. [preloadedState] (any): 初始時的 state。 在同構應用中,你可以決定是否把服務端傳來的 state 水合(hydrate)後傳給它,或者從之前保存的用戶會話中恢復一個傳給它。如果你使用 combineReducers 創建 reducer,它必須是一個普通對象,與傳入的 keys 保持同樣的結構。否則,你可以自由傳入任何 reducer 可理解的內容。
  3. enhancer (Function): Store enhancer,可選。可以用第三方第能力如中間價、時間旅行、持久化來增強 store。是一個組合 store creator 的高階函數,返回一個新的強化過的 store creator。Redux 中唯一內置的 store enhander 是 applyMiddleware()

返回值

(Store): 保存了應用所有 state 的對象。改變 state 的唯一方法是 dispatch action。你也可以 subscribe 監聽 state 的變化,然後更新 UI。

createStore所創建的store有 3 個重要方法

getState () 獲取狀態#

getState() 檢索 Redux 存儲的當前狀態。返回應用當前的 state 樹。它與 store 的最後一個 reducer 返回值相同。

dispatch () 調度,發起 action#

dispatch() 是最常用的。分發 action。這是觸發 state 變化的唯一途徑

它將使用當前 getState()的結果和傳入的 action同步方式調用 store 的 reduce 函數。它的返回值會被作為下個 state。從現在開始,這就成為了 getState() 的返回值,同時變化監聽器 (change listener) 會被觸發

subscribe () 註冊#

添加一個變化監聽器每當 dispatch action 的時候就會執行,state 樹中的一部分可能已經變化。可以在回調函數裡調用 getState() 來拿到當前 state。

你可以在變化監聽器裡面進行 dispatch(),但你需要注意下面的事項:

  1. 監聽器調用 dispatch() 僅僅應當發生在響應用戶的 actions 或者特殊的條件限制下(比如: 在 store 有一個特殊的字段時 dispatch action)。雖然沒有任何條件去調用 dispatch() 在技術上是可行的,但是隨著每次 dispatch() 改變 store 可能會導致陷入無窮的循環。
  2. 訂閱器(subscriptions) 在每次 dispatch() 調用之前都會保存一份快照。當你在正在調用監聽器(listener)時訂閱 (subscribe) 或者去掉訂閱(unsubscribe),對當前的 dispatch() 不會有任何影響。但是對於下一次的 dispatch(),無論嵌套與否,都会使用訂閱列表裡最近的一次快照。
  3. 訂閱器不應該注意到所有 state 的變化,在訂閱器被調用之前,往往由於嵌套的 dispatch() 導致 state 發生多次的改變。保證所有的監聽器都註冊在 dispatch() 啟動之前,這樣,在調用監聽器的時候就會傳入監聽器所存在時間裡最新的一次 state。

這是一個底層 API。大多數情況下,你不會直接使用它,會使用一些 React(或其它庫)的綁定。如果你想讓回調函數執行的時候使用當前的 state,你可以 寫一個定制的 observeStore 工具Store 也是一個 Observable, 所以你可以使用 RxJS 的這樣的庫來 subscribe 訂閱更新。

如果需要解绑這個變化監聽器,執行 subscribe 返回的函數即可。

// ... `counter` reducer as above ...

const { createStore } = Redux;
const store = createStore(counter);

store.subscribe(() => {
  document.body.innerText = store.getState();
});

document.addEventListener('click', () => {
    store.dispatch({ type : 'INCREMENT' })
});

按照以上方法的話,初始狀態不會被更新,因為渲染發生在 subscribe 的回調之後,而經過補救過後:

const render = () => {
  document.body.innerText = store.getState();
};

store.subscribe(render);
render(); // 先調用一次來渲染初始狀態0, 之後的渲染則在每次dispatch之後

document.addEventListener('click', () => {
    store.dispatch({ type : 'INCREMENT' })
});

上面的官網摘下來的文檔看似很難理解,看看接下來的簡易實現就能理解了。

實現一個簡易版的 createStore~!#

Redux: Implementing Store from Scratch | egghead.io

在前面的學習中,我們研究了如何使用createStore(),但是為了更好理解它,讓我們從頭開始編寫它!

首先復盤前面的案例,我們知道

  • createStore函數接收一個reducer函數,這個 reducer 函數返回當前 state,會被內部的dispatch函數所調用

  • createStore函數創建出來的 store 需要擁有兩個變量:

    • state 當前狀態,它是一個 JavaScript 對象,
    • listeners 監聽器數組,它是一個函數數組對象
  • createStore函數創建出來的 store 需要擁有這三個方法:getStatedispatchsubscribe

    • getState 方法返回當前 state
    • dispatch 函數是更改內部 state 的唯一方法,它傳入一個 action,通過將內部的當前 state 和 action 傳入reducer函數(createStore 的入參)來計算新的 state。更新後,我們通知每個變化監聽器(通過調用它們) dispatch
    • subscribe 傳入一個 listener 函數作為參數,將其放入內部的 listener 數組,為了取消訂閱事件監聽器,subscribe需要返回一個函數, 調用這個返回的函數就可以取消監聽,這個函數內部通過filter()將 listeners 數組賦值為一個新的監聽器數組(去除了與當前監聽相同引用後返回的新監聽器數組)。subscribe
    • 在返回store時,我們需要要填充初始狀態。我們要分派一個假的 action 來讓 reducer 返回初始值。
const createStore = (reducer) => {	// 返回 store,可以調用getState、dispatch、subscribe
	let state;	
    let listeners = [];
    const getState = () => state;   // 外部可以通過調用getState獲取當前state

    const dispatch = (action) => {
        state = reducer(state, action); // 因為reducer是純函數,所以每次返回的state不會修改原先的state~
        listeners.forEach(listener => listener());     // 在dispatch事件中,調用reducer成功後調用監聽器們~
    }

    const subscribe = (listener) => {   // 添加監聽!
        listeners.push(listener);
        return () => {      // 為了能夠取消監聽器,返回一個函數
            listeners = listeners.filter(l => l !== listener);
        }
    }

    dispatch({});   // 為了讓state有一個初始狀態!

    return { getState, dispatch, subscribe };
};

改進計數器#

Redux: Implementing Store from Scratch | egghead.io

改進後示例:Counter (cos)

在上文我們實現了一個計數器的 Reducer,在這裡我們改進一下,用 React 實際的將其渲染出來

編寫 Counter 組件#

編寫一個 Counter 組件,該組件是一個 "哑" 組件(dump component)。它不包含任何業務邏輯

哑組件僅指定如何將當前狀態呈現到輸出中,以及如何將通過 prop 傳遞的回調函數綁定到事件處理程序。

// Counter 組件
const Counter = ({
  value,
  onIncrement,
  onDecrement,
  onElse
}) => (
  <div>
    <h1>{value}</h1>
    <button onClick={onIncrement}>+</button>
    <button onClick={onDecrement}>-</button>
    <button onClick={onElse}>else</button>
  </div>
);
// 該組件的渲染函數
const render = () => {
  console.log("render!");
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={() => store.dispatch({
        type:'INCREMENT'
      })}
      onDecrement={() => store.dispatch({
        type:'DECREMENT'
      })}
      onElse={() => store.dispatch({
        type:'else'
      })}
      />, document.getElementById('root')
  );
}

當該哑組件呈現時,我們指定其值應該從 Redux 存儲的當前狀態中獲取值。當用戶按下一個按鈕時,相應的 action 將被 dispatch 到 Redux 的 store。

調用 createStore、添加監聽#

調用 createStore 創建一個 store, 調用 store.subscribe 添加一個 render 監聽器

const counter = (state = 0, action) => {
  console.log("now state:", state);
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
} 
// store create with counter reducer
const { createStore } = Redux;
const store = createStore(counter);
store.subscribe(render);  // 添加監聽,每次狀態更新都會重新渲染
render();

reducer 指定如何根據當前 state 和傳入的 action 計算下一個 state。 最後,我們訂閱 store(傳入 render 函數),這樣 render 函數在每次調用 dispatch 改變 state 時運行。

ps: 原文這裡是 "Finally, we subscribe to the Redux store so our render() function runs any time the state changes so our Counter gets the current state." 說是每當 state 變化時觸發 render,根據上文 createStore 的實現來說感覺不準確,state 就算值沒變也會觸發,每次 dispatch 即發送 action 時都會 render,實際試了試也是這樣的(也許可以理解為,Redux 要求每次 dispatch 都有改變狀態的意圖,後面視頻要是有提到的話我再回來勘誤)

Redux: React Counter example |egghead.io

查看過程#

通過下面的例子了解一下這個過程:

運行試試:Counter (cos)

初始值#

一開始什麼都不做,可以看到 createStore 的時候就進行了一次 dispatch,通過 reducer(即 counter 函數)將 state 置為初始值 0 後進行了一次渲染。(注意)

請添加圖片描述

增加計數#

點一下 +,發現又調用了一次 render

請添加圖片描述

else#

點了好幾次 else 之後,發現每次都會重新渲染,但是 state 的值看起來似乎沒變,而且組件表面上看也沒有變化
請添加圖片描述

總結#

首先要明確一點,雖然 Redux 是一個很不錯的管理狀態工具,但還是要考慮下它是否適合你的場景。

  • 不要僅僅因為有人說過應該使用 Redux 而使用,而是應該花一些時間來了解使用它的潛在好處和取捨

看完這 1-8 期視頻,基本上了解了 Redux 什麼時候用比較好與它的缺點,了解了 Redux 三原則和 Redux 中的 Reducer、(getState、dispatch、subscribe)以及 createStore 的原理及實現,並且實現了一個極其簡易的計數器(順帶知道了哑組件是啥)。

Redux 三原則:

  • 應用程序的整個狀態將由一個 JavaScript 對象來表示,而這個 JavaScript 對象就稱之為狀態樹
  • 狀態樹是只讀的。沒辦法直接修改或寫入,只能通過 **“發起 Action”** 這個行為來間接對其進行修改。
  • 描述狀態變化必須編寫一個純函數,採用應用的先前狀態 state發起的 action,然後返回應用的下一個 state(next state )。而這個純函數稱為Reducer

createStore函數接收一個reducer函數,這個Reducer函數返回當前 state,會被內部的dispatch函數所調用

  • createStore函數創建出來的 store 需要擁有兩個變量:

    • state 當前狀態,它是一個 JavaScript 對象,
    • listeners 監聽器數組,它是一個函數數組對象
  • createStore函數創建出來的 store 還需要擁有這三個方法:getStatedispatchsubscribe

    • getState 方法返回當前 state
    • dispatch 函數傳入一個 action,通過將內部的當前 state 和 action 傳入reducer函數(createStore 的入參)來計算新的 state。更新後,我們通知每個變化監聽器(通過調用它們) dispatch
    • subscribe 傳入一個 listener 函數作為參數,將其放入內部的 listener 數組,為了取消訂閱事件監聽器,subscribe需要返回一個函數, 調用這個返回的函數就可以取消監聽,這個函數內部通過filter()將 listeners 數組賦值為一個新的監聽器數組(去除了與當前監聽相同引用後返回的新監聽器數組)。subscribe
    • 在返回store時,我們需要要填充初始狀態。我們要分派一個假的 action 來讓 reducer 返回初始值。
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。