什么是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
})
})
}
标签:分析,...,const,module,state,源码,._,vuex,store
From: https://www.cnblogs.com/aliceKurapika/p/16662404.html