Vue状态管理插件Vuex源码探秘01

本文主要分析Vuex插件store的初始化过程。先找到插件入口install方法,其会给Vue mixin钩子,让子组件获取$store属性。接着解析$store,它指向Vuex.Store实例,介绍了其构造函数中属性定义、方法绑定和调用等操作。最后总结了初始化后store的样子。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.入口

分析Vue插件,首先应找到其install方法,这是插件的入口。Vuex的install如下:

// src/store.js
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
复制代码

可以看到,Vuex保存了局部变量Vue,防止插件重复安装,接下来调用了applyMixin(Vue)。

这里似乎存在一个问题,即用Vue安装Vuex后,仍然可以用Vue的子类安装,但用Vue子类安装之后,保存的局部变量Vue就变成了子类,这样Vue使用Vuex时似乎会受到子类的影响。这里暂时不去探究,如果我想错了或者你有什么想法,请在评论中告诉我。

根据依赖import applyMixin from './mixin'找到mixin,源码如下:

// src/mixin.js
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) { //2.0 and upper
    // mixin Vue with a beforeCreate hook
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }
复制代码

install中对Vue 1.x 做了一个兼容,然后给Vue mixin了一个beforeCreate的钩子,往下看,这个钩子到底干了什么

function vuexInit () {
    // 这是被vue实例在beforeCreate hook 是调用
    // 给实例添加了一个$store 属性,是Vuex.Stroe的实例
    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
    }
}
复制代码

这个钩子的作用就是从新建Vue实例的options中找到store属性,然后赋值给vm.$store。如果当前options没有该属性,则从父类上找。这样只要根组件具有$store,则每个子组件都会具有$store属性。子组件也可以有一个不同于父级的$store属性。

2. $store解析

上面讲到Vuex会给每个vm实例都添加一个$store属性,那这个$store是什么呢?我们回想一下Vuex的用法:

import Vue from 'vue'
import Vuex from 'vuex'
import mutations from 'xx/mutations'
import actions from 'xx/action'
import * as getters from 'xx/getters'

Vue.use(Vuex)
const state = {
    //...
}
let store = new Vuex.Store({
    state,
    getters,
    actions,
    mutations
});

let vm = new Vue({
    //...
    store
});
vm.$store....
复制代码

所以,Vue实例上的$store属性,指向的是一个Vuex.Store的实例。然后我们回到store.js寻找Store的构造函数。

constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    // 省略 测试语句 及插件语句 

    // 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()

    // bind commit and dispatch to self
    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)
    }

    // 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)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)

    // apply plugins  省略
  }
复制代码

构造函数首句,如果当前没有安装Vuex但是Vue暴露在了window下,则自动安装,用途不详。

  • 属性定义

接下来,给store实例定义了一些属性,以_开头,意为私有属性。

this._committing = false  //flag
this._actions = Object.create(null) //定义的actions
this._actionSubscribers = []  //action的订阅者
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)  //包装Getter
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []  //订阅者
this._watcherVM = new Vue()
复制代码

这里插一句,Object.create(null)表示以null为原型创建一个对象,这是一个纯净的空对象,它没有原型,所以不会继承Object.prototype.toString等方法,也没有contructor构造函数。

其中我们看一下_modules属性,它是以options为参数,新建的一个ModuleCollection。找到ModuleCollection。

// src/module/module-collection
export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }  
  register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }

    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // 注册嵌套的modules,我们这里没有  
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
  // 其余省略
}
复制代码

构造函数中直接调用了register。我们往下看register。它新建了新建了一个newModule,并且由于狗造函数中传入的path为[],所以,这个newModule被赋值给ModuleCollection实例的root属性。

再去看看Module构造函数

constructor (rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
 }
复制代码

所以newModule其实是这样的:

newModule = {
    runtime:false,
    _children:{},
    _rawModule:options, // 新建Store时传入的options
    state:options.state  //若是函数,则是求值后的,为空时则为{}
}
复制代码

所以store实例的_modules属性其实是这样的:

_modules:{
    root:{ //Module对象的实例,方法省略
        runtime:false,
        _children:{},
        _rawModule:options, // 新建Store时传入的options
        state:options.state  //若是函数,则是求值后的,为空时则为{}
    }
}
复制代码
  • 方法绑定

属性定义之后,是这样的代码:

// bind commit and dispatch to self
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)
}
复制代码

目的是绑定dispatch和commit的运行环境为当前实例,为什么要这么做呢? 我们来看一下MapMutation的用法:

// vue模板中这样使用
methods:{
    ...mapMutations(['method1','method2'])
}
复制代码

这相当于把mutation方法赋值到了Vue实例上,然后可以通过vm.method1()这样的调用,此时method1可以绑定内部this为store,但method1内部使用的commit却无法绑定,所以需要预先绑定。

  • 方法调用

Store构造方法的最后,调用了两个函数

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)

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
复制代码

state临时变量,其实就是我们options的state属性求值后的结果,所以我们的state可以定义成一个函数或者说应该定义成一个函数,就像Vue的data一样。

接下来先看一下installModule(this,state,[],this._modules.root)函数

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length  //true
  //...子module命名空间相关,此处省略
  
  const local = module.context = makeLocalContext(store, namespace, path)

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
  // child module 处理,省略...
}
复制代码

根据精简后的代码可以看到,代码先是定义了一个local局部变量,用来获取当前命名空间下的属性,这里暂时我们不需要关心,因为我们没有使用命名空间,可以把它看成是简单复制了store的几个属性。

然后使用了四个foreachXXX,看名字就应该知道是将options参数中的mutations、actions...注册到store上,以registerMutation(store, namespacedType, mutation, local)为例:

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}
复制代码

可以看到这个函数的目的就是将options.mutations放入到store._mutations,并绑定了执行环境为store。假设你定义了如下mutation:

options = {
    mutations:{
        initData:()=>{},
        initProps:()=>{}
    }
}
复制代码

则store._mutations会变成:

_mutations:{
    initData:[wrappedInitData],
    initProps:[wrappedInitProps]
}
复制代码

然后到了resetStoreVM(this, state)函数

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  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 enviroment.
    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
  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())
  }
}
复制代码

首先用局部变量oldVm保存了store._vm属性,这是一个Vue实例,但目前并未定义

接着给store定义了getters属性,

然后遍历了store._wrappedGetters,用computed缓存了里面方法的闭包,同时以_wrappedGetters的keys给getters定义了若干存储器属性,代理访问store._vm的同名属性

接下来定义了store._vm,是一个Vue实例,里面的data包含一个属性$$state,以state赋值,还包含了前面定义的computed作为计算属性,即是保存了参数的_wrappedGetters。新建Vue实例时用到了Vue.config.silent开关,目的是取消Vue的所有日志和警告。 接下来就没有了,store的初始化已经完成。

总结一下,就是store定义了一个_vm,并将state作为data的属性,getters作为计算属性。所以说Vuex还是利用了Vue来完成响应式。

3.store长什么样

写了这么多,这里先总结一下经过初始化的store长成什么样,便于理解记忆

vm:{
    $store:{
        _committing:false,
        _actions:{ //经过包装的选项中的actions
            action1:[wrappedActionFunc],
            // ... 
        },
        _actionSubscribers:[],
        _mutations:{ //经过包装的选项中的mutations
            mutation1:[wrappedMutationFunc],
            //... 
        },
        _wrappedGetters:{
            getter1:wrappedGetter  //这里并不是数组,getter不能重复
        },
        _modules:{
            root:{
                runtime:false,
                _children:{},
                _rawModule:options, // 新建Store时传入的options
                state  //求值后的options.state
            }
        },
        _modulesNamespaceMap:{},
        _subscribers:[],
        dispatch:()=>{},
        commit:()=>{}, //这两个方法专门绑定过,内部执行环境始终指向store
        getters:{
            //根据options.getters的key定义的存储器属性,
            // 实际访问的是store._vm的computed属性
        },
        _vm:{
            data:{
                $$state:state
            },
            computed:{
                //由options.getters生成,被store.getters代理访问
            }
        },
        state:{
            get(){
    
            },
            set(v){
                //无效,应使用 store.replaceState() 代替
            }
        },
        subscribe:(fn)=>{},
        subscribeAction:(fn)=>{},
        watch:(getter, cb, options)=>{},
        replaceState:(state)=>{},
        registerModule:(path, rawModule, options = {})=>{},
        unregisterModule:(path)=>{},
        hotUpdate:(newOptions)=>{},
        _withCommit:(fn)=>{}
    }
}
复制代码

以上就是store初始化后的全貌了,接下来准备以一个简单的Vuex的使用例子去探寻更多的实现细节及原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值