10 | 【阅读Vue2源码】Vuex的实现原理
前言
本篇文章分析Vue2的生态库Vuex,本次选择Vuex的版本为3.x的最新版本3.6.2。
为什么选择3.6.2的版本呢?是因为我接触的大多数项目都是三四年前创建的项目,甚至更久远,大多数Vuex的版本都是3.x的,而4.x的Vuex增加了对vue3的支持,实现方式上有一些变化,所以选择3.x版本最新的3.6.2版本作为分析对象。
我们都知道Vuex是Vue的状态管理库,并且更改store中的state就可以更新视图,那么Vuex是怎么做的state更新就更新视图呢?本篇文章就来研究这个主题。
相关链接:
搭建阅读环境
- 在github上folk一个vuex的仓库
- 拉取自己folk的仓库到电脑本地
- 基于tag v3.6.2新建一个分支,例如我的分支为
alanlee/read-source/v3.6.2
- 打开项目,安装依赖,推荐使用yarn安装依赖
- 调整rollup打包配置文件rollup.config.js,在output对象中增加
sourcemap: true
- 执行打包命令
npm run build:main
,打包好后dist文件夹就多了对应的xxx.map
的源码映射文件,方便调试 - 在examples文件中选择一个示例项目作为分析对象,这里我选择
todomvc
- 修改示例代码中store.js引入的Vuex为dist文件夹下的打包出来的vuex
- 修改webpack.config.js的配置,增加
devtool: 'eval-source-map'
,打包出源码映射文件 - 运行示例代码,执行
npm run dev
- 最后在示例代码中打断点,然后进行调试分析源码
源码分析
在进行源码分析之前,先准备一个简单的demo作为分析对象,这里选择的是源码中自带的examples的todomvc
,自己改造了一下
Demo示例代码
app.js
import Vue from 'vue'
import App from './components/SimpleApp.vue'
import Vuex from '../../../dist/vuex'
const storeConfig = {
state: {
hello: 'AlanLee'
},
mutations: {
changeHello(state, newVal) {
state.hello = newVal
}
},
actions: {
updateHello(ctx, payload) {
ctx.commit('changeHello', payload)
}
}
}
const store = new Vuex.Store(storeConfig)
Vue.use(store)
new Vue({
store, // inject store to all children
el: '#app',
render: h => h(App)
})
SimpleApp.vue
<template>
<div>
SimpleApp:<span @click="changeText">{{ hello }}</span>
</div>
</template>
<script>
export default {
name: 'SimpleApp',
computed: {
hello () {
return this.$store.state.hello
}
},
methods: {
changeText () {
// this.$store.commit('changeHello', Math.random())
this.$store.dispatch('updateHello', Math.random())
}
}
}
</script>
这段代码主要展示的是vuex的简单使用方式
- 定义Vuex的
state
、mutations
、actions
- 在vue组件的方法中调用
$store
的commit
/dispatch
派发actions更改state,触发视图更新
Vuex的核心概念
Vuex的核心概念有:
- state
- mutations
- actions
- getters
- module
其中我们主要关注state
、mutations
、actions
即可
调试源码分析
install
因为Vuex本质上是一个vue的插件,所以需要提供install方法
export function install (_Vue) {
if (Vue && _Vue === Vue) {
if (__DEV__) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}
主要实现在applyMixin,来看看其实现
applyMixin
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 兼容vue1的老代码,不是我们分析的重点,忽略代码
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
逻辑也很简单:
applyMixin
调用Vue.mixin
,把vuexInit
赋值给beforeCreate
,相当于Vue组件初始化时会先执行vuexInit
vuexInit
就是从options
中取用户配置的store,赋值给组件实例的$store
,所以我们在Vue组件实例中可以通过this.$store.state.xxx
来访问store中的数据
Store
Store的初始化是在new Vuex.Store()
时开始的,所以我们找到入口为Store的定义
/src/store.js
export class Store {
constructor (options = {}) {
// ...
const {
plugins = [],
strict = false
} = options
// 做一些初始化
// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)
// bind commit and dispatch to self
const store = this
// 包装一下dispatch, commit方法
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
// strict mode
this.strict = strict
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// 核心逻辑 实现state的响应式
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
// ...
}
get state () {
return this._vm._data.$$state
}
set state (v) {
if (__DEV__) {
assert(false, `use store.replaceState() to explicit replace store state.`)
}
}
commit (_type, _payload, _options) {
// ...
}
dispatch (_type, _payload) {
// ...
}
subscribe (fn, options) {
// ...
}
subscribeAction (fn, options) {
// ...
}
watch (getter, cb, options) {
// ...
}
replaceState (state) {
// ...
}
registerModule (path, rawModule, options = {}) {
// ...
}
unregisterModule (path) {
// ...
}
hasModule (path) {
// ...
}
hotUpdate (newOptions) {
// ...
}
_withCommit (fn) {
// ...
}
}
可以看到store中定义了十几个属性和方法,其中我们主要分析:
- constructor
- state
- commit
- dispatch
- resetStoreVM,实现state的响应式
State响应式的实现
实现细节在resetStoreVM
函数中
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// bind store public getters
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure environment.
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
Vue.config.silent = true
// 核心逻辑,new了一个Vue,只提供了data和computed,将state作为data,以实现state的响应式
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
实现逻辑也很简单,核心逻辑就是:新new了一个Vue
,只提供了data
和computed
,将state
作为data
,以实现state
的响应式。
那么问题来了,我们的主应用本身是new了一个Vue,那么用了Vuex后,store里又new了一个Vue,就算state的响应式是通过store中的新的Vue实例提供的,理论上来讲,state发生变化,应该是触发当前(store中)Vue实例中的视图更新才对呀,而且这个Vue实例并没有提供模板,也没有$mount
挂载元素。
那么Vuex又是怎么做到:store中的Vue实例的data更新,去触发我们主应用的视图更新呢?
实现视图更新
其实实现视图的更新也很简单,Store也不需要做什么特殊的处理,因为Vue已经实现了这个功能。
- 其实就是在模板中访问store时
<span>{{ $store.state.hello }}</span>
,会触发defineReactive中设置的Object.defineProperty的get
- 在get中触发dep.depend()收集依赖,收集依赖时会把当前的这个Vue实例的渲染函数作为Watcher的回调函数
- 当更新state的值时,触发set,执行depend收集的依赖(Watcher的回调函数),也就是主应用Vue实例的渲染函数,所以主应用Vue对应的视图也会更新
- 简单来讲,就是Store里的Vue收集的依赖是主应用Vue的更新函数
八卦一下:
Vuex为什么叫Vuex?难道就是因为store里面new了一个Vue,所以叫做Vue的扩展?为什么不叫Vue Store呢?
commit/dispatch
这两个API就是用来改变state的,commit是同步执行,dispatch是异步执行actions。
小总结
- Vuex是在初始化vue时,混入一个函数,给当前Vue组件实例增加一个
$store
属性,所有在vue组件中可以通过this.$store.state.xxx
访问store中的数据 - 通过new了一个Vue来实现state的响应式
- state在主应用中的模板中访问,所以state的响应式收集的依赖是主应用Vue实例的渲染函数,当state更新时,执行收集回调函数(主应用Vue实例的更新函数),所以可以更新视图
自己实现简单版Vuex
依葫芦画瓢,按照Vuex的实现方式,我们自己可以实现一个极简版的Vuex
- 定义Store类,定好框架
-
constructor
- 接收options参数
-
属性
- state
-
方法
- install
- commit
- applyMixin
- commit
- dispatch
class Store {
state = {}
constructor(options = {}) {
this.state = options.state
}
install(vm) {
this.applyMixin(vm)
}
applyMixin(Vue) {
}
commit(handler, payload) {
}
dispatch(action, payload) {
}
}
- 实现插件需要的install方法,其实就是调用applyMixin,然后实现applyMixin方法,Vue.use(Vuex)时会执行install方法
install(vm) {
this.applyMixin(vm)
}
applyMixin(Vue) {
// 缓存一份原来的_init方法
const _init = Vue.prototype._init
// 定义vuex初始化方法
function vuexInit() {
const options = this.$options
// 赋值store到$store
if(options.store) {
this.$store = options.store
} else if(options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
// 重新赋值_init
Vue.prototype._init = function(options = {}) {
// 混入vuexInit方法
Vue.mixin({ beforeCreate: vuexInit })
// 执行原来的_init方法
_init.call(this, options)
}
}
- 在constructor中实现state的响应式
class Store {
state = {}
constructor(options = {}) {
this.state = options.state
this.options = options
const store = this
// 实现state的响应式
store._vm = new Vue({
name: 'DemoVuex',
data: {
// 你只管放数据到data,剩下的交给Vue
$$state: options.state
}
})
}
// ...
}
OK,响应式实现了,很简单,直接new一个Vue就,把state放进data里就行了,你只管放数据到data,剩下的交给Vue。
- 实现commit
// commit接收两个参数,handler-定义的mutations的名字,payload-提交的数据
commit(handler, payload) {
// 取出mutations
const {mutations = {}} = this.options
// 执行取出mutations
mutations[handler].call(this, this.state, payload)
}
commit接收两个参数
- handler-定义的mutations的名字
- payload-提交的数据
- 实现dispatch
// 实现思路跟commit一样,使用Promise包裹了一下
// 接收两个参数,action-定义的action的名字,payload-提交的数据
dispatch(action, payload) {
return new Promise((resolve) => {
const {actions = {}} = this.options
resolve(actions[action].call(this, this, payload))
})
}
实现思路跟commit一样,使用Promise包裹了一下
dispatch接收两个参数
- action-定义的action的名字
- payload-提交的数据
效果演示
完整代码
index.html
<!doctype html>
<html data-framework="vue">
<head>
<meta charset="utf-8">
<title>Vue.js • Simple Demo For Vuex</title>
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<section id="app">
<h1 @click="changeHello">{{$store.state.hello}}</h1>
</section>
<script src="../../dist/vue.js"></script>
<script src="store.js"></script>
<script src="app.js"></script>
</body>
</html>
app.js
const storeConfig = {
state: {
hello: 'AlanLee'
},
mutations: {
changeHello(state, newVal) {
state.hello = newVal
}
},
actions: {
updateHello(ctx, payload) {
ctx.commit('changeHello', payload)
}
}
}
const store = new Store(storeConfig)
Vue.use(store)
var app = new Vue({
name: 'SimpleDemo_Vuex',
store,
methods: {
changeHello() {
// this.$store.commit('changeHello', Math.random())
this.$store.dispatch('updateHello', Math.random())
}
}
})
app.$mount('#app')
console.log('alan-> app', app)
window.appVue = app
store.js
class Store {
state = {}
constructor(options = {}) {
this.state = options.state
this.options = options
const store = this
// 实现state的响应式
store._vm = new Vue({
name: 'DemoVuex',
data: {
// 你只管放数据到dara,剩下的交给Vue
$$state: options.state
}
})
}
install(vm) {
this.applyMixin(vm)
}
applyMixin(Vue) {
// 缓存一份原来的_init方法
const _init = Vue.prototype._init
// 定义vuex初始化方法
function vuexInit() {
const options = this.$options
// 赋值store到$store
if(options.store) {
this.$store = options.store
} else if(options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
// 重新赋值_init
Vue.prototype._init = function(options = {}) {
// 混入vuexInit方法
Vue.mixin({ beforeCreate: vuexInit })
// 执行原来的_init方法
_init.call(this, options)
}
}
// commit接收两个参数,handler-定义的mutations的名字,payload-提交的数据
commit(handler, payload) {
// 取出mutations
const {mutations = {}} = this.options
// 执行取出mutations
mutations[handler].call(this, this.state, payload)
}
// 实现思路跟commit一样,使用用Promise包裹了一下
// 接收两个参数,action-定义的action的名字,payload-提交的数据
dispatch(action, payload) {
return new Promise((resolve) => {
const {actions = {}} = this.options
resolve(actions[action].call(this, this, payload))
})
}
}
总结
其实Vuex的实现原理很简单,Vuex的代码其实也很少。
实现原理就是给state去new了一个Vue放到data中管理。
转载自:https://juejin.cn/post/7274555781486788665