前提知识

Vuex涉及Vue原理中的computed,Vue的响应式原理。

其实Vuex的原理可以归结为它源码里面的一句话(如下),下面来看看这句话的威力所在。

store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })

 

install阶段

我们在使用Vuex的时候可能会这么书写配置。

import Vue from 'vue';
import Vuex from 'vuex';
import set = Reflect.set;

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
      
    },
    getters: {
      
    },
    mutations: {
       
    },
    actions: {
   
    },
    modules: {}
});

配置第一句话就是Vue.use()。Vue的插件基本上都要执行这句话,这句话的作用就是执行插件的install方法。下面是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

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

if (version >= 2) {
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)
}
}

/**
* 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
}
}
}

install关键就是执行了applyMixin,applyMixin其实很好理解。它的作用就是在于给Vue的组件混入一个beforeCreate的钩子函数,这个钩子函数的作用其实就是在Vue的实例挂载这个$store属性。install阶段做的重要东西就这个

 

new Store阶段

export class 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)
    }

    if (__DEV__) {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `store must be called with the new operator.`)
    }

    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
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    //我们执行store commit的时候是在执行这个方法
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // strict mode
    this.strict = strict

    //这个state应该就是根目录的用户编写的state
    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
    //有plugins才会执行
    plugins.forEach(plugin => plugin(this))

    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)
    }
  }

  //state的get
  get state () {
    //this._vm._data.$$state 就是用户写state写的东西
    return this._vm._data.$$state
  }

  set state (v) {
    if (__DEV__) {
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }

...
}

我们关注一句 resetStoreVM(this, state)。这句话的作用就是为我们用户写的state和Vue的视图更新产生关联重要地方。源码如下:

 

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, {
      //这里应该才是真正执行computed函数的
      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())
  }
}

 

我们首先关注forEachValue函数,它的源码如下:

export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

 

在上面调用它的使用传入了两个参数第一个是wrappedGetters,第二个函数是一个fn。wrappedGetters是一个对象,它存放着用户写的getters函数,例如你的getters是这么写的

 

getters: {
    getUser: state => state.username
},

//那么wrappedGetters就长这样
wrappedGetters: {
    getUser: fn
}

 

所以对他使用forEachValue方法就是遍历这个getters的每一个键,然后去执行fn,参数是getters里面用户写的函数和键的名称。从上面看fn是这个函数(如下):

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

它接受用户getters里面的键值函数和getters键为参数。

这个函数的作用就是把getters里面的key,复制到computed中,并且执行一个叫做partical的函数。partical这个函数的作用其实就是执行用户的那个getters定义的函数而已,就不贴代码了。就是说computed和getters里面的东西有相同的键值

然后为store的getters属性,定义一个get的拦截器。

上面就是forEachValue的作用。

 

回到resetStoreVM上来,我们继续关注这段话。

store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
})

这句话在整个Vuex起着十分关键的作用。他在store上定义了一个_vm属性,这个属性的值是一个Vue的实例。并且传入一个data参数,他有一个$$state属性,这个属性的值就是用户编写的state对象。第二个参数是一个computed计算属性。通过上面对forEachValue的分析,我们知道了这个computed是一个对象,对象的键和用户自定义的getters里面的键是一样的。

那为什么Vuex需要这么做。

他把用户写的state作为新Vue实例的data,那么在Vue响应式原理中,当我们读取这个state里面的属性的时候就会开始收集依赖,意思就是说,假如你在别的组件使用计算属性或者别的方法引用着state里面的属性,那么这些依赖即Watcher就会被收集到state对象其下的属性中。意思就是说,一些别的组件引用着这个store里面的state,本质上就是在使用这个new Vue里面的data。当你这个data的state里面的属性一更新,就会通知引用它的组件进行更新。所以这就解释了为什么state里面的数据是响应式的。本质上就是通过这个new Vue组件去让这个state成为一个响应式数据,让它去收集使用着它的组件的Watcher依赖。

 

还有一个问题:就是Vuex的commit是怎么响应式的更新视图

 

commint的时候Vuex会通过一些方法执行到你的自定义函数,在我们的自定义函数中我们就会把store里面state的值进行修改。此时就会走回上面state响应式的流程。你修改state的值,就会触发Vue响应式里面的set拦截器,从而会导致视图的更新。这就是为什么commit修改数据之后可以响应式的更新视图。

 

还有一个小问题,在Vuex官方文档中,它的响应式规则有这么句话:

 

 

看圈住的两句话,为什么会是这样的。从上面的原理上看,我们在state里面写好的属性都是作为参数传入new Vue,还是这句话(如下),从而使他成为一个响应式的数据。

store._vm = new Vue({
    data: {
      $$state: state
    },
    computed})

但是假如你在中途突然增加一个属性,那么它是不会被作为new Vue的参数传入其中,从而这个属性不是响应式的,更加不可能收集到依赖进行视图的更新。

 

posted on 2020-09-22 16:34  余圣源  阅读(331)  评论(0编辑  收藏  举报