React系列 --- 從零構建狀態管理及Redux源碼解析(七)

React系列

React系列 --- 簡單模擬語法(一)
React系列 --- Jsx, 合成事件與Refs(二)
React系列 --- virtualdom diff算法實現分析(三)
React系列 --- 從Mixin到HOC再到HOOKS(四)
React系列 --- createElement, ReactElement與Component部分源碼解析(五)
React系列 --- 從使用React瞭解Css的各類使用方案(六)
React系列 --- 從零構建狀態管理及Redux源碼解析(七)
React系列 --- 擴展狀態管理功能及Redux源碼解析(八)html

前言

雖然擺在React系列裏,可是我沒有把這當作是實現Redux的文章,而是分析狀態管理實現原理的科普文,因此我會從Redux的實現思想和部分源碼作參考,用最原始的Js實現一個基本庫,因此這裏不會出現任何框架庫.git

並且我默認你們都懂得基本概念,因此我不會特地展開過多篇幅在細節上,並且由於時間關係,我會將相關的類型判斷省略掉.github

文章的完整代碼能夠直接查看算法

Redux誕生的契機

隨着 JavaScript 單頁應用開發日趨複雜,JavaScript 須要管理比任什麼時候候都要多的 state (狀態)。 這些 state 可能包括服務器響應、緩存數據、本地生成還沒有持久化到服務器的數據,也包括 UI 狀態,如激活的路由,被選中的標籤,是否顯示加載動效或者分頁器等等。

管理不斷變化的 state 很是困難。若是一個 model 的變化會引發另外一個 model 變化,那麼當 view 變化時,就可能引發對應 model 以及另外一個 model 的變化,依次地,可能會引發另外一個 view 的變化。直至你搞不清楚到底發生了什麼。state 在何時,因爲什麼緣由,如何變化已然不受控制。 當系統變得錯綜複雜的時候,想重現問題或者添加新功能就會變得舉步維艱。redux

Redux將這些複雜度很大程度歸因於: 變化和異步.它們採起的方案是經過限制更新發生的時間和方式,Redux 試圖讓 state 的變化變得可預測segmentfault

Redux 三大原則

咱們先從Redux的三大原則擴展開來一個基本雛形緩存

單一數據源

整個應用的 state 被儲存在一棵 object tree 中,而且這個 object tree 只存在於惟一一個 store 中。

咱們用一個對象做惟一數據源,裏面能夠自定義各類數據服務器

// 惟一數據源
let state = {};

State 是隻讀的

惟一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通對象。

確保修改的來源是惟一的, 而Action 就是普通對象而已,所以它們能夠被日誌打印、序列化、儲存、後期調試或測試時回放出來框架

{
  type: 'DOSOMETHING',
  data: {}
}

使用純函數來執行修改

接收先前的 state 和 action,並返回新的 state

由於 reducer 只是函數,你能夠控制它們被調用的順序,傳入附加數據,甚至編寫可複用的 reducer 來處理一些通用任務dom

function channgeState(state, action) {
  switch (action.type) {
    case 'DOSOMETHING':
      return action.data
    default:
      return state
  }
}

示例一

簡單的數字計算器爲例

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>js-redux</title>
</head>

<body>
    <div class="container">
        <button id="add">+</button>
        <span id="num">0</span>
        <button id="reduce">-</button>
    </div>
    <script>
        const $add = document.getElementById('add');
        const $num = document.getElementById('num');
        const $reduce = document.getElementById('reduce');

        let val = 0;
        $add.onclick = () => $num.innerText = ++val;
        $reduce.onclick = () => $num.innerText = --val;
    </script>
</body>

</html>

咱們實現了基本加減功能

文章的完整代碼能夠直接查看demo1

示例二(三大原則)

把原生寫法轉成上面說的三大原則思想實現

index.html

-------------省略部分代碼----------------
// 初始數據
let initStore = {
    count: 0
}
// 純函數修改
function reducer(state, action) {
    switch (action.type) {
        case 'ADD':
            return {
                ...state,
                count: state.count + 1
            };
        case 'REDUCE':
            return {
                ...state,
                count: state.count - 1
            }
    }
}
// 實例化store
let store = createStore(initStore, reducer);
$add.onclick = () => {
    store.dispatch({
        type: 'ADD'
    })
    $num.innerText = store.getState().count
}
$reduce.onclick = () => {
    store.dispatch({
        type: 'REDUCE'
    })
    $num.innerText = store.getState().count
}

index.js

function createStore (initStore = {}, reducer) {
  // 惟一數據源
  let state = initStore

  // 惟一獲取數據函數
  const getState = () => state

  // 純函數來執行修改,只返回最新數據
  const dispatch = (action) => {
    state = reducer(state, action)
  }

  return {
    getState,
    dispatch
  }
}

如今看各自功能劃分基本明確,可是比較麻煩的是每次修改以後都得手動獲取最新的數據展現,這種體驗至關繁瑣,而Redux的store提供了一個監聽事件,因此咱們也來實現一個

文章的完整代碼能夠直接查看demo2

實例三(監聽事件)

咱們看看介紹

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

index.js

function createStore (initStore = {}, reducer) {
  // 惟一數據源
  let state = initStore
  // 監聽隊列
  const listenList = []

  // 惟一獲取數據函數
  const getState = () => state

  // 純函數來執行修改,只返回最新數據
  const dispatch = (action) => {
    state = reducer(state, action)
    listenList.forEach((listener) => {
      listener(state)
    })
  }

  // 添加監聽器, 同時返回解綁該事件的函數
  const subscribe = (fn) => {
    listenList.push(fn)
    return function unsubscribe () {
      listenList = listenList.filter((listener) => fn !== listener)
    }
  }

  return {
    getState,
    dispatch,
    subscribe
  }
}

index.html

-------------省略部分代碼----------------
// 實例化store
let store = createStore(initStore, reducer);
// 自動監聽渲染數據
store.subscribe(() => {
    $num.innerText = store.getState().count
})
$add.onclick = () => {
    store.dispatch({
        type: 'ADD'
    })
}
$reduce.onclick = () => {
    store.dispatch({
        type: 'REDUCE'
    })
}

文章的完整代碼能夠直接查看demo3

實例四(模塊劃分)

由於咱們已經達到功能使用的階段,接下來就該將每一個功能區劃分開來,按照Redux的使用模式重寫代碼

createStore.js

function createStore (initStore = {}, reducer) {
  // 惟一數據源
  let state = initStore
  // 監聽隊列
  const listenList = []

  // 惟一獲取數據函數
  const getState = () => state

  // 純函數來執行修改,只返回最新數據
  const dispatch = (action) => {
    state = reducer(state, action)
    listenList.forEach((listener) => {
      listener(state)
    })
  }

  // 添加監聽器, 同時返回解綁該事件的函數
  const subscribe = (fn) => {
    listenList.push(fn)
    return function unsubscribe () {
      listenList = listenList.filter((listener) => fn !== listener)
    }
  }

  return {
    getState,
    dispatch,
    subscribe
  }
}

actions.js

將每一個action都定義成一個函數

function add () {
  return {
    type: 'ADD'
  }
}
function reduce () {
  return {
    type: 'REDUCE'
  }
}

reducers.js

注意,即便沒有符合條件,也必須返回原值

這裏能夠看出,隨着分發器越多顯得就越臃腫,不適於業務代碼的編寫,下面會講怎麼解決

// 純函數修改
function reducers (state, action) {
  switch (action.type) {
    case 'ADD':
      return {
        ...state,
        count: state.count + 1
      }
    case 'REDUCE':
      return {
        ...state,
        count: state.count - 1
      }
    // 默認返回原值
    default:
      return state
  }
}

store.js

// 初始數據
const initStore = {
  count: 0
}
// 實例化store
let store = createStore(initStore, reducers)

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>js-redux</title>
</head>

<body>
    <div class="container">
        <button id="add">+</button>
        <span id="num">0</span>
        <button id="reduce">-</button>
    </div>
    <script src="./createStore.js"></script>
    <script src="./actions.js"></script>
    <script src="./reducers.js"></script>
    <script src="./store.js"></script>
    <script>
        // 選擇器
        const $add = document.getElementById('add');
        const $num = document.getElementById('num');
        const $reduce = document.getElementById('reduce');
        // 自動監聽渲染數據
        store.subscribe(() => {
            $num.innerText = store.getState().count
        })
        $add.onclick = () => {
            store.dispatch(add())
        }
        $reduce.onclick = () => {
            store.dispatch(reduce())
        }
    </script>
</body>

</html>

文章的完整代碼能夠直接查看demo4

合併分發器

combineReducers 輔助函數的做用是,把一個由多個不一樣 reducer 函數做爲 value 的 object,合併成一個最終的 reducer 函數,而後就能夠對這個 reducer 調用 createStore 方法。

合併後的 reducer 能夠調用各個子 reducer,並把它們返回的結果合併成一個 state 對象。 由 combineReducers() 返回的 state 對象,會將傳入的每一個 reducer 返回的 state 按其傳遞給 combineReducers() 時對應的 key 進行命名。

從介紹能夠知道大概須要實現的功能

  • 接收多個不一樣的reducer 函數對象
  • 將傳入的每一個 reducer 返回的 state 按其傳遞給 combineReducers() 時對應的 key 進行命名
  • 每一個reducer單獨處理子state
  • 返回最終的 reducer 函數

combineReducers .js

function combineReducers (reducers) {
  // 獲取索引值
  const reducerKeys = Object.keys(reducers)
  // 最終返回的reducer對象
  const finalReducers = {}
  // 篩選索引值對應的函數類型才賦值到最終reducer對象
  reducerKeys.forEach((key) => {
    if (typeof reducers[key] === 'function') finalReducers[key] = reducers[key]
  })
  // 獲取最終reducer對象索引值
  const finalReducerKeys = Object.keys(finalReducers)

  // 返回給store初始化使用的分發函數
  return function (state = {}, action) {
    // 是否改變和新的state
    let isChange = false
    const nextState = {}
    // 遍歷觸發對應分發器
    finalReducerKeys.forEach((key) => {
      // 當階段數據
      const oldState = state[key]
      // 分發器處理後最新數據
      const newState = finalReducers[key](oldState, action)
      nextState[key] = newState
      // 對比先後數據是否一致
      isChange = isChange || oldState !== newState
    })
    // 檢測分發器處理後階段的數據值有沒發生變化
    isChange = isChange || finalReducerKeys.length !== Object.keys(state).length
    return isChange ? nextState : state
  }
}

實際源碼大致一致,只是裏面使用ts實現而且我省略了不少參數判斷和錯誤提示,你們能夠直接查看源碼,兩百行左右並不複雜 combineReducers

示例五(合併分發器)

咱們投入實戰使用

actions.js

新增action描述

function add () {
  return {
    type: 'ADD'
  }
}
function reduce () {
  return {
    type: 'REDUCE'
  }
}
function multiply () {
  return {
    type: 'MULTIPLY'
  }
}
function divide () {
  return {
    type: 'DIVIDE'
  }
}

reducer.js

實現重點:

  • 數據處理映射到每一個單獨的函數操做
  • 每一個函數只負責該映射數據的處理
// 純函數修改
function arNum (state, action) {
  switch (action.type) {
    case 'ADD':
      return state + 1
    case 'REDUCE':
      return state - 1
    // 默認返回原值
    default:
      return state
  }
}

// 純函數修改
function mdNum (state, action) {
  switch (action.type) {
    case 'MULTIPLY':
      return state * 2
    case 'DIVIDE':
      return state / 2
    // 默認返回原值
    default:
      return state
  }
}

const reducers = combineReducers({
  arNum,
  mdNum
})

store.js

數據源的初始數據修改

// 初始數據
const initStore = {
  arNum: 0,
  mdNum: 1
}
// 實例化store
let store = createStore(initStore, reducers)

index.html

新增結構實現加減乘除功能

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>js-redux</title>
</head>

<body>
    <div class="container">
        <button id="add">+</button>
        <span id="num1">0</span>
        <button id="reduce">-</button>
        <button id="multiply">×</button>
        <span id="num2">1</span>
        <button id="divide">÷</button>
    </div>
    <script src="./createStore.js"></script>
    <script src="./combineReducers .js"></script>
    <script src="./actions.js"></script>
    <script src="./reducers.js"></script>
    <script src="./store.js"></script>
    <script>
        // 選擇器
        const $add = document.getElementById('add');
        const $reduce = document.getElementById('reduce');
        const $multiply = document.getElementById('multiply');
        const $divide = document.getElementById('divide');
        const $num1 = document.getElementById('num1');
        const $num2 = document.getElementById('num2');
        // 自動監聽渲染數據
        store.subscribe(() => {
            $num1.innerText = store.getState().arNum
            $num2.innerText = store.getState().mdNum
        })
        $add.onclick = () => store.dispatch(add())
        $reduce.onclick = () => store.dispatch(reduce())
        $multiply.onclick = () => store.dispatch(multiply())
        $divide.onclick = () => store.dispatch(divide())
    </script>
</body>

</html>

至此redux的基本功能咱們都已經一步步實現完成了

文章的完整代碼能夠直接查看demo5

相關文章
相關標籤/搜索