vuex 响应式原理

4,073 阅读1分钟

知识预备,vue是如何在data变化后就更新dom的

vue模板渲染时怎么对各属性值产生依赖的?且看mounted源码

function mountComponent (){
	...
    } else {
      updateComponent = function () {
        vm._update(vm._render(), hydrating);
      };
    }

    new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */);
    ...
}

实际上是给vm对象的模板生成/更新函数注册了个Watcher

Watcher默认是非lazy的,就是说不是懒执行的,而是初始化就会立刻执行,所以vue内部调用mountComponent时,会直接调用updateComponent渲染真实dom树

Watcher简略版构造函数:

function Watcher(vm, fn, cb, options){
    this.getter = fn
    this.lazy = !!options.lazy
    this.value = this.lazy ? undefined : this.getter()
}

Watcher本质上是订阅者,会在执行其更新函数getter过程中订阅所有getter的依赖项,至于具体如何绑定依赖,可以阅读此文章:vue computed原理

当依赖产生后,一旦vm.data中的被依赖项发生了变化,就会触发Watcher去重新执行getter,也就是mountComponent里生成的updateComponent函数,从而更新dom

vuex响应式原理

一旦理解了vue的模板如何响应数据变化,那么vuex就好理解了

vuex本质上是将state值绑定到了一个vue对象上,请看超简略源码:

class Store {
    constructor(options){
        this.state = new Vue({
            data:options.state
        })
    }
}

于是当我们在test.vue中写出这种代码:

<template>
	<div>{{ $store.state.xx }}</div>
</template>

test.vue实例mount的时候执行updateComponent,就会为updateComponent函数绑定一个依赖:Store.state.xx这个属性的Dep对象(暂时命名为xxDep,便于后续说明)

那么一旦通过commit或其他手段更新了属性Store.state.xxxxDep就会通知updateComponent所绑定的Watcher去执行update

Watcher.prototype.update = function(){
	if (this.lazy) {
    	...
    } else {
    	// 将此watcher加入队列,在nextick中执行
        // 最终会执行到Watcher.getter,本例中也就是updateComponent
		queueWatcher(this);
	}
}

从而最终又执行到了updateComponent去更新dom树,而在执行updateComponent过程中解析dom树时会重新获取{{ $store.state.xx }},从而正确的更新了dom,实现了store.statevue对象的绑定

store.getters

上面讲了store.state如何绑定到vue对象,那么store.getters呢?

var wrappedGetters = store._wrappedGetters;
var computed = {};
forEachValue(wrappedGetters, function (fn, key) {
  computed[key] = partial(fn, store);
  Object.defineProperty(store.getters, key, {
    get: function () { return store._vm[key]; },
    enumerable: true // for local getters
  });
});

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

可以看到对于每个getters的值,最终放在两个地方:store.getters, store内部的vue对象上的computed属性,computed属性的双向绑定机制跟data属性类似,这里不多讲

而通过store.getters.key获取的值根据以上代码,得到的是store._vm[key],而这个就是computed[key],因为computed属性都会绑定到vm对象上。所以store.getters[key]===computed[key],是完完全全的同一个值

-----------------------2022/4/8更新----------------------------

装载到vue

vue2中使用vuex需要执行vue.use(vuex)。最终会执行到vuexinstall方法

// 初始化全局Vue对象时挂载store,并在跟元素上生成
new Vue({
    store,
    ...
})

function install() {
    Vue.mixin({
        beforeCreate() {
            if (this.$options.store) {
                this.$store = this.$options.store // 这里对应根组件
                return
            }
            this.$store = this.$parent.$store // 其他组件逐级向上取
        } 
    })
}

通过生命周期给每个组件单独挂载$store,而不是直接Vue.prototype.$store =,这样可以防止声明多个vuex实例后覆盖

vue3中挂载vuex要执行app.use(store)。最终会执行到Store.prototype.install

function install (app, injectKey) {
    // globalProperties属性上挂载的属性可以在app下所有组件实例中访问到
    app.config.globalProperties.$store = this;
}