如何在Vue里實(shí)現(xiàn)一個(gè)Redux狀態(tài)管理?

如何在Vue里實(shí)現(xiàn)一個(gè)Redux狀態(tài)管理

嗯,我們都知道 redux 通常是 react 項(xiàng)目中 中一種管理數(shù)據(jù)的手段样屠,它跟我們 vue 項(xiàng)目里的 Vuex 狀態(tài)管理類似,功能相同缺脉,但是使用方法卻有不同~

最近在學(xué)習(xí) redux 這一塊痪欲,為了更好的幫助跟我一樣的萌新更加深入的了解 redux 的內(nèi)部原理,我們就來試試攻礼,手動(dòng)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 redux 狀態(tài)管理业踢。

不同的是,我們是在 vue 項(xiàng)目中來實(shí)現(xiàn)的~

代碼未動(dòng)礁扮,目標(biāo)先行

我們要實(shí)現(xiàn) redux 知举,首先要給自己定個(gè)小目標(biāo)瞬沦,也就是一個(gè)業(yè)務(wù)場(chǎng)景,我們呀雇锡,也別想那么復(fù)雜逛钻,就以最簡(jiǎn)單的還是最經(jīng)典的計(jì)數(shù)器開始,如圖:

看圖锰提,大家就應(yīng)該很清楚做什么曙痘,當(dāng)我點(diǎn)擊加號(hào)按鈕,希望倉(cāng)庫(kù)里的數(shù)據(jù)能增 1立肘,點(diǎn)擊減號(hào)按鈕边坤,希望倉(cāng)庫(kù)里的數(shù)據(jù)能減 1 ,嗯谅年,很明確的需求茧痒,我們就來看看怎么實(shí)現(xiàn)吧~

具體實(shí)現(xiàn)

  • redux創(chuàng)建

首先我們需要在src目錄下創(chuàng)建一個(gè)redux目錄,用來放我們redux的核心代碼踢故,其中最核心的代碼就是 createStore 方法了文黎,因?yàn)槲覀冊(cè)诔跏蓟?store 的時(shí)候就是通過這個(gè)方法進(jìn)行創(chuàng)建的惹苗,redux源碼里它會(huì)返回 dispatch殿较、subscribegetState桩蓉、replaceReducer 四個(gè)方法淋纲,但是實(shí)際上我們通常用到的就是前三個(gè)了~

ok,我們接下來慢慢的來實(shí)現(xiàn)~

redux目錄下創(chuàng)建一個(gè)index.js院究,代碼如下:

// index.js

function createStore () {
    let state ;
    
    //分發(fā)action
    let dispatch = ()=>{
    
    }
    
    //訂閱數(shù)據(jù)更新
    let subscribe = ()=> {
    
    }
    
    //獲取store的狀態(tài)
    let getState = ()=>{
    
    }
    
    return{
        dispatch ,
        subscribe ,
        getState
    }

}

export {
    createStore
}

這樣我們就有了一個(gè) redux 雛形洽瞬,接下來主要是完善里面的方法而已~

  • 初始化store

我們先把這個(gè) store 掛載到 vue 的原型上,方便后面的使用业汰,我們?cè)?code>src創(chuàng)建一個(gè)store文件夾伙窃,并新建一個(gè)index.js,代碼如下:

import { createStore } from "../redux"

const store = createStore();

export default store;
  • 掛載store

我們?cè)谌肟谖募?main.js 引入样漆,并掛載即可:

import store from './store'

Vue.prototype.$store = store;

接著为障,如果在頁(yè)面中打印 this.$store,出現(xiàn)如下結(jié)果放祟,則證明呢已經(jīng)掛載成功了~

但是此時(shí)鳍怨,并沒有什么實(shí)際的功能,因?yàn)槲覀儾]有完善我們的 createStore 里方法~

  • 完善createStore

使用過 redux 的同學(xué)都知道跪妥,我們?cè)诔跏蓟?store , 也就是調(diào)用 createStore 的時(shí)候是需要傳遞一個(gè)reducers 參數(shù)的鞋喇,用來寫入更改狀態(tài)的處理邏輯,我們這里以計(jì)數(shù)器為例眉撵,將計(jì)數(shù)器 的 reducer 作為入?yún)

這里我將計(jì)數(shù)器相關(guān)actions侦香、constants落塑、reducer 直接貼代碼了,就不在贅述了~

constants counter.js

    // 常量定義
    export const ADD = 'add';
    export const MINUS = 'minus'

actions counter.js

import * as constants from '../constants/counter'

//增加
export const add = ()=>({
    type : constants.ADD
})

//減少
export const minus = ()=>({
    type : constants.MINUS
})

reducers counter.js

import * as constants from '../constants/counter'

const defaultState = {
    count: 0
}

export default (state = defaultState, action) => {
    switch ( action.type ) {
        case constants.ADD :
            return {
                ...state ,
                count : state.count + 1
            }
        case constants.MINUS :
            return {
                ...state ,
                count : state.count - 1
            }
        default :
            return  state;
    }
}

dispatch

我們期望鄙皇,我們點(diǎn)擊加號(hào)的時(shí)候芜赌,會(huì)派發(fā)一個(gè)addaction,然后對(duì)應(yīng)的 reducer 接收到 action 做對(duì)應(yīng)的處理伴逸,減號(hào)按鈕同理~

順著這個(gè)思路缠沈,我們先來看看 dispatch 方法,需要接受一個(gè)參數(shù) action 错蝴,action 必須是一個(gè)對(duì)象洲愤,必須包含一個(gè) type 值,表明需要 action的類型顷锰,然后我們可以通過傳過來的 recuder 獲取到新的狀態(tài)柬赐,因此,dispatch 方法如下:

//分發(fā)action
let dispatch = (action)=>{
    //判斷 action 的 type 值
    if( typeof action !== 'object') throw Error('Expected the action to be a object.');
    if( action.type === undefined ) throw Error('The action.type is not defined');
    //獲取新的 state
    state = reducer(state, action);
}

我們打印 state 官紫, 通過點(diǎn)擊 加號(hào) 和 減號(hào) 按鈕肛宋,可以看到 state 確實(shí)有變換,跟我們預(yù)期的一樣~

getState

我們可以通過 getState() 方法獲取當(dāng)前 state 束世,因此該方法相對(duì)比較簡(jiǎn)單酝陈,如下~

//獲取store的狀態(tài)
let getState = () => state;

這樣我們?cè)诿看闻砂l(fā) action 的后,通過 getState 方法確實(shí)能獲取到對(duì)應(yīng)的狀態(tài)~

但其實(shí)有個(gè)問題毁涉,我們初始獲取 state 的時(shí)候會(huì)返回 undefined 沉帮,因?yàn)槲覀兂跏嫉臅r(shí)候只是定義了 state , 卻沒有賦值~

為了解決這個(gè)問題贫堰,我們可以在 初始化的時(shí)候 dispatch 一個(gè) initaction 穆壕,這樣會(huì)返回 reducer 中默認(rèn)的 state ,即:

    //初始化state
    dispatch({ type : '@@redux/INIT'});
    
    //獲取store的狀態(tài)
    let getState = () => state;

subscribe

雖然我們現(xiàn)在通過按鈕其屏,在控制臺(tái)打印出每次 count的變化喇勋,但是并沒有反饋到頁(yè)面上,因?yàn)轫?yè)面上我們的值并沒有做更改~

為了監(jiān)控?cái)?shù)據(jù)變化后做對(duì)應(yīng)的處理偎行,redux 提供了一個(gè)叫 subscribe川背,它的入?yún)⑹且粋€(gè)函數(shù),作用就是訂閱數(shù)據(jù)變化睦优,做對(duì)應(yīng)的邏輯處理渗常,返回一個(gè)函數(shù),用來取消訂閱汗盘,因此我們可以對(duì) subscribe 函數(shù)做如下處理:

//訂閱處理函數(shù)
let listeners = [];

//訂閱數(shù)據(jù)更新
let subscribe = (fn)=> {
    listeners.push(fn);
    return ()=>{
        listeners = listeners.filter(listener => fn != listener );
    }
}

注意皱碘,上面只是訂閱,當(dāng)我們數(shù)據(jù)變化的時(shí)候需要發(fā)布隐孽,即要循環(huán) listeners 中的方法癌椿,依次執(zhí)行健蕊,因此需要在 dispatch 方法的最后加上一句:

listeners.forEach(listener => listener());

效果

接著我們來試試~

代碼如下:

<template>
    <div class='wrapper'>
        <div class="">計(jì)數(shù)器:{{ number }}</div>
        <div class="btn-box">
            <button class="btn" @click="handleAddBtnClick">+</button>
            <button class="btn" @click="handleMinusBtnClick">-</button>
        </div>
        <div class="btn-box">
            <button class="btn" @click="handleRemoveListenerBtnClick">取消打印監(jiān)聽</button>
        </div>
    </div>
</template>

<script>
    import { add , minus } from '../actions/counter'
    export default {
        data(){
            return{
                number : this.$store.getState().count ,
                consoleHandler : null
            }
        },
        methods : {
            //計(jì)數(shù)器加一
            handleAddBtnClick(){
                this.$store.dispatch(add())
            },
            //計(jì)數(shù)器減一
            handleMinusBtnClick(){
                this.$store.dispatch(minus())
            },
            //取消打印事件監(jiān)聽
            handleRemoveListenerBtnClick(){
                this.consoleHandler();
            }
        },
        mounted () {
            //數(shù)據(jù)更新事件
            this.$store.subscribe(()=>{
                this.number = this.$store.getState().count;
            })
            //打印事件
            this.consoleHandler = this.$store.subscribe(()=>{
                console.log('我的值發(fā)生了更改~')
            })
        }
    }
</script>

<style scoped>
    .wrapper{
        padding: 30px 15px;
        text-align: center;
    }
    .btn-box{
        margin-top: 20px;
    }
    .btn{
        display: inline-block;
        font-size: 20px;
        min-width: 50px;
        text-align: center;
        margin: 0 10px;
    }
</style>

我們?cè)诔跏嫉臅r(shí)候 添加了兩個(gè) 事件監(jiān)聽,一個(gè)用來更新 number的值踢俄,另一個(gè)用來打印缩功,我們每次數(shù)據(jù)變更都會(huì)觸發(fā)這兩個(gè)方法,當(dāng)我們點(diǎn)擊取消打印監(jiān)聽按鈕的時(shí)候都办,之后的數(shù)據(jù)變化不會(huì)在觸發(fā)打印操作~

自己動(dòng)手試試~

combineReducers

到此嫡锌,我們已經(jīng)基本實(shí)現(xiàn)了一個(gè) 簡(jiǎn)單版 的 redux ,但是還不夠完美琳钉,問題在于我們還差一個(gè)函數(shù)势木,叫 combineReducers , 它是用來合并多個(gè) reducer 返回一個(gè)新的 reducer歌懒,以此區(qū)分不同的狀態(tài)~

我們?cè)趧偛诺?redux 文件夾下的 index.js 添加這個(gè)方法啦桌,如下:

// 合并reducer
// key是新狀態(tài)的命名空間 值是reducer,執(zhí)行后會(huì)返回一個(gè)新的reducer
function combineReducers (reducers) {
    // 第二次調(diào)用reducer 及皂,內(nèi)部會(huì)自動(dòng)的把第一次的狀態(tài)傳遞給reducer
    return (state = {}, action) => {
        // reducer默認(rèn)要返回一個(gè)狀態(tài)
        let newState = {}
        for (let key in reducers) {
            // 默認(rèn)reducer倆參數(shù) 一個(gè)叫state甫男,一個(gè)叫action
            let s = reducers[key](state[key], action);
            newState[key] = s;
        }
        return newState;
    }
}

接著,將我們的reducer更換一下即可验烧,這里就不在贅述了~

結(jié)語(yǔ)

通過上面的學(xué)習(xí)板驳,我們手動(dòng)實(shí)現(xiàn)了一個(gè)redux,雖然比較簡(jiǎn)單噪窘,核心代碼很少笋庄,但起碼功能是完善的效扫,主要是學(xué)習(xí)一種編碼的思想吧倔监,我們可以在學(xué)習(xí)一些框架或者庫(kù)的同時(shí)可以多去關(guān)注,它的內(nèi)部實(shí)現(xiàn)菌仁,然后我們開始可以自己一步一步模仿一個(gè)簡(jiǎn)單的版本浩习,后面在不停的拓展,希望和大家一起繼續(xù)加油~

代碼我也上傳到github上了济丘,有需要的小伙伴可以參考參考~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谱秽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子摹迷,更是在濱河造成了極大的恐慌疟赊,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件峡碉,死亡現(xiàn)場(chǎng)離奇詭異近哟,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鲫寄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門吉执,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疯淫,“玉大人,你說我怎么就攤上這事戳玫∥醪簦” “怎么了?”我有些...
    開封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵咕宿,是天一觀的道長(zhǎng)币绩。 經(jīng)常有香客問我,道長(zhǎng)府阀,這世上最難降的妖魔是什么类浪? 我笑而不...
    開封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮肌似,結(jié)果婚禮上费就,老公的妹妹穿的比我還像新娘。我一直安慰自己川队,他們只是感情好力细,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著固额,像睡著了一般眠蚂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上斗躏,一...
    開封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天逝慧,我揣著相機(jī)與錄音,去河邊找鬼啄糙。 笑死笛臣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的隧饼。 我是一名探鬼主播沈堡,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼燕雁!你這毒婦竟也來了诞丽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤拐格,失蹤者是張志新(化名)和其女友劉穎僧免,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捏浊,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡懂衩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勃痴。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谒所,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出沛申,到底是詐尸還是另有隱情劣领,我是刑警寧澤,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布铁材,位于F島的核電站尖淘,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏著觉。R本人自食惡果不足惜村生,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饼丘。 院中可真熱鬧趁桃,春花似錦、人聲如沸肄鸽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)典徘。三九已至蟀苛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間逮诲,已是汗流浹背帜平。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留梅鹦,地道東北人裆甩。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像帘瞭,于是被迫代替她去往敵國(guó)和親淑掌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蒿讥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容