redux 原始碼淺析

Grewer發表於2021-05-31

redux 原始碼淺析

redux 版本號: "redux": "4.0.5"

redux 作為一個十分常用的狀態容器庫, 大家都應該見識過, 他很小巧, 只有 2kb, 但是珍貴的是他的 reducerdispatch 這種思想方式

在閱讀此文之前, 先了解/使用 redux 相關知識點, 才能更好地閱讀本文

入口檔案

入口是在 redux/src/index.js 中, 在入口檔案中只做了一件事件, 就是引入檔案, 集中匯出
現在我們根據他匯出的方法, 來進行分析

createStore

這個是 redux 最主要的 API

使用

搭配這使用方法一起, 可以更好的瀏覽原始碼

createStore(reducer, [preloadedState], [enhancer])

他的主要功能就是建立一個 store, 將 reducer 轉換到 store

引數

一共可接受三個引數:

  1. reducer (函式): 一個返回下一個狀態樹的還原函式,給定當前狀態樹和一個要處理的動作。

  2. [preloadedState] (任意值): 初始值, 可以是來自於 storage 中的; 如果你用combinedReducers產生了reducer,這必須是一個普通物件,其型別與傳遞給它的鍵相同。
    也可以自由地傳遞任何你的reducer能夠理解的東西。

  3. [enhancer] (函式): store 的增強器, 可以選擇性的增強, 用程式碼來說就是 enhancer(createStore)(reducer, preloadedState), enhancer
    接受的引數就是 createStore, 同樣地他也需要 return 一個類似於 createStore 的結果, 也就是說, 只有我們返回的是 一個像 createStore 的東西,
    他的具體實現我們就可以有很多微調 這裡附上一篇探討 enhancerapplyMiddleware 的文章 https://juejin.cn/post/6844903543502012429

// 簡單的例子:

function counterReducer(state, action) {
    switch (action.type) {
        case 'counter/incremented':
            return {value: state.value + 1}
        case 'counter/decremented':
            return {value: state.value - 1}
        default:
            return state
    }
}


let store = createStore(counterReducer, {
    value: 12345
})

store

createStore 返回的當然是一個 store, 他有自己的 api

getState

返回應用程式的當前狀態樹

const state = store.getState()

dispatch(action)

這個其實不用我多說, 會 redux 的都應該知道這個

store.dispatch({type: 'counter/incremented'})

subscribe(listener)

新增一個監聽器, 每當 action dispatch 的時候, 都會呼叫 listener, 在 listener 中可以使用 getState 來獲取當前的狀態樹

const unsubscribe = store.subscribe(() => {
    console.log('listener run')
    const current = store.getState()
    if (current.value === 12350) {
        unsubscribe()
    }
})

展示一個場景, 監聽事件, 當達到某個條件之後, 解除監聽事件

replaceReducer(nextReducer)

使用一個 reducer 替換當前的 reducer,對於 reducers 實現動態載入,也可以為 Redux 實現熱過載機制

原始碼解析

createStore 檔案是在 redux/src/createStore.js 中, 他接受的引數就是上面我們說的那三個, 返回的也就是 store

首先是一段引數的判斷, 以及 enhancer 的返回

// 為了適配 createStore(reducer, enhancer) 的情況
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
}

if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
        throw new Error('Expected the enhancer to be a function.')
    }
    // enhancer 的使用場景
    return enhancer(createStore)(reducer, preloadedState)
}

接下來定義一些變數和函式

let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false


// 如果相等 , 做了一層淺拷貝  將 currentListeners 同步到 nextListeners 中
// 避免相互影響
function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
        nextListeners = currentListeners.slice()
    }
}

store.getState

function getState() {
    // isDispatching 預設為 false, 表示當前 store 是否正在 dispatch
    if (isDispatching) {
        throw new Error('//...')
    }

    // 直接返回當前 state , 預設為入參 preloadedState
    return currentState
}

store.subscribe

// 忽略了錯誤判斷
function subscribe(listener) {
    let isSubscribed = true

    // 同步 nextListeners , currentListeners
    ensureCanMutateNextListeners()

    // 將 listener 加入 nextListeners 
    nextListeners.push(listener)

    // 返回解除監聽函式
    return function unsubscribe() {
        if (!isSubscribed) {
            // 如果 isSubscribed 已經為 false 了 則 return
            // 情況 1, 已經執行過unsubscribe了一次
            return
        }

        // flag
        isSubscribed = false

        // 同步 nextListeners , currentListeners
        ensureCanMutateNextListeners()
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
        // 搜尋 監聽器, 刪除
        currentListeners = null
    }
}

store.dispatch

  function dispatch(action) {
    // 省略了 action 的 錯誤丟擲
    // 總結:  action  必須是一個 Object  且 action.type 必須有值存在

    // 如果當前正在 isDispatching 則丟擲 錯誤(一般來說不存在

    try {
        isDispatching = true
        // 執行 reducer, 需要注意的是 currentReducer 不能為非同步函式
        currentState = currentReducer(currentState, action)
    } finally {
        isDispatching = false
    }

    //  將 nextListeners 賦值給 currentListeners 執行 nextListeners 裡面的監聽器
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i]
        listener()
    }

    // 返回 action
    return action
}

store.replaceReducer

function replaceReducer(nextReducer) {
    // 如果 nextReducer 不是函式則丟擲錯誤

    // 直接替換
    currentReducer = nextReducer

    // 類似 ActionTypes.INIT.  替換值
    dispatch({type: ActionTypes.REPLACE})
}

store.observable

還有一個額外的 observable 物件:

// 一個 Symbol.observable 的 polyfill
import $$observable from 'symbol-observable'

function observable() {
    // subscribe 就是 store.subscribe
    const outerSubscribe = subscribe
    return {
        subscribe(observer) {
            // 如果 observer 不是物件或者為 null 則丟擲錯誤

            function observeState() {
                if (observer.next) {
                    // next 的入參為 當然 reducer 的值
                    observer.next(getState())
                }
            }

            observeState()

            // 新增了監聽
            const unsubscribe = outerSubscribe(observeState)
            return {unsubscribe}
        },

        // 獲取到當前 物件, $$observable 值是一個 symbol
        [$$observable]() {
            return this
        }
    }
}

這裡使用了 tc39 裡未上線的標準程式碼 Symbol.observable, 如果你使用或者瞭解過 rxjs, 那麼這個對於你來說就是很簡單的, 如果不熟悉,
可以看看這篇文章: https://juejin.cn/post/6844903714998730766

剩餘程式碼

function createStore() {
    // 省略

    // 初始化了下值
    dispatch({type: ActionTypes.INIT})

    // 返回
    return {
        dispatch,
        subscribe,
        getState,
        replaceReducer,
        [$$observable]: observable
    }
}

combineReducers

使用

// 可以接受多個 reducer, 實現一種 module 的功能
rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer})


// 返回值
{
    potato: {
        // 某些屬性
    }
,
    tomato: {
        // 某些屬性
    }
}


const store = createStore(rootReducer, {
    potato: {
        // 初始值
    }
})

有一點需要注意的是, reducer 都是需要預設值的,如:

function counterReducer(state = {value: 0}, action) {
    //...
}

原始碼解析

combineReducers

先看 combineReducers 執行之後產生了什麼

function combineReducers(reducers) {
    // 第一步是獲取 key, 他是一個陣列
    const reducerKeys = Object.keys(reducers)
    const finalReducers = {}

    // 遍歷 reducers, 賦值到 finalReducers 中, 確保 reducer 是一個函式, 不是函式則過濾
    for (let i = 0; i < reducerKeys.length; i++) {
        const key = reducerKeys[i]

        // 省略 reducers[key] 如果是 undefined 丟擲錯誤

        if (typeof reducers[key] === 'function') {
            finalReducers[key] = reducers[key]
        }
    }

    // finalReducerKeys 一般來說是和 reducerKeys 相同的
    const finalReducerKeys = Object.keys(finalReducers)

    //定義了兩個遍歷
    let unexpectedKeyCache
    let shapeAssertionError

    try {
        // 此函式後面會詳細講述
        // 答題作用就是確認 finalReducers 中都是有初始值的
        assertReducerShape(finalReducers)
    } catch (e) {
        shapeAssertionError = e
    }
    //...
}

再看他又返回了什麼(記住結果必然也是一個 reducer)

function combineReducers(reducers) {

    //...


    return function combination(state = {}, action) {
        // 如果 assertReducerShape 出錯則丟擲錯誤
        if (shapeAssertionError) {
            throw shapeAssertionError
        }

        // 忽略非 production 程式碼

        // 預先定義一些變數
        let hasChanged = false
        const nextState = {}

        // 迴圈 finalReducerKeys 
        for (let i = 0; i < finalReducerKeys.length; i++) {
            const key = finalReducerKeys[i]
            const reducer = finalReducers[key]

            const previousStateForKey = state[key] // 這是一開始的值
            const nextStateForKey = reducer(previousStateForKey, action) // 通過 reducer 再次生成值

            // 如果 nextStateForKey === undefined 則再次丟擲異常

            // 給 nextState 賦值
            nextState[key] = nextStateForKey

            // 判斷是否改變 (初始值是 false)  判斷簡單的使用 !== 來比較
            // 如果已經為 true   就一直為 true 了
            hasChanged = hasChanged || nextStateForKey !== previousStateForKey
        }

        // 迴圈後再次對 true 做出判斷
        // 是否少了 reducer 而造成誤判
        hasChanged =
            hasChanged || finalReducerKeys.length !== Object.keys(state).length

        // 如果改變了 返回新值, 否則返回舊值
        return hasChanged ? nextState : state
    }
}

combineReducers 基本就是上述兩個函式的結合, 通過迴圈遍歷所有的 reducer 計算出值

assertReducerShape

function assertReducerShape(reducers) {
    Object.keys(reducers).forEach(key => {
        // 遍歷引數裡的 reducer
        const reducer = reducers[key]

        //執行初始操作 產生初始值都有初始值
        const initialState = reducer(undefined, {type: ActionTypes.INIT})

        //...   如果 initialState 是 undefined 則丟擲錯誤


        // 如果 reducer 執行未知操作  返回的是 undefined 則丟擲錯誤
        // 情景: 當前 reducer 使用了 ActionTypes.INIT 來產生值, 這能夠通過上一步
        // 但在這一步就會被檢測出來
        if (
            typeof reducer(undefined, {
                type: ActionTypes.PROBE_UNKNOWN_ACTION()
            }) === 'undefined'
        ) {
            //... 丟擲錯誤
        }
    })
}

這裡我們可以知道一點, 所有 reducer 我們都必須要有一個初始值, 而且他不能是 undefined, 可以是 null

compose

這裡需要先講 compose 的使用 才能順帶過渡到下面

使用

就如他的名字, 是用來組合函式的, 接受刀哥函式, 返回執行的最終函式:

// 這裡常用來 連結多箇中介軟體
const store = createStore(
    reducer,
    compose(applyMiddleware(thunk), DevTools.instrument())
)

原始碼解析

他的原始碼也很簡單:

function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }

    if (funcs.length === 1) {
        return funcs[0]
    }
    // 上面都是 控制, 引數數量為 0 和 1 的情況


    // 這裡是重點, 將迴圈接收到的函式陣列
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

我們將 reduce 的執行再度裝飾下:

// reduce 中沒有初始值的時候, 第一個 `prevValue` 是取  `funcs[0]` 的值

funcs.reduce((prevValue, currentFunc) => (...args) => prevValue(currentFunc(...args)))

reducer 返回的就是 這樣一個函式 (...args) => prevValue(currentFunc(...args)), 一層一層巢狀成一個函式

舉一個簡單的輸入例子:

var foo = compose(val => val + 10, () => 1)

foo 列印:

(...args) => a(b(...args))

執行 foo() , 返回 11

applyMiddleware

使用

applyMiddleware 是使用在 createStore 中的 enhancer 引數來增強 redux 的作用

可相容多種三方外掛, 例如 redux-thunk, redux-promise, redux-saga 等等

這裡使用官網的一個例子作為展示:


function logger({getState}) {
    // next 就是真正的 store.dispatch
    return next => action => {
        console.log('will dispatch', action)

        const returnValue = next(action)

        console.log('state after dispatch', getState())

        return returnValue
    }
}

const store = createStore(rootReducer, {
    counter: {value: 12345}
}, applyMiddleware(logger))

原始碼解析

default

function applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        // 因為使用了 enhancer 引數, 他的內部沒有 createStore 的東西, 所以這裡需要重新 createStore
        const store = createStore(...args)
        let dispatch = () => {
            // 在中介軟體中 不允許使用 dispatch
            throw new Error(
                // 省略報錯...
            )
        }

        // 這是要傳遞的引數
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }

        // 重新 map 所有 middlewares 返回需要的結果
        const chain = middlewares.map(middleware => middleware(middlewareAPI))

        // 這裡就是我們上面的 compose 相關的程式碼, 返回的結果 再次執行 得到真正的 dispatch
        dispatch = compose(...chain)(store.dispatch)

        // 返回 store 和 dispatch
        return {
            ...store,
            dispatch
        }
    }
}

這裡我們需要重新捋一捋函式的執行, 中介軟體以上述的 logger 為例子

applyMiddleware(logger) -> 返回的是一個函式(createStore) => (...args) => {/*省略*/} 我把他記為中介軟體函式 1
也就是說 applyMiddleware(logger) === (createStore) => (...args) => {/*省略*/}

這個函式將在 createStore 中使用 enhancer(createStore)(reducer, preloadedState) 這裡的 enhancer 就是中介軟體函式 1 通過 createStore
的執行我們可以發現
store === createStore(reducer, preloadedState, enhancer) === enhancer(createStore)(reducer, preloadedState)
=== applyMiddleware(logger)(createStore)(reducer, preloadedState)
=== ((createStore) => (...args) => {/*省略*/})(createStore)(reducer, preloadedState)
=== 中介軟體函式 1 中的{/*省略*/} 返回結果 通過這一層的推論我們可以得出 store === 中介軟體函式 1中的 {/*省略*/} 返回結果

bindActionCreators

使用

這個 API 主要是用來方便 dispatch 的 他接受 2 個引數 , 第一個是物件或函式, 第二個就是 dispatch 返回值的型別很第一個引數相同

首先我們要定義建立 action 的函式

function increment(value) {
    return {
        type: 'counter/incremented',
        payload: value
    }
}

function decrement(value) {
    return {
        type: 'counter/decremented',
        payload: value
    }
}

使用情況 1:

function App(props) {
    const {dispatch} = props

    // 因為在 hooks 中使用 加上了 useMemo
    const fn = useMemo(() => bindActionCreators(increment, dispatch), [dispatch])

    return (
        <div className="App">
            <div>
                val: {props.value}
            </div>
            <button onClick={() => {
                fn(100)
            }}>plus
            </button>
        </div>
    );
}

使用情況 2:

function App(props) {
    const {dispatch} = props

    const fn = useMemo(() => bindActionCreators({
        increment,
        decrement
    }, dispatch), [dispatch])


    // 如果想用 decrement 也是這樣呼叫 fn.decrement(100)
    return (
        <div className="App">
            <div>
                val: {props.value}
            </div>
            <button onClick={() => {
                fn.increment(100)
            }}>plus
            </button>
        </div>
    );
}

原始碼解析

function bindActionCreator(actionCreator, dispatch) {
    return function () {
        // 執行 dispatch(actionCreator()) === dispatch({type:''}) 
        return dispatch(actionCreator.apply(this, arguments))
    }
}

function bindActionCreators(actionCreators, dispatch) {
    if (typeof actionCreators === 'function') {
        // 如果是函式直接執行 bindActionCreator
        return bindActionCreator(actionCreators, dispatch)
    }

    if (typeof actionCreators !== 'object' || actionCreators === null) {
        throw new Error(/*省略*/)
    }

    // 定義變數
    const boundActionCreators = {}
    
    // 因為是物件 迴圈遍歷, 但是 for in 效率太差
    for (const key in actionCreators) {
        const actionCreator = actionCreators[key]
       
       // 過濾
        if (typeof actionCreator === 'function') {
            // 和函式同樣 執行 bindActionCreator 並且賦值到 boundActionCreators 中
            boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
        }
    }
    return boundActionCreators
}

bindActionCreators 的原始碼相對簡單一點, 理解起來相對也容易很多

總結

redux 中設計的很多地方都是很巧妙的,並且短小精悍, 值得大家作為首次原始碼閱讀的選擇

如果我講的有什麼問題, 還望不吝指教

相關文章: react-redux 原始碼淺析

本文程式碼倉庫: https://github.com/Grewer/react-redux-notes

參考文件:

相關文章