用最基礎的方法講解 Redux 實現原理

叫我小明呀發表於2019-03-01

寫在前面

用最基礎的方法講解 Redux 實現原理?說白了其實是我能力有限,只能用最基礎的方法來講解,為了講的更加清楚,文章可能比較拖沓。不過我相信,不是很瞭解 Redux 的同學,看完我今天分享的文章一定會有所收穫!

什麼是 Redux ?

這不是我今天要說的重點,想知道什麼是 Redux 點選傳送門

開始

在開始之前我想先講一種常用的設計模式:觀察者模式。先來說一下我對觀察者模式的個人理解:觀察者模式(Publish/Subscribe)模式。對於這種模式很清楚的同學下面這段程式碼可以跳過。如果你還不清楚,你可以試著手敲一遍下面的程式碼!!

觀察者模式

觀察者模式,基於一個主題/事件通道,希望接收通知的物件(稱為subscriber)通過自定義事件訂 閱主題,通過deliver釋出主題事件的方式被通知。就和使用者訂閱微信公眾號道理一樣,只要釋出,使用者就能接收到最新的內容。

/**
 * describe: 實現一個觀察者模式
 */
let data = {
    hero: '鳳凰',
};
//用來儲存 訂閱者 的陣列
let subscribers = [];
//訂閱 新增訂閱者 方法
const addSubscriber = function(fn) {
    subscribers.push(fn)
}
//釋出
const deliver = function(name) {
    data.hero = name;
    //當資料發生改變,呼叫(通知)所有方法(訂閱者)
    for(let i = 0; i<subscribers.length; i++){
        const fn = subscribers[i]
        fn()
    }
}
//通過 addSubscriber 發起訂閱
addSubscriber(() => {
    console.log(data.hero)
})
//改變data,就會自動列印名稱
deliver('發條') 
deliver('狐狸')
deliver('卡牌')
複製程式碼

這個釋出訂閱通過 addSubscriber 來儲存訂閱者(方法fn),當通過呼叫 deliver 來改變資料的時候,就會自動遍歷 addSubscriber 來執行裡面的 fn 方法 。

為啥要講這個釋出訂閱模式呢?因為搞清楚了這個模式那麼接下來你讀該文章就會感覺更加清晰。

Redux 起步

首先我們把上面那個釋出訂閱程式碼優化一下,順便改一下命名,為什麼要改命名?主要是緊跟 Redux 的步伐。讓同學們更加眼熟。

let state = {hero: '鳳凰'};
let subscribers = [];
//訂閱 定義一個 subscribe 
const subscribe = (fn) => {
    subscribers.push(fn)
}
//釋出
const dispatch = (name) => {
    state.hero = name;
    //當資料發生改變,呼叫(通知)所有方法(訂閱者)
    subscribers.forEach(fn=>fn())
}
//通過 subscribe 發起訂閱
subscribe(() => {
    console.log(state.hero)
})
//改變state狀態,就會自動列印名稱
//這裡要注意的是,state狀態不能直接去修改
dispatch('發條') 
dispatch('狐狸')
dispatch('卡牌')
複製程式碼

現在這樣一改是不是很眼熟了,沒錯這就是一個類似redux改變狀態的思路。但是光一個釋出訂閱還是不夠的,不可能改變一個狀態需要去定義這麼多方法。所以我們把他封裝起來。

creatStore 方法

const creatStore = (initState) => {
    let state = initState;
    let subscribers = [];
    //訂閱 定義一個 subscribe 
    const subscribe = (fn) => {
        subscribers.push(fn)
    }
    //釋出
    const dispatch = (currentState) => {
        state = currentState;
        //當資料發生改變,呼叫(通知)所有方法(訂閱者)
        subscribers.forEach(fn=>fn())
    }
    // 這裡需要新增這個獲取 state 的方法
    const getState = () => {
        return state;
    }
    return {
        subscribe,
        dispatch,
        getState,
    }
}
複製程式碼

這樣就建立好了一個 createStore 方法。沒有什麼新東西,就傳進去一個初始狀態,然後在返回 subscribe, dispatch, getState 三大方法。這裡新增了個 getState 方法,程式碼很簡單就是一個 return state 為了獲取 state.

creatStore 使用

實現了 createStore 下面我們來試試如何使用他,那就拿那個非常經典的案例--計數器來試試

let initState = {
    num: 0,
}
const store = creatStore(initState);
//訂閱
store.subscribe(() => {
    let state = store.getState()
    console.log(state.num)
})
// +1
store.dispatch({
   num: store.getState().num + 1
})
//-1
store.dispatch({
   num: store.getState().num - 1
})
複製程式碼

這個樣子又接近了一點 Redux 的模樣。 不過這樣有個問題。如果你使用 store.dispatch 方法時,中間萬一寫錯了或者傳了個其他東西那就比較麻煩了。就比如下面這樣:

用最基礎的方法講解 Redux 實現原理

其實我是想 +1,+1,-1 最後應該是 1 (初始 num 為0)!但是由於寫錯了一個導致後面的都會錯。而且他還有個問題就是可以隨便的給一個新的狀態。那麼就顯得不那麼單純了。比如下面這樣:

用最基礎的方法講解 Redux 實現原理

因為惡意修改 num 為 String 型別,導致後面在使用 dispatch 由於 num 不再是 Number 型別,導致列印出 NaN,這就不是我們想要的啦。所以我們要在改造一下,讓 dispatch 變得單純一些。那要怎麼做呢?我們請一個管理者來幫我們管理,暫且給他命名 reducer

為什麼叫 reducer

我在 reducer 官網中找到下面這段介紹 reducer

用最基礎的方法講解 Redux 實現原理
什麼意思,對於這種英語上來我就是有道翻譯一下

用最基礎的方法講解 Redux 實現原理

當然這個翻譯感覺並沒什麼作用,

找一找中文 Redux 官網,他是這樣說的:

之所以將這樣的函式稱之為reducer,是因為這種函式與被傳入 Array.prototype.reduce(reducer, ?initialValue) 裡的回撥函式屬於相同的型別。保持 reducer 純淨非常重要。永遠不要在 reducer 裡做這些操作。

誒,這個翻譯似乎就清楚了很多。正如下面評論者說的一樣 靈感來自於陣列中reduce方法,是一種運算合成。那麼說到這裡我就來介紹一下 reduce。

什麼是 reduce

話不多說直接上程式碼

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15

/*該減速作用有四個引數:
*累加器(acc)
*當前價值(cur)
*當前指數(idx)
*源陣列(src)
*您的reducer函式的返回值被分配給累加器,其值在整個陣列的每次迭代中被記住,並最終成為最終的單個結果值。
*/
複製程式碼

具體引數介紹

callback
  函式在陣列中的每個元素上執行,有四個引數:
accumulator
  累加器累加回撥的返回值; 它是先前在回撥呼叫中返回的累計值,或者initialValue,如果提供(參見下文)。
currentValue
  當前元素在陣列中處理。
currentIndex可選的
  陣列中正在處理的當前元素的索引。如果initialValue提供了an,則從索引0開始,否則從索引1開始
array可選的
  該陣列reduce()被召喚。
initialValue可選的
  用作第一次呼叫的第一個引數的值callback。如果未提供初始值,則將使用陣列中的第一個元素。呼叫reduce()沒有初始值的空陣列是一個錯誤。
複製程式碼

這個方法相對比 forEach, map, filter 這個理解起來還是算比較困難的。也可以看 MDN 的 Array.prototype.reduce() 詳細介紹

注:首先感謝下面評論者 panda080 的指導,受他的建議,我重新去 Rudex 官網尋找。通過學習自己也更加的理解了 reducer 和 reduce reducer官網

ps:理解完之後,其實個人覺得 reducer 這個命名從翻譯過來的角度總覺得很怪異。可能英語有限,或許他有更加貼切的意思我還不知道。

什麼是 reducer

reducer 在我學習的過程中我把他認為是個管理者(可能這個認為是不正確的),然後我們每次想做什麼就去通知管理者,讓他在來根據我們說的去做。如果我們不小心說錯了,那麼他就不會去做。直接按預設的事情來。噔噔蹬蹬 reducer 登場!!

function reducer(state, action) {
    //通過傳進來的 action.type 讓管理者去匹配要做的事情
    switch (action.type){
        case 'add':
            return {
                ...state,
                count: state.count + 1
            }
        case 'minus':
            return {
                ...state,
                count: state.count - 1
            }
        // 沒有匹配到的方法 就返回預設的值
        default:
            return state;
    }
}
複製程式碼

增加了這個管理者,那麼我們就要重新來寫一下之前的 createStroe 方法了:把 reducer 放進去

const creatStore = (reducer,initState) => {
    let state = initState;
    let subscribers = [];
    //訂閱 定義一個 subscribe 
    const subscribe = (fn) => {
        subscribers.push(fn)
    }
    //釋出
    const dispatch = (action) => {
        state = reducer(state,action);
        subscribers.forEach(fn=>fn())
    }
    const getState = () => {
        return state;
    }
    return {
        subscribe,
        dispatch,
        getState,
    }
}
複製程式碼

很簡單的一個修改,為了讓你們方便看出修改的地方,和區別,我特意重新碼了這兩個前後的方法對比,如下圖

用最基礎的方法講解 Redux 實現原理

好,接下來我們試試新增了管理者的 creatStore 效果如何。

function reducer(state, action) {
    //通過傳進來的 action.type 讓管理者去匹配要做的事情
    switch (action.type){
        case 'add':
            return {
                ...state,
                num: state.num + 1
            }
        case 'minus':
            return {
                ...state,
                num: state.num - 1
            }
        // 沒有匹配到的方法 就返回預設的值
        default:
            return state;
    }
}

let initState = {
    num: 0,
}
const store = creatStore(reducer,initState);
//訂閱
store.subscribe(() => {
    let state = store.getState()
    console.log(state.num)
})
複製程式碼

為了看清楚結果,dispatch(訂閱)我直接在控制檯輸出,如下圖:

用最基礎的方法講解 Redux 實現原理

效果很好,我們不會再因為寫錯,而出現 NaN 或者其他不可描述的問題。現在這個 dispatch 比較純粹了一點。

我們只是給他一個 type ,然後讓管理者自己去幫我們處理如何更改狀態。如果不小心寫錯,或者隨便給個 type 那麼管理者匹配不到那麼這個動作那麼我們這次 dispatch 就是無效的,會返回我們自己的預設 state。

好叻,現在這個樣子基本上就是我腦海中第一次使用 redux 看到的樣子。那個時候我使用起來都非常困難。當時勉強實現了一下這個計數器 demo 我就默默的關閉了 vs code。

接下來我們再完善一下這個 reducer,給他再新增一個方法。並且這次我們再給 state 一個

function reducer(state, action) {
    //通過傳進來的 action.type 讓管理者去匹配要做的事情
    switch (action.type){
        case 'add':
            return {
                ...state,
                num: state.num + 1
            }
        case 'minus':
            return {
                ...state,
                num: state.num - 1
            }
        // 增加一個可以傳參的方法,讓他更加靈活
        case 'changeNum':
            return {
                ...state,
                num: state.num + action.val
            }
        // 沒有匹配到的方法 就返回預設的值
        default:
            return state;
    }
}

let initState = {
    num: 0,
}
const store = creatStore(reducer,initState);
//訂閱
store.subscribe(() => {
    let state = store.getState()
    console.log(state.num)
})
複製程式碼

控制檯再使用一次新的方法:

用最基礎的方法講解 Redux 實現原理

好叻,這樣是不是就讓 dispatch 更加靈活了。

現在我們在 reducer 中就寫了 3 個方法,但是實際專案中,方法一定是很多的,那麼都這樣寫下去,一定是不利於開發和維護的。那麼這個問題就留給大家去思考一下。

提示:Redux 也知道這一點,所以他提供了 combineReducers 去實現這個模式。這是一個高階 Reducer 的示例,他接收一個拆分後 reducer 函式組成的物件,返回一個新的 Reducer 函式。

思考完之後可以參考 redux 中文文件 的combineReducers介紹

具體實現原理我將會在下次分享。

總結

Redux 這個專案裡,有很多非常巧妙的方法,很多地方可以借鑑。畢竟這可是在 github 上有 47W+ 的 Star。

今天也只是講了他的一小部分。自己也在努力學習中,希望今後能分享更多的看法,並和大家深入探討。

寫在最後

上述每個案例,和程式碼我都託管在 github 上了,分享給大家可以直接開啟即用。github 傳送門

全文章,如有錯誤或不嚴謹的地方,請務必給予指正,謝謝!

參考:

基於下面 dsying 評論。我又扒了一上午找到曾經看的這個觀察者模式,如有知道思路類似哪裡的可以提供一個連結給我,必修改上去。

相關文章