想要更好的使用一個插件,可以嘗試理解其實現的方式。
當然,瞭解一個優秀的插件,本身也會增強自己的能力。
本文,努力從零開始實現一個簡易版的vuex
,期間會用到很多編程思想,希望自己越來越靈活使用。
TL;DR
state
是響應式的,巧用Vuegetters
用Object.defineProperty
實現和state的緊密相關mutations
是定義commit
方法,commit
的this需要固定為store實例actions
是定義dispatch
,邏輯神似mutations
- 翻到文末可以直接看
vuex.js
的簡易版代碼
vuex 的初版樣子
先可以用vue create xx
創建一個項目,不帶vuex
的。
先看看,如果有vuex
插件的main.js
。
!!!特別注意
{state:{},mutations:...}
,是用户傳的參數- store 雖然可以
this.$store.state
,但這個 state 不完全是用户傳的 state,而是處理過的 state,這兩有本質區別 - 同樣,用户傳過來的其他屬性,也會做處理,這樣才有後期的
this.$store.getters.xx
等等 - 換言之,
store
就是對用户傳的參數做各種處理,以方便用户操作她的數據。
從這推理出vuex
,應該具有的特徵:
Vue.use
表明,vuex 肯定有install
方法new Vuex.Store
表明,vuex 導出對象裏,有個Store
的類- 每個組件內部都可以
this.$store
表明,需要注入$store
如果對插件一臉懵的話,可以簡單看下vue 插件的入門説明
第一版vuex.js
就出來了:
但這樣,$store
和store實例
並沒有掛鈎,此時可以藉助Vue.mixins的beforeCreate鈎子
拿到當前的 Vue 實例,從而拿到實例的$options
。
export default {
install(Vue) {
Vue.mixin({
beforeCreate() {
// 這裏的this是vue的實例,其參數store就是store實例
(!Vue.prototype.$store) && (Vue.prototype.$store = this.$options.store;)
}
});
},
Store
};
複製代碼
改進:不要輕易在原型上面添加屬性,應該只在根實例有store
的時候才設置$store
,子實例會拿到根實例的$store
github源碼 切換到c1
分支
處理用户傳的 state
store 實例的state
可以出現在視圖裏,值變化的時候,視圖也一併更新。
所以,state
是被劫持的,這裏投機取巧的用下Vue
。
// vuex.js
class Store {
constructor(options) {
this.options = options;
this.state = new Vue({ data: options.state });
}
}
複製代碼
<!-- App.vue -->
<div id="app">
{{ $store.state.a }}
<button @click="$store.state.a++">
增加
</button>
</div>
複製代碼
github源碼 切換到c2
分支
!!!因為state是用Vue進行響應式,所有vuex重度依賴vue,不能脱離vue使用
處理用户傳的 getters
- 用户傳的
getters
是一個函數集合 - 但是實際使用中,屬性值是函數的返回值
- 屬性依舊是劫持的,這邊因為是函數,所以不能再投機取巧了
// vuex.js
constructor(options) {
this.options = options;
this.state = new Vue({ data: options.state });
if (options.getters) {
this.getters = {};
Object.keys(options.getters).forEach(key => {
// 這裏必須是屬性劫持
Object.defineProperty(this.getters, key, {
get: () => {
return options.getters[key](this.state);
}
});
});
}
}
複製代碼
// main.js
state: { a: 1, b: 2 },
getters: { a1(state) { return state.a + 1; } }
複製代碼
<!-- app.vue -->
<div id="app">
{{ $store.state.a }} {{ $store.getters.a1 }}
<button @click="$store.state.a++">
增加
</button>
</div>
複製代碼
github源碼 切換到c3
分支
處理 mutations
mutations
,傳的參數是一個函數集合的對象,使用的時候commit('函數名',payload)
代碼翻譯:
mutations:{
addA(state,payload){state.a+=payload}
}
// 使用的時候
this.$store.commit('addA',2)
複製代碼
由此推理出,vuex 其實寫了一個commit
方法。這個就很簡單了,直接溜上來。
// vuex.js
class Store {
constructor(options) {
// ...
if (options.mutations) {
this.mutations = { ...options.mutations };
}
}
commit(mutationName, ...payload) {
console.log(mutationName, ...payload);
this.mutations[mutationName](this.state, ...payload);
}
}
複製代碼
// <button @click="$store.commit('addA', 2)"> 增加 </button>
const store = new Vuex.Store({
state: { a: 1, b: 2 },
getters: {
a1(state) {
return state.a + 1;
},
},
mutations: {
addA(state, num) {
state.a += num;
},
},
});
複製代碼
github源碼 切換到c4
分支
處理 actions
actions
和mutations
是很相似的。
actions:{
// 注意!!!,這裏的第一個參數是store實例
addA({commit},payload){setTimeout(()=>{commit('addA',payload)},1000)}
}
// 使用的時候
this.$store.dispatch('addA',100)
複製代碼
這下更容易了,直接copy
commit(mutationName, ...payload) {
this.mutations[mutationName](this.state, ...payload);
}
dispatch(actionName, ...payload) {
// 注意這裏是this,不是this.state
this.actions[actionName](this, ...payload);
}
複製代碼
// <button @click="$store.dispatch('addA', 2)"> 1s後增加100 </button>
const store = new Vuex.Store({
// ...
actions: {
addA(store, num) {
setTimeout(() => {
store.commit("addA", num);
}, 1000);
}
},
});
複製代碼
github源碼 切換到c5
分支
優化
- commit做處理的時候,最好用下切片思維,這樣方便修改邏輯
- commit裏面的
this
,最好固定執行store實例,因為這樣在action那邊的時候,可以直接解構賦值 - action也一樣
// vuex.js
constructor(options){
// ...
if (options.mutations) {
this.mutations = {};
Object.keys(options.mutations).forEach(mutationName => {
// 切片思維,這裏上下都可以加邏輯
this.mutations[mutationName] = (...payload) => {
options.mutations[mutationName](...payload);
};
});
}
}
// 將this始終執行store實例
commit = (mutationName, ...payload) => {
this.mutations[mutationName](this.state, ...payload);
};
複製代碼
action操作一樣,不在贅述代碼。
actions: {
addA({commit}, num) {
// 這裏可以解構了!!!
setTimeout(() => {
commit("addA", num);
}, 1000);
}
},
複製代碼
github源碼 切換到c6
分支。
還有模塊空間的內容,考慮到篇幅較長,就不在本文繼續了。
附註:vuex.js的所有代碼
let Vue;
class Store {
constructor(options) {
this.options = options;
this.state = new Vue({ data: options.state });
if (options.getters) {
this.getters = {};
Object.keys(options.getters).forEach(key => {
Object.defineProperty(this.getters, key, {
get: () => {
return options.getters[key](this.state);
}
});
});
}
if (options.mutations) {
this.mutations = {};
Object.keys(options.mutations).forEach(mutationName => {
this.mutations[mutationName] = (...payload) => {
options.mutations[mutationName](...payload);
};
});
}
if (options.actions) {
this.actions = {};
Object.keys(options.actions).forEach(actionName => {
this.actions[actionName] = (...payload) => {
options.actions[actionName](...payload);
};
});
}
}
commit = (mutationName, ...payload) => {
this.mutations[mutationName](this.state, ...payload);
};
dispatch = (actionName, ...payload) => {
this.actions[actionName](this, ...payload);
};
}
export default {
install(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
// 這裏的this是vue的實例,其參數store就是store實例
const hasStore = this.$options.store;
// 根實例的store
hasStore
? (this.$store = this.$options.store)
: this.$parent && (this.$store = this.$parent.$store);
}
});
},
Store
};
複製代碼