在阅读Vuex源码之前,因为Vuex的api和使用功能略微复杂,默认以为实现起来相当复杂,望而生畏。然而通过深入学习源码,发现核心功能结合vue实现起来非常巧妙,也就核心几行代码,直呼内行。本文也就100左右行代码就能快速手写自己的Vuex代码!
前言
Vuex 是⼀个专为 Vue.js应⽤程序开发的状态管理模式。它采⽤集中式存储管理应⽤的所有组件的状态,并以相应的规则保证状态以⼀种可预测的⽅式发⽣变化。那么Vuex和单纯的全局对象有什么不同呢?
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发⽣变 化,那么相应的组件也会相应地得到⾼效更新。不能直接改变 store 中的状态。改变 store 中的状态的唯⼀途径就是显式地提交 (commit) mutation。这样使得我们可以⽅便地跟踪每⼀个状态的变化,从⽽让我们能够实现⼀些⼯具帮助我 们更好地了解我们的应⽤。
通过以上两点认知我们来快速实现自己的Vuex!
Vuex 初始化
为什么在vue实例化的时候要传入store
去实例化呢?那是为了让vue
所有的组件中可以通过 this.$store
来获取该对象,即 this.$store
指向 store
实例。
// Store 待实现const store = new Store({ state: { count: 0, num: 10})new Vue({ el: '#app', store: store // 此处的 store 为 this.$options.store})
Vuex
提供了install
属性,通过Vue.use(Vuex)
来注册。
const install = function (Vue) { Vue.mixin({ beforeCreate() { if (this.$options.store) { Vue.prototype.$store = this.$options.store } } }) }
Vue全局混⼊了⼀个 beforeCreated
钩⼦函数, options.store
保存在所有组件的 this.$store
中,这个 options.store
就是我们在实例化 Store
对象的实例。Store 对象的构造函数接收⼀个对象参数,它包含 actions
、 getters
、 state
、 mutations
等核⼼概念,接下来我们一一实现。
Vuex state
其实 state 是 vue 实例中的 data ,通过 Store 内部创建Vue实例,将 state 存储到 data 里,然后改变 state 就是触发了 data 数据的改变从而实现了视图的更新。
// 实例化 Storeconst store = new Store({ state: { count: 0, num: 10 } })// Store 实现class Store { constructor({state = {}}) { this.vm = new Vue({ data: {state} // state 添加到 data 中 }) } get state() { return this.vm.state // 将 state代理到 vue 实例中的 state } set state(v) { console.warn(`Use store.replaceState() to explicit replace store state.`) } }
由上可知,store.state.count
等价于 store.vm.state
。不论是获取或者改变state里面的数据都是间接的触发了vue中data数据的变化,从而触发视图更新。
Vuex getters
知道state
是vue
实例中的data
,那么同理,getters 就是 vue中的计算属性 computed。
// 实例化 Storeconst store = new Store({ state: { count: 0, num: 10 }, getters: { total: state => { return state.num + state.count } }, })// Store 实现class Store { constructor({state = {}, getters = {}}) { this.getters = getters // 创建模拟 computed 对象 const computed = {} Object.keys(getters).forEach(key => { const fn = getters[key] // 入参 state 和 getters computed[key] = () => fn(this.state, this.getters) // 代理 getters 到 vm 实例上 Object.defineProperty(this.getters, key, { get: () => this.vm[key] }) }) // 赋值到 vue 中的 computed 计算属性中 this.vm = new Vue({ data: { state, }, computed, }) } get state() { return this.vm.state } set state(v) { console.warn(`Use store.replaceState() to explicit replace store state.`) } }
使用 Object.defineProperty
将getters上的所有属性都代理到了vm实例上的computed计算属性中,也就是 store.getters.count
等价于 store.vm.count
。
Vuex mutations
mutations等同于发布订阅模式,先在mutations中订阅事件,然后再commit发布事件。
// 实例化 Storeconst store = new Store({ state: { count: 0, num: 10 }, mutations: { INCREASE: (state, n) =>{ state.count += n }, DECREASE: (state, n) =>{ state.count -= n } } })// Store 实现class Store { constructor({state = {}, mutations = {}, strict = false}) { this.mutations = mutations // 严格摸索只能通过 commit 改变 state this.strict && this.enableStrictMode() } commit(key, payload) { // 获取事件 const fn = this.mutations[key] // 开始 commit this.committing = true // 执行事件 并传参 fn(this.state, payload) // 结束 commit 所以说明 commit 只能执行同步事件 this.committing = false } enableStrictMode () { // vm实例观察 state 是否由 commit 触发改变 this.vm.$watch('state', () => { !this.committing && console.warn(`Do not mutate vuex store state outside mutation handlers.`) }, { deep: true, sync: true }) } get state() { return this.vm.state } set state(v) { console.warn(`Use store.replaceState() to explicit replace store state.`) } }
store.commit
执行 mutations
中的事件,通过发布订阅实现起来并不难。 Vuex
中的严格模式,只能在commit
的时候改变state
数据,不然提示错误。
Vuex actions
mutations
用于同步更新 state
,而 actions
则是提交 mutations
,并可进行异步操作,从而间接更新 state
。
// 实例化 Storeconst store = new Store({ actions: { getToal({dispatch, commit, state, getters}, n){ return new Promise(resolve => { setTimeout(() => { commit('DECREASE', n) resolve(getters.total) }, 1000) }) } } })// Store 实现class Store { constructor({actions = {}}) { this.actions = actions } dispatch(key, payload) { const fn = this.actions[key] const {state, getters, commit, dispatch} = this // 注意 this 指向 const result = fn({state, getters, commit: commit.bind(this), dispatch: dispatch.bind(this)}, payload) // 返回 promise return this.isPromise(result) ? result : Promise.resolve(result) } // 判断是否是 promise isPromise (val) { return val && typeof val.then === 'function' } }
mutations
和 actions
的实现大同小异,actions
核心在于处理异步逻辑,并返回一个 promise
。
完整案例代码
这边把以上的代码统一归纳起来,可以根据这份完整代码来分析Vuex
逻辑。
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script></head><body> <div id="app"> <child-a></child-a> <child-b></child-b> </div> <script> class Store { constructor({state = {}, getters = {}, mutations = {}, actions = {}, strict = false}) { this.strict = strict this.getters = getters this.mutations = mutations this.actions = actions this.committing = false this.init(state, getters) } get state() { return this.vm.state } set state(v) { console.warn(`Use store.replaceState() to explicit replace store state.`) } init (state, getters) { const computed = {} Object.keys(getters).forEach(key => { const fn = getters[key] computed[key] = () => fn(this.state, this.getters) Object.defineProperty(this.getters, key, { get: () => this.vm[key] }) }) this.vm = new Vue({ data: {state}, computed, }) this.strict && this.enableStrictMode() } commit(key, payload) { const fn = this.mutations[key] this.committing = true fn(this.state, payload) this.committing = false } dispatch(key, payload) { const fn = this.actions[key] const {state, getters, commit, dispatch} = this const res = fn({state, getters, commit: commit.bind(this), dispatch: dispatch.bind(this)}, payload) return this.isPromise(res) ? res : Promise.resolve(res) } isPromise (val) { return val && typeof val.then === 'function' } enableStrictMode () { this.vm.$watch('state', () => { !this.committing && console.warn(`Do not mutate vuex store state outside mutation handlers.`) }, { deep: true, sync: true }) } } const install = function () { Vue.mixin({ beforeCreate() { if (this.$options.store) { Vue.prototype.$store = this.$options.store } } }) } // 子组件 a const childA = { template: '<div><button @click="handleClick">click me</button> <button @click="handleIncrease">increase num</button> <button @click="handleDecrease">decrease num</button></div>', methods: { handleClick() { this.$store.state.count += 1 }, handleIncrease() { this.$store.commit('INCREASE', 5) }, handleDecrease() { this.$store.dispatch('getToal', 5).then(data => { console.log('total', data) }) } } } // 子组件 b const childB = { template: '<div><h1>count: {{ count }}</h1><h1>total: {{ total }}</h1></div>', mounted() { // 严格模式下修改state的值将警告 // this.$store.state.count = 1 }, computed: { count() { return this.$store.state.count }, total(){ return this.$store.getters.total } } } const store = new Store({ state: { count: 0, num: 10 }, getters: { total: state => { return state.num + state.count } }, mutations: { INCREASE: (state, n) =>{ state.count += n }, DECREASE: (state, n) =>{ state.count -= n } }, actions: { getToal({dispatch, commit, state, getters}, n){ return new Promise(resolve => { setTimeout(() => { commit('DECREASE', n) resolve(getters.total) }, 1000) }) } } }) Vue.use({install}) new Vue({ el: '#app', components: { 'child-a': childA, 'child-b': childB }, store: store }) </script> </body></html>
总结
通过上面的完整案例可知,Vuex
核心代码也就100
行左右,但是他巧妙的结合了vue
的data
和computeds
属性,化繁为简,实现了复杂的功能,所以说vuex
是不能脱离vue
而独立运行的。
本文是结合官网源码提取核心思想手写自己的Vuex
,而官网的Vuex
,为了避免store
结构臃肿,还实现了modules
等功能,具体实现可以查看Vuex官网源码。