Vue(ES6)中的data屬性為什麼不能是一個物件?

YaHuiLiang(Ryou)發表於2018-07-16

以下引官網原文:當一個元件被定義,data 必須宣告為返回一個初始資料物件的函式,因為元件可能被用來建立多個例項。如果 data 仍然是一個純粹的物件,則所有的例項將共享引用同一個資料物件!通過提供 data 函式,每次建立一個新例項後,我們能夠呼叫 data 函式,從而返回初始資料的一個全新副本資料物件。

最近來面試的很多人。我都會問這個問題“vue中,為什麼data是一個方法返回一個物件,而不是直接賦給一個物件”,只有少數人會回答出是怕重複建立例項造成多例項共享一個資料物件。更多的人回答是不知道,或者是官方文件要求這麼寫就這麼寫了。

其實這個問題的考點無非就是對vue的熟悉情況,挖掘應聘者的自驅學習能力,對技術的求知慾。這樣的人往往技術成長快,具備很強的獨立解決問題能力。也是各個技術團隊都喜歡的一種人。

首先在vue的原始碼中,有這樣的處理:

  // vue/src/core/instance/state.js
  function initData (vm: Component) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
    if (!isPlainObject(data)) {
      data = {};
      process.env.NODE_ENV !== 'production' && warn(
        'data functions should return an object:\n' +
        'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      );
    }
    ...
  }
複製程式碼

顯然,vue是支援將一個物件作為vue構造引數中data屬性的值並且,如果data是方法的話,也會先取得內部返回的物件結果。並且在vuex中又存在這樣的用法:

  // vuex/src/store.js
  function resetStoreVM (store, state, hot) {
    ...
    const silent = Vue.config.silent
    Vue.config.silent = true
    store._vm = new Vue({
      data: {
        $$state: state
      },
      computed
    })
    ...
  }
複製程式碼

這是怎麼回事呢?既然支援,又不讓我們用,而且當我們在一個vue檔案中,直接給一個data賦予一個物件則會引起紅色警告:

  [Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.
複製程式碼

這個警告來自於Vue原始碼中的vue/src/core/util/options.js

  strats.data = function (
    parentVal: any,
    childVal: any,
    vm?: Component
  ): ?Function {
    if (!vm) {
      if (childVal && typeof childVal !== 'function') {
        process.env.NODE_ENV !== 'production' && warn(
          'The "data" option should be a function ' +
          'that returns a per-instance value in component ' +
          'definitions.',
          vm
        )
        return parentVal
      }
      return mergeDataOrFn(parentVal, childVal)
    }
    return mergeDataOrFn(parentVal, childVal, vm)
  }
複製程式碼

首先我們需要了解在vue檔案的程式碼被例項化成vue元件的過程需要經歷下面這些步驟:

  1. vue檔案被loader處理,template被編譯成render函式,script被編譯成一個物件變數
  2. 將script編譯後的物件傳入render中,並在render函式中呼叫vue.createElement(來自vue/src/core/vdom/create-element.js)構建vue元件
  3. 在createElement中,如果是vue元件的話,通過createComponent(vue/src/core/vdom/create-component.js)構建元件
  4. 將script編譯出來的物件變數通過上下文的$options中取出,並使用Vue.extends(vue/src/core/global-api/extend.js)通過該物件構建出一個新的Vue物件

在4中因為使用了mergeOptions,進而觸發了對data的型別驗證,也就顯示了之初的那個警告。

那麼為一個物件的屬性賦予一個物件真的就會造成共享物件麼?讓我們看下面的程式碼:

  class A {
    constructor(opt) {
      this.opt = opt;
    }

    update() {
      this.opt.data.a++;
    }

    notify() {
      console.log(this.opt);
    }
  }
複製程式碼

我們用這個類來虛擬化Vue的構造。然後進行測試:

  // test
  let c = new A({ data: { a: 1 }});
  let d = new A({ data: { a: 1 }});

  c.update();
  d.update();
  c.notify(); // Object data: a: 2
複製程式碼

我們通過字面量的方式來為構造引數傳入一個物件屬性,然而我們驚奇的發現,其實並沒有發生共享引用的問題。這是什麼鬼?

哦,不對,我們通常在使用vue的時候是在vue檔案中export出一個物件,然後這個物件會在vue-loader的時候被編譯傳入到模版編譯後的render函式中。那麼我們換一個方法來做一個實驗:

  // test.js檔案,用於虛擬vue檔案匯出的vue options物件
  export default {
    data: {
      a: 1
    }
  }
  
  // index.js
  let a = new A(test);
  let b = new A(test);

  a.update();
  b.update();
  a.notify(); // Object data: a: 3
複製程式碼

什麼?在這裡產生了vue文件中提到的共享引用的問題。這是為什麼呢?

原因在於vue的編譯過程以及引入的import過程,通過babel編譯,test.js會被轉化為es5語法的js檔案:

  var Re = {
    data: {
      a: 1
    }
  };
  var Oe = function () {
    function e(t) {
      Object(i["a"])(this, e), this.opt = t
    }
    return Object(o["a"])(e, [{
      key: "update",
      value: function () {
        this.opt.data.a++
      }
    }, {
      key: "notify",
      value: function () {
        console.log(this.opt)
      }
    }]), e
  }(),
  Fe = new Oe(Re),
  Ne = new Oe(Re);
  Fe.update(), Ne.update(), Fe.notify();
  var $e = new Oe({
    data: {
      a: 1
    }
  }),
  Ve = new Oe({
    data: {
      a: 1
    }
  });
  $e.update(), Ve.update(), $e.notify(), 
複製程式碼

What?原來我們的每一個vue檔案經過babel編譯,將匯出的物件直接替換成了一個物件變數,然後將這個變數傳入到對應的元件建構函式中。因此,也就產生了引用共享的問題(所有js物件皆引用)。

由於vue原始碼並沒有通讀,因此如有錯誤請指教

相關文章