博客 / 詳情

返回

vue2子組件中定義的data為什麼必須是一個function

是什麼

需求:現在我們需要在一個組件中定義一個name變量,並且把這個變量顯示在界面上,代碼:

<template>
  <div>
    {{ name }}
  </div>
</template>
<script>
export default {
  data() {
    return {
      name: 'syz'
    }
  }
}
</script> 

那麼問題來了,為什麼組件中的data需要是一個函數呢,可以將data聲明為一個對象嗎,這樣不是更直接嗎?代碼:

<template>
  <div>
    {{ name }}
  </div>
</template>
<script>
export default {
  data: {
    name: 'syz'
  }
}
</script> 

如果是上面這種寫法,不出意外的話,在vs code data處將會出現紅線報錯 data property in component must be a function,提示我們data必須是一個function。

為什麼

好,重點來了,為什麼呢?原因就是:通過函數返回數據對象,保證了每個組件實例都有一個唯一的數據副本,避免了組件間數據互相影響。

先上結論:

每個子組件註冊後會被處理成一個VueComponent實例,類似下面的MyVueComponent(...), data屬性會掛在他的原型鏈上面

當子組件中的data是一個Object時,代碼:

function MyVueComponent(){
  
}
MyVueComponent.prototype.data = {
  name:'jack',
  age:22,
}
const componentA = new MyVueComponent();
const componentB = new MyVueComponent();
componentA.data.age=55;
console.log(componentA.data.age,componentB.data.age) // 55,55

可以看到,當componentA.data.age賦值為55時,componentB.data.age也被更改為55了

當子組件的data是一個function時,代碼:

function MyVueComponent(){
  this.data = this.data()
}
MyVueComponent.prototype.data = function() {
    return  {
      age:22
    }
}
const componentA = new MyVueComponent();
const componentB = new MyVueComponent();
componentA.data.age=55;
console.log(componentA.data.age,componentB.data.age) // 55,22

可以看到,componentA.data.age更改後,並不會影響到componentB.data.age的值,也就是我們上面説的,避免了組件間數據互相影響

注意: 避免組件中數據相互影響,除了將data定義為function,還需要在data中返回一個全新的Object(擁有獨立的內存地址),我們知道,js中對象的賦值是引用傳遞,代碼:

const myData = {
  age:22
}
function MyVueComponent(){
  this.data = this.data()
}
MyVueComponent.prototype.data = function() {
    return myData
}
const componentA = new MyVueComponent();
const componentB = new MyVueComponent();
componentA.data.age=55;
console.log(componentA.data.age,componentB.data.age) // 55,55

可以看到,雖然我們將data定義成function了,但是在datareturn了對象myData,這裏是引用傳遞,後續在修改data中的值時,其實修改的是同一個內存地址的數據,所以還是存在數據相互影響了。

再看細節:

當我們需要在一個父組件中使用子組件時,以全局子組件為例子,代碼:

// app.vue
<template>
  <div id="app">
    {{name}}
  </div>
</template>
<script>
export default {
  name: 'App',
  data() {
    return {
      name: 'haha'
    }
  }
}
</script>
// main.js
import Vue from 'vue/dist/vue.esm.js'
import App from './App.vue'
Vue.component('app', App)
new Vue({
  el: '#app',
  template: '<app></app>'
})

首先,執行Vue.component(...)註冊子組件

調用Vue.component()這個方法,看一下Vue.component()的內部實現:

var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];
function initAssetRegisters (Vue) {
  ASSET_TYPES.forEach(function (type) {
    Vue[type] = function (
      id,
      definition
    ) {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id);
        }
        // 關鍵邏輯
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id;
          definition = this.options._base.extend(definition);
        }
        // ...略去部分無關代碼
        this.options[type + 's'][id] = definition;
        return definition
      }
    };
  });
}

Vue初始化時會執行initAssetRegisters(...)這個方法,那麼Vue.component(...)就出來了。

definition參數就是我們在上面例子中調用Vue.component(...)傳進來的第二個參數,也就是:

{
  name: 'App',
  data() {
    return {
      name: 'haha'
    }
  }
}

接着會執行definition = this.options._base.extend(definition),會調用Vue.extend(...)將外部傳進來的對象經過各種處理,最後構造出一個VueComponent的實例對象,當然,我們傳進來的data對象也掛在裏面,在瀏覽器debugger看下過程數據會比較清楚。代碼:

Vue.extend = function (extendOptions) {
    extendOptions = extendOptions || {};
    var Super = this;
    var SuperId = Super.cid;
    var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }
    var name = extendOptions.name || Super.options.name;
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name);
    }
    var Sub = function VueComponent (options) {
      this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.cid = cid++;
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    );
    Sub['super'] = Super;
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type];
    });
    if (name) {
      Sub.options.components[name] = Sub;
    }
    Sub.superOptions = Super.options;
    Sub.extendOptions = extendOptions;
    Sub.sealedOptions = extend({}, Sub.options);
    cachedCtors[SuperId] = Sub;
    return Sub
};

接着,執行new Vue(...)

代碼:

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

執行順序:this._init(...) -> initState(vm) ,也就是在initState(vm) 這個方法中會去初始化組件中data數據,看下代碼:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
}

看下initData(...)方法的內部實現:

function initData (vm: Component) {
  let 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
    )
  }
  // ...略去部分無關代碼
}

可以看到,在第二行代碼,會先判斷data是不是一個function
如果是function,接着調用getData(...)方法獲取數據。
如果不是function,就直接獲取組件中的data數據。

那什麼情況下data不是一個function呢?
當我們使用new Vue(...) 實例的組件(一般是根節點)是不會被複用的。
因此也就不存在組件中數據相互影響的問題了。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.