Vue(ES6)中的data屬性爲何不能是一個對象?

如下引官網原文:當一個組件被定義,data 必須聲明爲返回一個初始數據對象的函數,由於組件可能被用來建立多個實例。若是 data 仍然是一個純粹的對象,則全部的實例將共享引用同一個數據對象!經過提供 data 函數,每次建立一個新實例後,咱們可以調用 data 函數,從而返回初始數據的一個全新副本數據對象。html

最近來面試的不少人。我都會問這個問題「vue中,爲何data是一個方法返回一個對象,而不是直接賦給一個對象」,只有少數人會回答出是怕重複建立實例形成多實例共享一個數據對象。更多的人回答是不知道,或者是官方文檔要求這麼寫就這麼寫了。vue

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

首先在vue的源碼中,有這樣的處理:vuex

// 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中又存在這樣的用法:api

// 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賦予一個對象則會引發紅色警告:babel

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

這個警告來自於Vue源碼中的vue/src/core/util/options.jsdom

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組件的過程須要經歷下面這些步驟:ide

  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源碼並無通讀,所以若有錯誤請指教

相關文章
相關標籤/搜索