實現一個迷你Redux(基礎版)

前言

本文從將從Redux原理出發,一步步本身實現一個簡單的Redux,主要目的是瞭解Redux內部之間的聯繫。看本文以前先要知道Redux是怎麼用的,對Redux用法不會講解太多。javascript

Redux介紹

首先要知道的是,Redux 和 React 沒有關係,Redux 能夠用在任何框架中。
Redux 是一個JavaScript 狀態管理器,是一種新型的前端「架構模式」。html

還有一般與redux一塊兒用的一個庫——react-redux, 它就是把 Redux 這種架構模式和 React.js 結合起來的一個庫,就是 Redux 架構在 React.js 中的體現。前端

設計思想

  1. Web 應用是一個狀態機,視圖與狀態是一一對應的。
  2. 全部的狀態,保存在一個對象裏面。

什麼時候使用Redux

  1. 用戶的使用方式複雜
  2. 不一樣身份的用戶有不一樣的使用方式(好比普通用戶和管理員)
  3. 多個用戶之間能夠協做
  4. 與服務器大量交互,或者使用了WebSocket
  5. View要從多個來源獲取數據

從組件的角度看:java

  1. 某個組件的狀態,須要共享
  2. 某個狀態須要在任何地方均可以拿到
  3. 一個組件須要改變全局狀態
  4. 一個組件須要改變另外一個組件的狀態

Redux工做流程

  1. Redux 將整個應用狀態(state)存儲到一個地方(一般咱們稱其爲 store)
  2. 當咱們須要修改狀態時,必須派發(dispatch)一個 action( action 是一個帶有 type 字段的對象)
  3. 專門的狀態處理函數 reducer 接收舊的 state 和 action ,並會返回一個新的 state
  4. 經過 subscribe 設置訂閱,每次派發動做時,通知全部的訂閱者。

從這個流程中能夠看出,Redux 的核心就是一個 觀察者 模式。一旦 store 發生了變化就會通知全部的訂閱者,視圖(在這裏是react組件)接收到通知以後會進行從新渲染。react

Redux案例

爲了簡化說明,我用和官網差很少的例子改寫來做案例
官網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值服務器

動手實現一個Redux

  1. 首先要手動實現一個Redux以前,先看看有上述代碼涉及到Redux什麼方法,首先引入了
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 
    }
}
  1. store.getState()用於獲取 state 數據,其實就是簡單地把 state 參數返回。因而
export function createStore (reducer) {
    let currentState = {}
    const getState = () => currentState
    
    return { 
        getState
    }
}
  1. dispatch方法會接收一個action,執行會調用 reducer 返回一個新狀態
export function createStore (reducer) {
    let currentState = {}
    const getState = () => currentState
    const dispatch = (action) => {
        currentState = reducer(currentState, action) // 覆蓋原來的state
    }
    return { 
        getState,
        dispatch
    }
}
  1. 經過 subscribe 設置監聽函數(設置訂閱),一旦 state 發生變化,就自動執行這個函數(通知全部的訂閱者)。

怎麼實現呢?咱們能夠直接使用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徹底同樣的輸出效果了。

完善Redux

接下來咱們繼續補充知識點

  1. createStore實際有三個參數,即
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)咱們下篇會說,這篇先忽略。

  1. 咱們實現了store.subscribe()方法,但仍是不完整的,subscribe方法能夠添加監聽函數listener,它還有返回值,返回一個移除listener的函數;另外咱們依然要對類型進行判斷。
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(); // 取消監聽
  1. diaptch方法執行完返回爲action,而後咱們一樣須要爲它做判斷
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
}

參考資料:

Redux 入門教程(一):基本用法


相關文章
相關標籤/搜索