震惊! Vue的Devtool Plugin居然...

本文深入剖析了一个Vue项目中遇到的诡异问题:DevTools插件显示的Vuex状态与当前页面不符。通过源码级别的调试,作者揭示了这一现象背后的原理,即DevTools插件只关联并展示首次创建的Vuex Store实例,而项目中多次创建Store导致的显示错误。文章最后给出了解决方案,并提出了对项目中Store实例管理的思考。

今天改vue项目的时候发现一个问题,

devtool这个插件的vuex tab里显示的不是我当前页面的store。

纳尼?!!浑身发抖好吗,我们的项目不是spa,该页面没有引入其他store,也没有用vuex的modules机制,甚至数据都是对的,只是devtool plugin里显示的内容对不上号。

究竟发生了什么?(赶时间的同学可直接跳到结尾^_^)

分析一下现场:

  • chrome控制台 vue devtool插件中的store

  • 当前页面的root app绑定的store

  • 页面B(第三者,元凶)的store

仔细看发现该store是页面B的store。

难道是cache了?随后,就在callstack里发现了cache关键字眼:

随后长枪如龙,一顿操作猛如虎:

rm -rf node_modules/
npm install
复制代码

问题依在

cmd shift del Chrome缓存全清
复制代码

问题依在

restart computer...
复制代码

问题依在,(最后像个250..)

种种排查,排除了plugin里vuex内容cache的可能性,

然后开始我想当然地开始怀疑起了vue devtool plugin本身的问题, 难道是尤大的bug?

将github的devtool issues挨个看过去,好像没什么搭边的,

唯一的bug还是chrome的控制台有时切tab会报错,影响到tab页内容显示空白,不过这也跟devtool没关系,

但我还是非常执着而认真的重装了最新的vue devtool...

问题依在

事实证明怀疑官方库实在是愚蠢的行为,程序99.9%的问题都是出于自身。

还是老老实实看我们的项目代码。

仔细看下来发现我们的entry中引入了一个commonEntry,

commonEntry中引入了我们自己的基础组件库

然后基础组件库里的某些组件引用了单独编写的工具类

工具类里又引入了一个common的供查询页面使用的store
该store里又引入了一个gridStore,供表格使用的store

下面来看下该GirdStore的大致定义:

let GridStore = function GridStore(options){
    let state = {
        columnConfigs: [],
        pageRecords: [],
        selectedRecords: [],
    };
    options.state =  Object.assign(state,options.state);
    Vuex.Store.call(this, options);
}
GridStore.prototype = Object.create(Vuex.Store.prototype);
export default GridStore
复制代码

可以看到GirdStore继承了Vuex.Store

(或者叫'委托',就像You don't know JavaScript中描述的那样,只是我觉得这个词吧,它有点绕....)

聪明的小伙伴应该发现问题所在了。

new GridStore相当于又new了一个Vuex.Store,导致了我们项目中实际上同时存在了多个vuex的store。

所以数据虽然没问题,但同时存在了一堆垃圾store。

接下来的问题就是这为什么会影响到devtool plugin,导致其vuex tab显示的不是我们想要的内容。

试想devtool plugin能将vuex数据可视化,那势必在vuex的实现体中有所关联。

理所当然的,我们应该尝试去探索new Vuex.Store的时候究竟发生了什么?

下面有个小技巧,不管用的editor是sublime或者vscode等,

常常我们查找函数定义体的时候会跳出来 一堆(有点类似idea里java的interface找实现类...):

这种情况下,如果想快速定位到真正的函数定义体,不妨在chrome控制台中利用step into 功能,

我们在这里加个断点

刷新页面断点进到gridStore实现体:

然后点击step into 进入到Vuex.Store的实现体:

现在我们准确的找到了Vuex的源码里store定义的地方,源码如下:

var Store = function Store (options) {
  var this$1 = this;
  if ( options === void 0 ) 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 (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;

  var state = options.state; if ( state === void 0 ) state = {};
  if (typeof state === 'function') {
    state = state() || {};
  }

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

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

  if (Vue.config.devtools) {
    devtoolPlugin(this);
  }
};
复制代码

仔细看,我们注意到最后有一个vue.config.devtools的判断,原来就是这里将vuex关联上了插件。

然后我们继续通过step into进入到devtoolPlugin的实现体,源码如下:

function devtoolPlugin (store) {
  if (!devtoolHook) { return }

  store._devtoolHook = devtoolHook;

  devtoolHook.emit('vuex:init', store);

  devtoolHook.on('vuex:travel-to-state', function (targetState) {
    store.replaceState(targetState);
  });

  store.subscribe(function (mutation, state) {
    devtoolHook.emit('vuex:mutation', mutation, state);
  });
}
复制代码

果不其然, 我们可以看到devtoolHook.emit('vuex:init',store),

通过一个钩子emit出去了一个vuex的init事件,并且载荷了传进来的store。

接着我们继续step into该emit函数,这时候代码是被压缩的,

我们点击控制台左下角的pretty Print(就那个大括号图标),将代码格式化一下,

emit(t) {
    var e = "$" + t,
        n = r[e];
    if (n) {
        var i = [].slice.call(arguments, 1);
        n = n.slice();
        for (var o = 0, u = n.length; o < u; o++)
            n[o].apply(this, i)
    } else {
        var f = [].slice.call(arguments);
        this._buffer.push(f)
    }
}
复制代码

然后单步调试下来不难发现,n拿到了init函数并通过apply方法去执行了初始化操作:

第一次初始化时调用了off方法,移除了上下文中的init事件,下次再调用devtoolPlugin的时候,r中已无init属性, n拿到的就是空数组,也就不会再次拿传进来的store去重新做初始化操作。

看到这里,我想大伙应该都明白了,

vuex devtool plugin关联vuex的初始化操作可以说是单例的,只会显示第一个new Vuex.Store的store。

呼,终于破案。。。。。。。喝杯水压压惊


  • 首先我们自己的基础组件库里有不少都有引用vuex store,

      有的是直接引用,有的是import的工具类里引用,
    
      并且调用了new Vuex.Store,造成了同时存在了多个Store,
    
      然后实际上只有当前页面root app自己绑定的store是有用的。
    
      我觉得这肯定是有问题的(不知道大家怎么想)
    复制代码
  • 其次,我想后面一通断点加源码分析,大家伙肯定想试验一个问题,究竟是不是像我分析的那样,

      只有第一次调用new Vuex.Store的store会显示在devtool plugin里。
    复制代码

下面大家不妨自行做个试验,

类似如下

new Vuex.Store({
  state: {
    test: 1
  }
})

new Vuex.Store({
  state: {
    test: 2
  }
})
复制代码

然后打开控制台,切到devtool plugin 的vuex tab中,看看state里是不是test: 1。

我先干为敬:

转载于:https://juejin.im/post/5cb2b0696fb9a06856567f5d

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值