探索Vue.js底层源码——Vuex 插件注册及初始化

引言

无论是 React 也好,Vue 也罢,它们都有自己的全局状态管理机制 Redux 和 Vuex。同样地,这也是它们框架的一大特色。并且,对于 Vue 而言,Vuex 和之前我讲的 Vue-Router 一样,是作为插件使用。

在认识 Vuex 的 Store 源码之前,我们先来回顾一下 Store 的特点:

  • 存储的数据是全局可访问的
  • 数据同样是响应式的,即它也是基于订阅者-发布模式
  • 其中的数据好比被存到仓库中,和其他变量没有交叉和影响
  • 数据的修改,需要通过提交 action,然后通过 mutation 来操作,可以很好地跟踪数据地变化过程

然后,我们带着这些优点,来认识一下 Vuex 的初始化过程它做了什么。

注册

对于 Vuex,同样地是通过 Vue.use() 方法注册。而 use 方法会对应地调用 Vuex 中的 install 方法。

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

可以看出和 Vue-Router 的 install 方法大同小异,同样地也会在 install 方法中判断是否已经注册过 Vuex。如果没有注册过,将 use 方法中传的 Vue 实例(此时代码中对应的为 _ Vue)赋值给 Vue,然后调用 applyMixin(Vue)。

function applyMixin (Vue) {
  var version = Number(Vue.version.split('.')[0]);

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit });
  } else { // 1x 走的逻辑
  	...
}

而 applyMixin 则是根据不同版本,命中不同逻辑(本次我们只看 2x 版本)。首先,会先在 Vue 实例的 beforeCreate 生命周期函数注入 vuexInit 函数。

function vuexInit () {
    var 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;
    }
  }
}

vuexInit 函数做了两件事,如果此时 o p t i o n s s t o r e t h i s . options 存在 store,则把它赋值给 this. store。反正,则将它父级的 s t o r e t h i s . store 传给当前 this. store。所以,这也是为什么我们在开发中可以使用 $store 的原因。

实例化

Vuex 的实例化,就是 new 一个 Vuex.Store() 实例。

var Store = function Store (options) {
  var this$1 = this;
  if ( options === void 0 ) options = {};

  if (!Vue && typeof window !== 'undefined' && window.Vue) {
    install(window.Vue);
  }

  if (process.env.NODE_ENV !== 'production') {
    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.");
  }

  var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
  var strict = options.strict; if ( strict === void 0 ) strict = false;

  // 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
  var store = this;
  var ref = this;
  var dispatch = ref.dispatch;
  var commit = ref.commit;
  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;

  var 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.forEach(function (plugin) { return plugin(this$1); });

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

store 构造函数的代码可能有点多,我们来分点讲解具体它做了什么:

1.对如果不是基于 Vue-Cli,即仅仅是通过 link 了一个 Vuex 的文件,然后在构造函数会通过在 window 上绑定一个 Vue 属性来作为 Install 的参数进行注册。

  if (!Vue && typeof window !== 'undefined' && window.Vue) {
    install(window.Vue);
  }

2.对开发环境下一些环境判断,例如必须使用 use 方法注册 Vuex、必须在支持或者有 polyfill 的 Promise 下开发、必须使用 new 关键字初始化 Vuex.Store。

  if (process.env.NODE_ENV !== 'production') {
    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.");
  }

3.在当前上下文 this 中绑定一些私有属性来存储 state、action、mutation、getter、module、moduleNamespaceMap(模块的映射)、subscribe(订阅者)、watcherVM(store对应的 watcher),里面有几个重要的属性,moduleNamespaceMap 这个的作用是记录此时的 moduleName(实际上是 /+name),然后每一个键值对应一个 module,从而完成 module 的映射,例如此时有一个 module a 和 b,而它们在源码中的映射为:
在这里插入图片描述
然后,这里出现了我们熟悉的 watcher 和 subscribe,是因为要用来动态更新那些订阅了 Store 的订阅者。

4.前面只是定义了我们需要使用的属性,然后,需要进行一些初始化操作,installModule 就是按照 module,并生成访问规则和模块映射关系,它的作用是便于开发时的代码编写,例如我们不同模块间是可以重复 action、state、getter 名称的。

  // init root module.
  // this also recursively registers all sub-modules
  // and collects all module getters inside this._wrappedGetters
  installModule(this, state, [], this._modules.root);

5.初始化 module 后,我们需要对 state 进行响应式处理,以及将 getters 转成 computed。

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

6.最后,会对 options 进行一些插件调用以及和 devtoolPlugin 的一些配合,然后我们就可以在 devtool 中观察到我们 vuex 的一些变化。

  // apply plugins
  plugins.forEach(function (plugin) { return plugin(this$1); });

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

总结

大致过了一下 Vuex 的初始化过程,主要是在传进的根实例上绑定和 Store 相关的一些私有属性,然后注册生成 module 树,然后对生成不同模块的属性的映射 Map,最后对 state 和 getters 进行一些转化,把它们传入到一个 vue 实例中(分别对应 vue 实例中的 data 和 compute),并通过 Obeject.defineProperty 对数据的访问做一层代理访问到 vue 实例中的 data 和 compute,从而实现数据的响应式。其实,我上面描述的一些具体的细节,有兴趣的同学还是写个简单的 Demo,然后 debugger 一下,这样记忆点会更深刻一些。

发布了140 篇原创文章 · 获赞 16 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_42049445/article/details/104587674