vuex源码分析
什么是vuex
是一个专为Vue.js应用程序开发的状态管理模式。
什么是状态管理模式,
vue 根据 data的变化会渲染模板,vuex则是把一些数据集中进行管理方便在vue 组件中使用。官方文档例子:
- state,驱动应用的数据源(相当于
vue的data);
- view,将 state 映射到视图(template);
- actions,在 view 上的用户输入导致的状态变化(state)。
new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `
<div>{{ count }}</div>
`,
// actions
methods: {
increment () {
this.count++
}
}
})
这个例子的数据流简图:
vuex解决的痛点是大型单页应用中状态依赖,1.嵌套组件依赖同个状态;2.兄弟组件状态依赖。比如嵌套组件依赖某个状态
state,没有vuex我们就必须不断从父组件传参到子组件,这样就很繁琐也很容易出错;兄弟组件状态依赖如果还是依照老方法就很难解决了。如果只是这么简单的单向数据流模式其实不需要用到
vuex我们可以构建一个最简单的store.一个最简单的
store模式:var store = {
debug: true,
state: {
message: 'Hello!'
},
setMessageAction (newValue) {
if (this.debug) console.log('setMessageAction triggered with', newValue)
this.state.message = newValue
},
clearMessageAction () {
if (this.debug) console.log('clearMessageAction triggered')
this.state.message = ''
}
}
//依赖store的组件,store中state的变化只能使用store.setMessageAction和store.clearMessageAction
var vmA = new Vue({
data: {
privateState: {},
sharedState: store.state
}
})
var vmB = new Vue({
data: {
privateState: {},
sharedState: store.state
}
})
但是如果遇到的是组件更多更复杂的大型应用,简单的
store模式就不适用了。2.一个最简单的vuex
vuex官方示意图:
既然要在vue组件中使用vuex,就必须初始化以及注入。
注入Vue
我们先看看如何注入Vue
注入vue实例代码:
import Vue from 'vue'
import Vuex from 'vuex'
// install Vuex框架
Vue.use(Vuex)
const store = new vuex({
xxx
})
// 创建并导出store对象。
export default store
new vue({
store
})
要实现注入首先要在
vuex实现install函数或方法,install首先要判断是否已经有vue,代码如下:
let Vue
function install(_vue) {
if(Vue &&_vue === Vue) {
return error
}
Vue = _vue
applyMixin(Vue)
}
function applyMixin(Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({beforeCreate: vuexInit})
}
function vuexInit() {
const options = this.$options
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
}
}
}
let Vue 是全局变量,如果 Vue 变量为undefined,那么局部变量_vue赋值给全局变量Vue,然后通过applyMixin将store挂载在vue实例上。ApplyMixin 函数在beforeCreate 周期检查new vue(option)的options是否有store,有就挂载在根组件上,并将其他子组件通过options.parent 建立一个根组件路径,一步步往上找到根组件store。这样就达成了vue实例只有一个store对象。页面结构图:
store流向图:
const store = new Vuex({
state:{
xxx
},
getters:{
xxxxx
},
mutations:{
xxx
},
actions:{
xxxx
}
})
vuex初始化,一般会传入下面的属性:
const store = new Vuex({
state:{
xxx
},
getters:{
xxxxx
},
mutations:{
xxx
},
actions:{
xxxx
},
modules:{
}
})
我们首先不考虑
module 模块化,只单独考虑一个最简单的store。一个最简单store
在store的构造函数会先生成_actions,_mutations,_wrappedGetters这些对象用来存储传入的actions,mutations和getters。
构造函数代码:
class store {
constructor() {
// store internal statethis._committing = false // 是否在进行提交状态标识this._actions = Object.create(null) // acitons操作对象this._mutations = Object.create(null) // mutations操作对象this._wrappedGetters = Object.create(null) // 封装后的getters集合对象this._modules = new ModuleCollection(options) // Vuex支持store分模块传入,存储分析后的modulesthis._modulesNamespaceMap = Object.create(null) // 模块命名空间mapthis._subscribers = [] // 订阅函数集合,Vuex提供了subscribe功能this._watcherVM = new Vue() // Vue组件用于watch监视变化
}
const store = this
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)
}
}
要修改state在store就只能通过
commit 和dispatch,这里不具体讲commit 和dispatch 是怎么写得,来讲讲怎么做到只通过 commit和 dispatch来对 state操作,在vue中直接对state数据操作会报错。源码中constructor 函数中定义this._committing = false 这个_committing为修改state的标志,每次修改都会触发_committing 变化。在
commit 函数中,使用_withCommiting 包裹触发的函数:commit(_type,_payload,_options) {
....
this._withCommiting(() =>{
entry.forEach( (handler) => handler(payload))
})
}
_withCommiting函数代码:_withCommiting(fn) {
const commiting = this._commiting
this._commiting = true
fn()
this._commiting = commiting
}
在
_withCommiting接受一个函数fn(实际就是commit调用的函数)为参数,每次commit ,_withCommiting中使用记录修改前状态,fn()运行完毕后,然后恢复之前的_commiting状态。每次运行
commit函数都会重复上面的步骤,必须通过 _withCommiting函数,_withCommiting 函数每次都会触发 committing 状态。但是直接修改state就不会触发
_commiting 那么如何监测有没有触发呢?这个问题先留着。到这里
store.commit 函数还未完成,回到constructor 代码可以看到commit 和dispatch 还要使用commit.call(store) 绑定到store 上,原因是什么?...
...
function constructor(){
...
...
...
const store = this
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)
}
}
看看
commit 代码:....
....
class store{
...
...
commit (_type, _payload, _options) {
....
const mutation = { type, payload }
const entry = this._mutations[type]
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
}
}
我们每次使用
commit('xxx',xxx) 形式调用时,函数其实都是从this._mutations[type] 找到对应函数,也就是说需要用到this,我么知道js中的this很灵活也非常容易丢失,如果遇到this 丢失的情况,commit 就会报错。我们看看todoMVC示例代码:
.....
addTodo ({ commit }, text) {
commit('addTodo', {
text,
done: false
})
},
commit 函数是通过解构拿到的,此时的commit的就只是个函数,而不是store.commit 。上面的解构效果相当于下面代码:
//{commit}
let commit =store.commit
如果没有在
constructor中强制绑定store那么addTodo 必然出错,无法执行下去。这也是为什么已经在class store 定义好了commit 和dispatch 但是还是要绑定的原因。模块收集
随着用到的状态越来越多vuex支持模块,来分割体积。每个模块都有
state,getters,mutations和actions。const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
在
vuex源码中构造函数会把传入的options 先传入到moduleCollection(option) 生成嵌套module,没有modules key值就是根模块,有的就是子模块。class store {
constructor(options={}) {
this._modules = new ModuleCollection(options)
}
}
moduleCollection基本逻辑:1.首先在
constructor调用register迭代注册完模块2.使用
get方法获取父模块/**
* path:Array
* rawModule:root / modules
*/
class ModuleCollection {
constructor(options = {}){
...
this.register([],options)
}
register(path,rawModule) {
const newModule = new Module(rawModule)
if(path.length === 0) {
this.root = rawModule
} else{
const parent =this.get(path.slice(0,-1))
parent.addChild(path[path.length-1],newModule)
}
if(rawModule.modules) {
forEachValue(rawModule.modules,(rawChildModule,key)=>{
this.register(path.concat(key),rawChildModule)
})
}
}
get (path) {
return path.reduce((module,key) =>{
return module.getChild(key)
},this.root)
}
}
这里很巧妙的使用递归方式模块化,逻辑上:1.判断是否是根模块,是的话立即生成模块2.不是根模块,那么就通过空数组
path 不断向 path 添加模块名,这样在添加子模块时方便找到父模块,建立正确关系图。1.首先path是用来保存
module name的数组2.在
register 函数判断是否是子模块,3.不是就 通过方法
this.register(path.concat(key),rawChildModule) 一边添加path数组一边注册子模块4.当模块化完成我们得到一个模块化的store和一个可用来寻找模块的path数组(存有所有除根模块的模块名)
然后在注册子模块通过
get来获取上一级模块也就是父模块,可以看看get是怎么实现通过path.reduce函数,将根模块(this.root)传入,迭代查询。register() {
//....
// ...else{
const parent =this.get(path.slice(0,-1))
parent.addChild(path[path.length-1],newModule)
}
}
get (path) {
return path.reduce((module,key) =>{
return module.getChild(key)
},this.root)
}
现在模块化完成我们该将所有模块,以及模块getters,actions,mutations全部注册。什么意思呢就是把所有的函数都塞进下面的存储对象,方便我们调用。
还有一个问题就是模块化之后像下面这样代码,参数
state 指向的就是moduleA中的state,但是之前的getter,action,mutation 都是指向全局的state,现在我们应该怎么办呢?const moduleA = {state: () => ({
count: 0}),
mutations: {increment (state) {// `state` is the local module state
state.count++}},
getters: {doubleCount (state) {return state.count * 2}}
}
所以现在我们要完成两件事1:注册好模块及其函数;2.传入参数state应该指向局部
注册模块
在vuex源码里
installModule 做完了这两件事。代码:
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && __DEV__) {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
store._modulesNamespaceMap[namespace] = module
}
// set state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
if (__DEV__) {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
Vue.set(parentState, moduleName, module.state)
})
}
//------分割线----------
const local = module.context = makeLocalContext(store, namespace, path)
module.forEachMutation((mutation, key) => {
...
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
...
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
....
registerGetter(store, namespacedType, getter, local)
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
上面的代码我们以分割线为界分割线上是
Vue.set(parentState, moduleName, module.state)用这个方法将store中所有state转成一个state树。
这里有一个值得注意的地方
const local = module.context = makeLocalContext(store, namespace, path) 这行代码是什么意思?从字面看是创建局部context。makeLocalContext 代码:
function installModule (store, rootState, path, module, hot) {
...
...
const local = module.context=makeLocalContext(store,namespace,path)
...
...
}
function makeLocalContext(store,namespace,path) {
const noNamespace =namespace === ''
const local = {
dispatch: noNamespace ? store.dispatch : (_type,_payload,_options) =>{
let { type, payload ,options} = unifyObjectStyle(_type, _payload,_options)
if (!options || !options.root) {
type = namespace+type
}
return store.dispatch(type,payload)
},
commit: noNamespace ? store.commit : (_type,_payload,_options) =>{
let { type, payload ,options} = unifyObjectStyle(_type, _payload,_options)
if (!options || !options.root) {
type = namespace+type
}
return store.commit(type,payload,options)
},
}
Object.defineProperties(local, {
getters:{
get:noNamespace ? () =>store.getters :() => makeLocalGetter(store,namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
可以看出
makeLocalContext 通过传入的namespace 来判断全局还是局部,创建了类似store的context,这个context有着局部模块所有的一切state,getters,mutations,actions。回到
installModule 代码,分割线下就是注册模块和模块内的getter,action,mutation 。每个
registerxxx 注册函数都将生成的local context传入其中。这样做就确保模块内的getter,action,mutation state都指向模块内的state。module.forEachMutation((mutation, key) => {
...
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
...
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
....
registerGetter(store, namespacedType, getter, local)
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
Vuex 文档示例代码:
const store = createStore({
modules: {
account: {
namespaced: true,// 局部模块
assetsstate: () => ({ ... }),
getters: {isAdmin () { ... } // -> getters['account/isAdmin']},
actions: {login () { ... } // -> dispatch('account/login')},
mutations: {login () { ... } // -> commit('account/login')},// nested mod
modules: {
// namespace继承父模块名
myPage: {state: () => ({ ... }),
getters: {profile () { ... } // -> getters['account/profile']}
},}
})
account 模块 namespaced:true 那么模块下的getter等在调用上实际为 getters['account/isAdmin']},只不过vuex源码把细节实现了不需要开发者手动调用。辅助函数
mapState和mapGetter等系列函数,为组件创建的语法糖,比如在组件可能需要将store的多个状态转成计算属性,
mapState 帮助我们生成计算属性import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
也可以使用对象展开运算符将
mapState返回的对象混入到外部对象中。computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
以
mapState为例,mapState(namespace?: string, map: Array<string> | Object<string | function>): Object第一个参数是可选的,可以是一个命名空间字符串也可以是个函数。
用法一:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}
用法二:
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}··
state和getter转化
store初始化后,其中定义
state也得是个可响应的对象(reactive object),否则就没法影响组件的渲染。但是我们通过vuex初始化后的state 只是个普通对象。到目前为止无法做到state 数据改变时使用该state相应组件也会跟着变化。因此state必须要想办法把它转成reactive 对象,才能在vue组件中使用。因为
vue中的data属性是个reactive object,所以我们只要把vuex的state传入vue中data属性,不需要在vuex另写一套把state转成可响应的代码。vuex中的getters也是同样道理,getters是依赖state的computed,那么我们也可以把getters做好处理后传入computed属性中。
vuex源码中
resetStoreVM 就是在vuex 中生成一个vue实例_vm ,然后将state 和getters 属性转换。伪代码:
class Store {
constrcutor() {
}
const store = this
//存储getters
this._wrapperGetters = Object.create(null)
function buildVM(store){
const wrapperGetters = store.wrapperGetters
forEachValue(wrapperGetters,(fn,key) =>{
/**将wrapperGetters转变成
computed:{
zzz(){
return xxxx
}
*/ }
computed[key] = partial(fn,store)
//把computed的getters通过get挂载在store.getters上
Object.defineProperty(store.getters, key, {
get: ()=>store._vm[key],
enumerable: true
})
})
store._vm= new vue({
data:{
$$state:store.state
},
computed
})
}
enableStrictMode(store)
get state() {
return this._vm._data.$$state
}
//将getters注册到this._wrapperGetters
registerGetters() {
...
}
}
function forEachValue(obj,fn) {
Object.keys(obj).forEach(key => fn(obj[key],key))
}
在 buildVM函数内使用
new vue 创建一个vue新实例_vm ,将_vm 挂载在store上面store._vm= new vue(...) ,state 传入到 _vm 中的 data 属性中,这样就完成了store.state 的可响应化。getters是依赖state的computed property,是不是和vue中·computed属性一致呢,所以按照相同逻辑,将wrapperGetters 转成vue实例中computed 属性函数。现在我们解决之前的问题就是只能通过
commit 修改state,直接修改就会报错。function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
if (__DEV__) {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
之前说每次
commit 都会改变_committing 值,所以在store._vm 创建后,使用$watch 来监测this._data.$$state 数据变化,如果$$state 数据变化了, 但是 _committing 值为false,那么就是直接修改state,就会报错了。插件和热重载
vuex支持插件plugin 系统,plugin 系统的基本原理其实很简单,就是传入一个实例化的store 这样就可以调用store 里的各种参数和方法了。plugin
代码:
const myPlugin = store => {
// 当 store 初始化后调用
store.subscribe((mutation, state) => {
// 每次 mutation 之后调用
// mutation 的格式为 { type, payload }
})
}
在插件中是也不能直接修改状态,只能通过
mutation和action修改。通过提交 mutation,插件可以用来同步数据源到 store。/index.js
const plugin = createWebSocketPlugin(socket)
const store = new Vuex.Store({
state,
mutations,
plugins: [plugin]
})
在
plugin函数中可以使用store.subscribe和store.subscribeAction用来订阅mutations 和actions 。可以看看
subscribe官方文档解释:订阅 store 的 mutation。handler会在每个 mutation 完成后调用,接收 mutation 和经过 mutation 。
1.subscribe
以
subscribe为例,首先把handle保存在store实例中,subscribe系列函数返回一个取消订阅函数。function subscribe(handle) {
this.subscriber.push(handle)
return ()=>{
this.subscriber.splice(handle,0)
}
}
2.调用方法
store.subscribe((mutation, state) => {
console.log(mutation.type)
console.log(mutation.payload)
})
handler 会在每个 mutation 完成后调用,接收 mutation 和经过 mutation 后的状态作为参数,因此handler调用是和commit息息相关的,mutation和handler顺序不能反,否则handler无法接受到mutation和经过 mutation 后的状态为参数。commit() {
this.withCommiting(()=>{
this.mutation[xxx]()
this.store.subscribers.forEach((sub)=>sub())
})
}
3.取消订阅
subscribe 方法会返回一个 unsubscribe 函数,当不再需要订阅时应该调用该函数。例如,你可能会订阅一个 Vuex 模块,当你取消注册该模块时取消订阅。或者你可能从一个 Vue 组件内部调用 subscribe,然后不久就会销毁该组件。在这些情况下,你应该记得手动取消订阅。const unscribe= store.subscribe(()=>{xxxx})
//取消订阅
unscribe()
hotreload
hotreload不是vuex本身具备的功能而是webpack提供。1.通过
module.hot来判断是否支持热重载2.然后通过
module.hot.accept(xx)jianggetters,mutations,actions当成模块载入,然后提取需要热重载的模块
3.
store.hotUpdate加载新模块// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import moduleA from './modules/a'
Vue.use(Vuex)
const state = { ... }
const store = new Vuex.Store({
state,
mutations,
modules: {
a: moduleA
}
})
if (module.hot) {
// 使 action 和 mutation 成为可热重载模块
module.hot.accept(['./mutations', './modules/a'], () => {
// 获取更新后的模块
// 因为 babel 6 的模块编译格式问题,这里需要加上 `.default`
const newMutations = require('./mutations').default
const newModuleA = require('./modules/a').default
// 加载新模块
store.hotUpdate({
mutations: newMutations,
modules: {
a: newModuleA
}
})
})
}
到这里可以总结一下vuex的源码流程
1.注入vue
2.模块化
3.注册模块下的actions,mutations,getters
4.初始化_vm,将store的state转成reactive和getters转成computed 属性
5.初始化插件
constructor (options = {}) {
//1注入vue
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
...
//创建存储actions,getters,mutations等对象,并模块初始化
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)
....
//注册模块下的actions,mutations,getters
installModule(this, state, [], this._modules.root)
//初始化_vm,将store的state转成reactive和getters转成computed 属性
resetStoreVM(this, state)
// 初始化插件
plugins.forEach(plugin => plugin(this))
}
tips
require.context动态加载模块,返回一个webpackContext,这个context有三个属性
resolve, keys, id。这个
webpackContext其实内部大概是var map = {
"./A.js": "./src/components/test/components/A.js",
"./B.js": "./src/components/test/components/B.js",
"./C.js": "./src/components/test/components/C.js",
"./D.js": "./src/components/test/components/D.js"
};
只不过map是模块内部变量,无法直接访问,所以通过提供的keys方法访问。
然后将内部的
map通过keys()迭代获取key值,然后通过key获取value,最后包装成{key:value}外部对象。const context = require.context("./modules", false, /([a-z_]+)\.js$/i)
const modules = context
.keys()
.map((key) => ({ key, name: key.match(/([a-z_]+)\.js$/i)[1] }))
.reduce(
(modules, { key, name }) => ({
...modules,
[name]: context(key).default
}),
zhe {}
)
利用
require,context动态导入模块const importAll = context => {
const map = {}
for (const key of context.keys()) {
const keyArr = key.split('/')
keyArr.shift() // 移除.
map[keyArr.join('.').replace(/\.js$/g, '')] = context(key)
}
return map
}
export default importAll
import importAll from '$common/importAll'
export default importAll(require.context('./', true, /\.js$/))
利用
require,context热重载还需要用到context返回的id- keys: 返回匹配成功模块的名字组成的数组
- resolve: 接受一个参数request,request为test文件夹下面匹配文件的相对路径,返回这个匹配文件相对于整个工程的相对路径
- id: 执行环境的id,返回的是一个字符串,主要用在module.hot.accept,热加载
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
// 加载所有模块。
function loadModules() {
const context = require.context("./modules", false, /([a-z_]+)\.js$/i)
const modules = context
.keys()
.map((key) => ({ key, name: key.match(/([a-z_]+)\.js$/i)[1] }))
.reduce(
(modules, { key, name }) => ({
...modules,
[name]: context(key).default
}),
{}
)
return { context, modules }
}
const { context, modules } = loadModules()
Vue.use(Vuex)
const store = new Vuex.Store({
modules
})
if (module.hot) {
// 在任何模块发生改变时进行热重载。
module.hot.accept(context.id, () => {
const { modules } = loadModules()
store.hotUpdate({
modules
})
})
}
posted on 2022-09-06 16:47 Alice_Fincher 阅读(28) 评论(0) 收藏 举报
浙公网安备 33010602011771号