本文從將從Redux原理出發,一步步本身實現一個簡單的Redux,主要目的是瞭解Redux內部之間的聯繫。看本文以前先要知道Redux是怎麼用的,對Redux用法不會講解太多。javascript
首先要知道的是,Redux 和 React 沒有關係,Redux 能夠用在任何框架中。
Redux 是一個JavaScript 狀態管理器,是一種新型的前端「架構模式」。html
還有一般與redux一塊兒用的一個庫——react-redux, 它就是把 Redux 這種架構模式和 React.js 結合起來的一個庫,就是 Redux 架構在 React.js 中的體現。前端
從組件的角度看:java
從這個流程中能夠看出,Redux 的核心就是一個 觀察者 模式。一旦 store 發生了變化就會通知全部的訂閱者,視圖(在這裏是react組件)接收到通知以後會進行從新渲染。react
爲了簡化說明,我用和官網差很少的例子改寫來做案例
官網redux demogit
新建一個文件redux.js,而後直接引入,觀察控制檯輸出github
import { createStore } from 'redux' const defaultState = { value: 10 } // reducer處理函數 function reducer (state = defaultState, action) { console.log(state, action) switch (action.type) { case 'INCREMENT': return { ...state, value: state.value + 1 } case 'DECREMENT': return { ...state, value: state.value - 1 } default: return state } } const store = createStore(reducer) const init = store.getState() console.log(`一開始數字爲:${init.value}`) function listener () { const current = store.getState() console.log(`當前數字爲:${current.value}`) } store.subscribe(listener) // 監聽state的改變 store.dispatch({ type: 'INCREMENT' }) // 當前數字爲:11 store.dispatch({ type: 'INCREMENT' }) // 當前數字爲:12 store.dispatch({ type: 'DECREMENT' }) // 當前數字爲:11 export default store
輸出結果:redux
{value: 10} {type: "@@redux/INIT1.a.7.g.7.t"} 一開始數字爲:10 {value: 10} {type: "INCREMENT"} 當前數字爲:11 {value: 11} {type: "INCREMENT"} 當前數字爲:12 {value: 12} {type: "DECREMENT"} 當前數字爲:11
全部對數據的操做必須經過 dispatch 函數,它接受一個參數action,action是一個普通的JavaScript對象,action必須包含一個type
字段,告訴它要修改什麼,只有它容許才能修改。數組
在每次調用進來reducer函數咱們都打印了state和action,咱們手動經過store.dispatch方法派發了三次action,但你會發現輸出了四次.這是由於Redux內部初始化就自動執行了一次dispatch方法,能夠看到第一次執行它的type對咱們數據來講是沒有影響的(由於type取值@@redux/INIT1.a.7.g.7.t
,咱們本身redux的數據type不會取名成這個樣子,因此不會跟它重複),即默認輸出state值服務器
import { createStore } from 'redux' // 傳入reducer const store = createStore(reducer)
createStore 會返回一個對象,這個對象包含三個方法,因而咱們能夠列出Redux雛形。
新建mini-redux.js
export function createStore (reducer) { const getState = () => {} const subscribe = () => {} const dispatch = () => {} return { getState, subscribe, dispatch } }
export function createStore (reducer) { let currentState = {} const getState = () => currentState return { getState } }
reducer
返回一個新狀態export function createStore (reducer) { let currentState = {} const getState = () => currentState const dispatch = (action) => { currentState = reducer(currentState, action) // 覆蓋原來的state } return { getState, dispatch } }
怎麼實現呢?咱們能夠直接使用subscribe函數把你要監聽的事件添加到數組, 而後執行dispatch方法的時候把listeners數組的監聽函數給執行一遍。
export function createStore (reducer) { let currentState = {} let currentListeners = [] // 監聽函數,可添加多個 const getState = () => currentState const subscribe = (listener) => { currentListeners.push(listener) } const dispatch = (action) => { currentState = reducer(currentState, action) // 覆蓋原來的state currentListeners.forEach(listener => listener()) } return { getState, subscribe, dispatch } }
翻開一開始咱們那個Redux例子,其實就是把store.getState()
添加進來,dispatch派發一個action後,reducer執行返回新的state,並執行了監聽函數store.getState()
,state的值就發生變化了。
function listener () { const current = store.getState() console.log(`當前數字爲:${current.value}`) } store.subscribe(listener) // 監聽state的改變
上述代碼,跟React依然沒有關係,只是純屬Redux例子。但想想當咱們把Redux和React一塊兒用的時候,還會多作這麼一步。
constructor(props) { super(props) this.state = store.getState() this.storeChange = this.storeChange.bind(this) store.subscribe(this.storeChange) } storeChange () { this.setState(store.getState()) }
在React裏面監聽的方法,還要用this.setState()
, 這是由於React中state的改變必須依賴於this.setState
方法。因此對於 React 項目,就是組件的render方法或setState方法放入listen(監聽函數),纔會實現視圖的自動渲染,改變頁面中的state值。
最後一步,注意咱們上面說的,當初始化的時候,dispatch會先自動執行一次,繼續改代碼
export function createStore (reducer) { let currentState = {} let currentListeners = [] // 監聽器,可監聽多個事件 const getState = () => currentState const subscribe = (listener) => { currentListeners.push(listener) } const dispatch = (action) => { currentState = reducer(currentState, action) // 覆蓋原來的state currentListeners.forEach(listener => listener()) } // 儘可能寫得複雜,使不會與咱們自定義的action有重複可能 dispatch({ type: '@@mini-redux/~GSDG4%FDG#*&' }) return { getState, subscribe, dispatch } }
寫到這裏,咱們把引入的redux替換咱們寫的文件
import { createStore } from './mini-redux'
當咱們執行的時候,發現結果並不如咱們所願:
{} {type: "@@mini-redux/~GSDG4%FDG#*&"} 一開始數字爲:undefined {} {type: "INCREMENT"} 當前數字爲:NaN {type: "INCREMENT"} 當前數字爲:NaN {value: NaN} {type: "DECREMENT"} 當前數字爲:NaN
這個怎麼回事呢?由於咱們寫的redux一開始就給state賦值爲{},在事實state初始值是由外部傳入的,一般咱們本身寫的時候會設置默認值
const defaultState = { value: 10 } function reducer (state = defaultState, action) { switch (action.type) { // ... default: return state } }
但在咱們Redux實現中卻把它手動置爲空對象,在這裏咱們暫時解決方法就是不給它賦值,讓它爲undefined,這樣reducer的默認參數就會生效。redux初始化第一次dispatch時,就會讓它自動賦值爲reducer傳入的第一個參數state默認值(ES6函數默認賦值),因此修改以下:
export function createStore (reducer) { let currentState let currentListeners = [] // 監聽器,可監聽多個事件 const getState = () => currentState const subscribe = (listener) => { currentListeners.push(listener) } const dispatch = (action) => { currentState = reducer(currentState, action) // 覆蓋原來的state currentListeners.forEach(listener => listener()) } dispatch({ type: '@@mini-redux/~GSDG4%FDG#*&' }) return { getState, subscribe, dispatch } }
這個mini-redux.js,咱們就能夠實現跟原來的redux徹底同樣的輸出效果了。
接下來咱們繼續補充知識點
createStore(reducer, [preloadedState], enhancer)
第二個參數 [preloadedState] (any)
是可選的: initial state
第三個參數enhancer(function)
也是可選的:用於添加中間件的
一般狀況下,經過 preloadedState 指定的 state 優先級要高於經過 reducer 指定的 state。這種機制的存在容許咱們在 reducer 能夠經過指明默認參數來指定初始數據,並且還爲經過服務端或者其它機制注入數據到 store 中提供了可能。
第三個參數咱們下篇會說,先繼續完善一下代碼,咱們須要對第二個和第三個可選參數進行判斷。
export function createStore (reducer, preloadedState, enhancer) { // 當第二個參數沒有傳preloadedState,而直接傳function的話,就會直接把這個function當成enhancer if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } // 當第三個參數傳了但不是function也會報錯 if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) } // reducer必須爲函數 if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.') } let currentState = preloadedState // 第二個參數沒傳默認就是undefined賦給currentState let currentListeners = [] // 監聽器,可監聽多個事件 // ... }
關於第三個參數判斷爲何返回return enhancer(createStore)(reducer, preloadedState)
咱們下篇會說,這篇先忽略。
export function createStore (reducer, preloadedState, enhancer) { // ... let currentListeners = [] // 監聽器,可監聽多個事件 const subscribe = (listener) => { if (typeof listener !== 'function') { throw new Error('Expected listener to be a function.') } currentListeners.push(listener) // 經過filter過濾,執行的時候將以前自己已經添加進數組的事件名移除數組 return () => { currentListeners = currentListeners.filter(l => l !== listener); } } // ... }
也能夠經過找數組下標的方式移除listener
const subscribe = (listener) => { if (typeof listener !== 'function') { throw new Error('Expected listener to be a function.') } currentListeners.push(listener) // 經過filter過濾,執行的時候將以前自己已經添加進數組的事件名移除數組 return () => { let index = currentListeners.indexOf(listener) currentListeners.splice(index, 1) } }
移除listener實際就是取消訂閱,使用方式以下:
let unsubscribe = store.subscribe(() => console.log(store.getState()) ); unsubscribe(); // 取消監聽
export function createStore (reducer, preloadedState, enhancer) { // ... let isDispatching = false const dispatch = (action) => { // 用於判斷action是否爲一個普通對象 if (!isPlainObject(action)) { throw new Error('Actions must be plain objects. ') } // 防止屢次dispatch請求同時改狀態,必定是前面的dispatch結束以後,才dispatch下一個 if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } try { isDispatching = true currentState = reducer(currentState, action) // 覆蓋原來的state } finally { isDispatching = false } currentListeners.forEach(listener => listener()) return action } } // 用於判斷一個值是否爲一個普通的對象(普通對象即直接以字面量形式或調用 new Object() 所建立的對象) export function isPlainObject(obj) { if (typeof obj !== 'object' || obj === null) return false let proto = obj while (Object.getPrototypeOf(proto) !== null) { proto = Object.getPrototypeOf(proto) } return Object.getPrototypeOf(obj) === proto } // ...
isPlainObject函數中經過 while 不斷地判斷 Object.getPrototypeOf(proto) !== null 並執行, 最終 proto 會指向 Object.prototype. 這時再判斷 Object.getPrototypeOf(obj) === proto, 若是爲 true 的話就表明 obj 是經過字面量或調用 new Object() 所建立的對象了。
保持action對象是簡單對象的做用是方便reducer進行處理,不用處理其餘的狀況(好比function/class實例等)
至此,咱們實現了最基本能用的Redux代碼,下篇再繼續完善Redux代碼,最後放出基礎版Redux全部代碼:
export function createStore (reducer, preloadedState, enhancer) { // 當第二個參數沒有傳preloadedState,而直接傳function的話,就會直接把這個function當成enhancer if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } // 當第三個參數傳了但不是function也會報錯 if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) } // reducer必須爲函數 if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.') } let currentState = preloadedState // 第二個參數沒傳默認就是undefined賦給currentState let currentListeners = [] // 監聽器,可監聽多個事件 let isDispatching = false const getState = () => currentState const subscribe = (listener) => { if (typeof listener !== 'function') { throw new Error('Expected listener to be a function.') } currentListeners.push(listener) // 經過filter過濾,執行的時候將以前自己已經添加進數組的事件名移除數組 return () => { currentListeners = currentListeners.filter(l => l !== listener); } } const dispatch = (action) => { // 用於判斷action是否爲一個普通對象 if (!isPlainObject(action)) { throw new Error('Actions must be plain objects. ') } // 防止屢次dispatch請求同時改狀態,必定是前面的dispatch結束以後,才dispatch下一個 if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } try { isDispatching = true currentState = reducer(currentState, action) // 覆蓋原來的state } finally { isDispatching = false } currentListeners.forEach(listener => listener()) return action } dispatch({ type: '@@mini-redux/~GSDG4%FDG#*&' }) return { getState, subscribe, dispatch } } // 用於判斷一個值是否爲一個普通的對象(普通對象即直接以字面量形式或調用 new Object() 所建立的對象) export function isPlainObject(obj) { if (typeof obj !== 'object' || obj === null) return false let proto = obj while (Object.getPrototypeOf(proto) !== null) { proto = Object.getPrototypeOf(proto) } return Object.getPrototypeOf(obj) === proto }
參考資料: