Vue-js 源碼解讀系列——this 直接調用數據源

前言

今天看到一篇文章:爲何 Vue 實例化後,經過 this.能獲取到 data 內的數據javascript

一下是這篇文章的結論:vue

不賣關子啦!其實 Vue 實例化的時候,經過 getData 方法中 call 修改 this 指針來實現 Vue 實例後能獲取到 data 體內的數據,小夥伴可能感受認爲我將 vue 初始化部分流程拉出來是的你愈來愈糊塗,可是若是我不講上面的代碼拉出來,我直接來這句總結,你會更加懵的 🙂🙂🙂java

這篇文章根本沒有說清楚,或者說根本就說錯了。可能不知道誤導了多少人,並且網上對於該問題的解釋都一致的看向源碼中的:git

return data.call(vm, vm);
複製代碼

圖片截至:Vue 源碼解析:this.$data、this._data、this.xxx 爲何都能獲取數據?data 爲何是個函數?github

然而實現該功能的其實並非這句話!!!api

然而實現該功能的其實並非這句話!!!瀏覽器

然而實現該功能的其實並非這句話!!!函數

(重要的事情說三遍!)oop

源碼分析

首先得從 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(options)

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    ...
    initState(vm)
  	...
}
複製代碼

在 Vue.prototype._init() 中,要注意的是:

  • 先將當前對象保存爲了一個常量 vm
  • 其次設置了 vm.$options 屬性,寫入了向 Vue 對象建立時傳入的 options 對象(這裏不作詳解)
  • 而且經過 initState(vm)作相關初始化操做
export function initState (vm: Component) {
	...
  const opts = vm.$options
	...
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
	...
}
複製代碼

在這段代碼中,若是 vm.$options.data(也就是咱們的數據源)若是有數據,咱們就進行 initData(vm)操做,傳入的 vm 即當前 Vue 對象!

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
	...
  // proxy data on instance
  const keys = Object.keys(data)
	...
  let i = keys.length
  while (i--) {
    const key = keys[i]
    ...
    proxy(vm, `_data`, key)
  }
	...
}
複製代碼

值得注意的是:

咱們實例化 Vue 數據源的時候寫法經常是:

data: function() {
  return {
    key: value,
    ...
  }
}
複製代碼

那麼這時候咱們就會走向data = vm._data = getData(data, vm)

export function getData (data: Function, vm: Component): any {
	...
  return data.call(vm, vm)
	...
}
複製代碼

這裏咱們就要理解一下,data.call(vm, vm)到底幹了啥?

對於 call 的深刻理解建議閱讀一下:對於 Function.call()的深刻理解

這裏我將簡單地進行解釋:

data.call(vm, vm)
// 實際上與是以以下方式運行的
vm.data(vm)

//假如咱們有以下數據源
data: function() {
  return {
    content: "Hello World!"
  }
}
//那麼data.call(vm, vm)執行的實際上就是獲得了return中的對象
{
  content: "Hello World!"
}
複製代碼

data.call(vm, vm) 並無實現咱們想要的功能。

data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
複製代碼

這段代碼的意思其實是:不管咱們定義的 data 是一個 function 仍是一個{},最終都將返回一個{}而且賦值給 data 這個變量!

**值得注意:**vm._data 就是返回的{},能夠在控制的進行打印輸出看一看,和 vm.$data 的值是同樣的!

那麼真正的實如今哪呢?

咱們看到 initData()函數的後半段代碼:

const keys = Object.keys(data)
...
let i = keys.length
while (i--) {
  const key = keys[i]
  ...
  proxy(vm, `_data`, key)
}
複製代碼

這裏遍歷了咱們以前所取到的 data,對其中每個屬性都進行了proxy(vm,_data, key)調用。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
};

export function proxy(target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key];
  };
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
複製代碼

理解這段代碼,這裏得先普及一個知識點:

Object.defineProperty()

方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。

經過 defineProperty()實際上就是給 Vue 實例定義了新的屬性!!!

這纔是重點,這裏經過遍歷 data,將 data 中的屬性/值綁定在了 vm(也就是 Vue 實例)上。

在 proxy()中的傳入的第二個參數_data,實際上爲了後面this[sourceKey][key]調用 vm._data(前面有提)中的的屬性/值。

這裏經過 vm 上建立新屬性,而且採用 set,get 屬性描述符使得每一次改變數據都會直接做用於 vm._data 之上。

結語

其實今天的話題在 Vue.js 的文檔就有解釋:

data

  • 類型Object | Function

  • 限制:組件的定義只接受 function

  • 詳細

    Vue 實例的數據對象。Vue 將會遞歸將 data 的屬性轉換爲 getter/setter,從而讓 data 的屬性可以響應數據變化。對象必須是純粹的對象 (含有零個或多個的 key/value 對):瀏覽器 API 建立的原生對象,原型上的屬性會被忽略。大概來講,data 應該只能是數據 - 不推薦觀察擁有狀態行爲的對象。

    一旦觀察過,不須要再次在數據對象上添加響應式屬性。所以推薦在建立實例以前,就聲明全部的根級響應式屬性。

    實例建立以後,能夠經過 vm.$data 訪問原始數據對象。Vue 實例也代理了 data 對象上全部的屬性,所以訪問 vm.a 等價於訪問 vm.$data.a

    _$ 開頭的屬性 不會 被 Vue 實例代理,由於它們可能和 Vue 內置的屬性、API 方法衝突。你可使用例如 vm.$data._property 的方式訪問這些屬性。

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

    若是須要,能夠經過將 vm.$data 傳入 JSON.parse(JSON.stringify(...)) 獲得深拷貝的原始數據對象。

因此說,遇到問題要多看文檔,實在不行再看源碼!

-EFO-


筆者專門在 github 上建立了一個倉庫,用於記錄平時學習全棧開發中的技巧、難點、易錯點,歡迎你們點擊下方連接瀏覽。若是以爲還不錯,就請給個小星星吧!👍


2019/05/27

AJie

相關文章
相關標籤/搜索