vuex使用
首先知道vuex的基本用法,才能具体实现原理。
1.创建项目
基于vue-cli 创建一个项目:
vue create vuex-project
打开项目,打开store/index.js
:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})
实现
可以看出来,Vuex需要实现两个功能,一个是 install
方法,以便支持use
,还有一个是 Store
类:
vuex/index.js
// 导出 install中传入的当前Vue,后续在使用Vue的时候就是直接使用的当前初始化时候传入的Vue
// export 输出的接口与绑定的值是动态绑定关系,所以后续取到的值 Vue是实时的值,这里最后取到的就是 install 中传入的 _Vue
// 更多参考 阮一峰的ES6:https://es6.ruanyifeng.com/#docs/module
export let Vue
function install (_Vue) {
Vue = _Vue
}
class Store {
constructor (options) {
console.log(options)
}
}
export default {
install,
Store
}
至此,vuex的基础壳子就出来了。不过后续为了维护方便,下面会将 install
方法 和 Store
类单独写在两个文件里。
在使用vuex的时候,还有一步,就是在main.js
中挂载到Vue实例上:
import store from './store'
new Vue({
store,
render: h => h(App)
}).$mount('#app')
然后在每个Vue组件实例上都可以通过this.$store
拿到这个store
。所以这个store
需要挂载到全局。
首先想到的是通过原型prototype
来挂载,但是这样会导致一个问题,假如我们的需求需要多个new Vue
,在其中一个new Vue
的时候添加store,另一个不添加 store:
let vm1 = new Vue({
store,
render: h => h(App)
}).$mount('#app')
let vm2 = new Vue({ // 这个不需要挂载 store
render: h => h(App)
}).$mount('#app')
但是通过prototype
方式挂载store,会导致每个Vue实例上都会有 store,不满足我们的需求。Vue的做法是通过Vue.mixin
,给每个组件挂载一个beforeCreate
钩子,然后拿到 store 挂载到自己身上。
install 方法
// 导出 install中传入的当前Vue,后续在使用Vue的时候就是直接使用的当前初始化时候传入的Vue
// export 输出的接口与绑定的值是动态绑定关系,所以后续取到的值 Vue是实时的值,这里最后取到的就是 install 中传入的 _Vue
// 更多参考 阮一峰的ES6:https://es6.ruanyifeng.com/#docs/module
export let Vue
function install (_Vue) {
Vue = _Vue
Vue.mixin({
// 这里通过 mixin 添加的这个钩子,会通过 mergeOptions 合并给每个组件实例,所以每个组件实例都会调用这个钩子
beforeCreate () { // 这里的this 是每个组件实例
const options = this.$options
// 获取根组件上的实例,共享给每个组件
if (options.store) {
this.$store = options.store
} else { // 如果没有 store,说明 new Vue的时候没有进行挂载,或者 是子组件调用,那就获取 父组件的 $store
if (this.$parent && this.$parent.$store) {
// 创建的时候 顺序 是 create根组件 -> 然后 create父组件 -> create 子组件 -> create 孙子 .....,所以当子组件执行这里时候,根组件的 $store 已经被挂到 子组件的父组件上了
this.$store = this.$parent.$store
}
}
}
})
}
export default install
store.js
class Store {
constructor (options) {
console.log(options)
const { state, mutations, actions, getters } = options
this.state = state
}
}
export default Store
这时候 在 App.vue
中通过 $store.state
就可以拿到值了。
不过此时的state数据无法响应式。在App.vue
中添加一个测试button:
<div id="app">
姓名:{{$store.state.name}}
年龄:{{$store.state.age}}
<!-- getters:{{$store.getters.myAge}} -->
<!-- 正常开发是不允许这样修改状态的,这里仅仅为了测试 -->
<button @click="$store.state.age++">更改年龄</button>
</div>
点击发现数据没有任何改变。官方做法是通过借助Vue来直接实现响应式(侧面也说明了vuex必须是和Vue搭配使用,不想redux不依赖react)。
store.js
import { Vue } from './install'
class Store {
constructor (options) {
console.log(options)
const { state, mutations, actions, getters } = options
this._vm = new Vue({
data: { // $ 开头的属性不会被挂载到vm实例上,会被挂载到_data 上 。所以直接通过_vm.$$state是取不到的
$$state: state
}
})
}
// 属性访问器
get state () {
return this._vm._data.$$state
}
}
export default Store
借助 vue的响应式,此时再点按钮就可以实时更新数据了。
实现getters
使用官方版的Vuex,修改App.vue
:
<div id="app">
姓名:{{$store.state.name}}
年龄:{{$store.state.age}}
getters:{{$store.getters.myAge}}
getters:{{$store.getters.myAge}}
getters:{{$store.getters.myAge}}
<!-- 正常开发是不允许这样修改状态的,这里仅仅为了测试 -->
<button @click="$store.state.age++">更改年龄</button>
</div>
在store/index.js
中打印:
getters: {
myAge (state) {
console.log('getters')
return state.age + 10
}
},
发现不管调用几次getters,只会打印一次,感觉就像我们的计算属性。其实getters内部就是基于计算属性来实现的。
在store.js
中添加下面逻辑:
const forEach = function (obj, fn) {
Object.keys(obj).forEach((key) => {
fn(obj[key], key)
})
}
class Store {
constructor (options) {
console.log(options)
const { state, mutations, actions, getters } = options
this.getters = {}
const computed = {}
forEach(getters, (fn, key) => {
computed[key] = () => {
return fn(this.state)
}
// 每次取getter值的时候,借助_vm.computed 来取值
Object.defineProperty(this.getters, key, {
get: () => this._vm[key]
})
})
this._vm = new Vue({
data: { // $ 开头的属性不会被挂载到vm实例上,会被挂载到_data 上 。所以直接通过_vm.$$state是取不到的
$$state: state
},
computed
})
}
实现mutations
mutations 就是发布订阅的思想。初始化的时候,将用户传入的 mutations 收集起来,等到调用commit
函数的时候,触发收集的所有符合的mutations
。
store.js
添加下面代码:
this.mutations = {}
forEach(mutations, (fn, key) => {
this.mutations[key] = (payload) => fn.call(this, this.state, payload)
})
commit (type, payload) {
this.mutations[type](payload)
}
实现 actions
基本思路和mutations 一样。
主要代码如下:
然后在外面使用:
<button @click="$store.dispatch('asyncChangeName','newName')">异步修改名字</button>
actions: {
asyncChangeName ({ commit }, val) {
setTimeout(() => {
commit('changeName', 'asyncName')
}, 1000)
}
},
至此 一个基础版的vuex就实现了,后面会继续完成module等功能。
扩展
export
export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代码输出变量foo,值为bar,500 毫秒之后变成baz。
这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新。
摘自 : 阮一峰老师的ES6 : https://es6.ruanyifeng.com/#docs/module