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

Joki發表於2019-04-14

今天改vue專案的時候發現一個問題,

devtool這個外掛的vuex tab裡顯示的不是我當前頁面的store。

納尼?!!渾身發抖好嗎,我們的專案不是spa,該頁面沒有引入其他store,也沒有用vuex的modules機制,甚至資料都是對的,只是devtool plugin裡顯示的內容對不上號。

究竟發生了什麼?

分析一下現場:

  • chrome控制檯 vue devtool外掛中的store

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

  • 當前頁面的root app繫結的store

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

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

震驚! Vue的Devtool Plugin居然...
仔細看發現該store是頁面B的store。

難道是cache了?隨後,就在callstack裡發現了cache關鍵字眼:

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

隨後長槍如龍,一頓操作猛如虎:

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,

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

commonEntry中引入了我們自己的基礎元件庫

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

然後基礎元件庫裡的某些元件引用了單獨編寫的工具類

震驚! Vue的Devtool Plugin居然...
工具類裡又引入了一個common的供查詢頁面使用的store
震驚! Vue的Devtool Plugin居然...
該store裡又引入了一個gridStore,供表格使用的store

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

下面來看下該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找實現類...):

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

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

這種情況下,如果想快速定位到真正的函式定義體,不妨在chrome控制檯中利用step into 功能,

震驚! Vue的Devtool Plugin居然...
我們在這裡加個斷點

震驚! Vue的Devtool Plugin居然...
重新整理頁面斷點進到gridStore實現體:

震驚! Vue的Devtool Plugin居然...
然後點選step into 進入到Vuex.Store的實現體:

震驚! Vue的Devtool Plugin居然...
現在我們準確的找到了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關聯上了外掛。

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

然後我們繼續通過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)
    }
}
複製程式碼

震驚! Vue的Devtool Plugin居然...
然後單步除錯下來不難發現,n拿到了init函式並通過apply方法去執行了初始化操作:

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

第一次初始化時呼叫了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。

我先乾為敬:

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

相關文章