如何在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
殿较、subscribe
、getState
桩蓉、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è)add
的action
,然后對(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è) init
的 action
穆壕,這樣會(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上了济丘,有需要的小伙伴可以參考參考~