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
文章的完整代碼能夠直接查看算法
隨着 JavaScript 單頁應用開發日趨複雜,JavaScript 須要管理比任什麼時候候都要多的 state (狀態)。 這些 state 可能包括服務器響應、緩存數據、本地生成還沒有持久化到服務器的數據,也包括 UI 狀態,如激活的路由,被選中的標籤,是否顯示加載動效或者分頁器等等。管理不斷變化的 state 很是困難。若是一個 model 的變化會引發另外一個 model 變化,那麼當 view 變化時,就可能引發對應 model 以及另外一個 model 的變化,依次地,可能會引發另外一個 view 的變化。直至你搞不清楚到底發生了什麼。state 在何時,因爲什麼緣由,如何變化已然不受控制。 當系統變得錯綜複雜的時候,想重現問題或者添加新功能就會變得舉步維艱。redux
Redux將這些複雜度很大程度歸因於: 變化和異步.它們採起的方案是經過限制更新發生的時間和方式,Redux 試圖讓 state 的變化變得可預測segmentfault
咱們先從Redux的三大原則擴展開來一個基本雛形緩存
整個應用的 state 被儲存在一棵 object tree 中,而且這個 object tree 只存在於惟一一個 store 中。
咱們用一個對象做惟一數據源,裏面能夠自定義各類數據服務器
// 惟一數據源 let 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
把原生寫法轉成上面說的三大原則思想實現
-------------省略部分代碼---------------- // 初始數據 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 }
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。
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 } }
-------------省略部分代碼---------------- // 實例化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的使用模式重寫代碼
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 } }
將每一個action都定義成一個函數
function add () { return { type: 'ADD' } } function reduce () { return { type: 'REDUCE' } }
注意,即便沒有符合條件,也必須返回原值
這裏能夠看出,隨着分發器越多顯得就越臃腫,不適於業務代碼的編寫,下面會講怎麼解決
// 純函數修改 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 } }
// 初始數據 const initStore = { count: 0 } // 實例化store let store = createStore(initStore, reducers)
<!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 進行命名。
從介紹能夠知道大概須要實現的功能
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
咱們投入實戰使用
新增action描述
function add () { return { type: 'ADD' } } function reduce () { return { type: 'REDUCE' } } function multiply () { return { type: 'MULTIPLY' } } function divide () { return { type: 'DIVIDE' } }
實現重點:
// 純函數修改 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 })
數據源的初始數據修改
// 初始數據 const initStore = { arNum: 0, mdNum: 1 } // 實例化store let store = createStore(initStore, reducers)
新增結構實現加減乘除功能
<!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