一探 Vue 數據響應式原理

一探 Vue 數據響應式原理

本文寫於 2020 年 8 月 5 日html

相信在不少新人第一次使用 Vue 這種框架的時候,就會被其修改數據便自動更新視圖的操做所震撼。react

Vue 的文檔中也這麼寫道:程序員

Vue 最獨特的特性之一,是其非侵入性的響應式系統。數據模型僅僅是普通的 JavaScript 對象。而當你修改它們時,視圖會進行更新。算法

單看這句話,像我這種菜鳥程序員必然是看不懂的。我只知道,在 new Vue() 時傳入的 data 屬性一旦產生變化,那麼在視圖裏的變量也會隨之而變。設計模式

但這個變化是如何實現的呢?接下來讓咱們,一探究竟。數組

1 偷偷變化的 data

咱們先來新建一個變量:let data = { msg: 'hello world' }閉包

接着咱們將這個 data 傳給 Vue 的 data:框架

let data = { msg: 'hello world' }

/*****留空處*****/

new Vue({
  data,
  methods: {
    showData() {
      console.log(data)
    }
  }
})

這看似是很是日常的操做,可是咱們在觸發 showData 的時候,會發現打出來 data 不太對勁:異步

msg: (...)
__ob__: Observer {value: {…}, dep: Dep, vmCount: 1}
get msg: ƒ reactiveGetter()
set msg: ƒ reactiveSetter(newVal)
__proto__: Object

它不只多了不少沒見過的屬性,還把裏面的 msg: hello world 變成了 msg: (...)ide

接下來咱們嘗試在留空處打印出 data,即在定義完 data 以後當即將其打印。

可是很不幸,打印出來依然是上面這個不對勁的值。

但是很明顯,當咱們不去 new Vue(),而且傳入 data 的時候,data 的打印結果絕對不是這樣。

因此咱們能夠嘗試利用 setTimeout()new Vue() 延遲 3 秒執行。

這個時候咱們就會驚訝的發現:

  1. 當咱們在 3s 內點開 console 的結果時,data 是普通的形式;
  2. 當咱們在 3s 後點開 console 的結果時,data 又變成了奇怪的形式。

這說明就是 new Vue() 的過程當中,Vue 偷偷的對 data 進行了修改!正是這個修改,讓 data 的數據,變成了響應式數據。

2 (...) 的由來

爲何好好的一個 msg 屬性會變成 (...) 呢?

這就涉及到了 ES6 中的 getter 和 setter。(若是理解 getter/setter,可跳至下一節)

通常咱們若是須要計算後的值,會定義一個函數,例如:

const obj = {
  number: 5,
  double() {
    return this.number * 2;
  }
};

在使用的時候,咱們寫上 obj.double(obj.number) 便可。

可是函數是須要加括號的,我太懶了,以致於括號都不想要了。

因而就有了 getter 方法:

const obj = {
  number: 5,
  get double() {
    return this.number * 2;
  }
};

const newNumber = obj.double;

這樣一來,就可以不須要括號,就能夠獲得 return 的值。

setter 同理:

const obj = {
  number: 5,
  set double(value) {
    if(this.number * 2 != value;)
    this.number = value;
  }
};

obj.double = obj.number * 2;

由此咱們能夠看出:經過 setter,咱們能夠達到給賦值設限的效果,例如這裏我就要求新值必須是原值的兩倍才能夠。

但常常的,咱們會用 getter/setter 來隱藏一個變量

好比:

const obj = {
  _number: 5,
  get number() {
    return this._number;
  },
  set number(value) {
    this._number = value;
  }
};

這個時候咱們打印出 obj,就會驚訝的發現 (...) 出現了:

number: (...)
_number: 5

如今咱們明白了,Vue 偷偷作的事情,就是把 data 裏面的數據全變成了 getter/setter。

3 利用 Object.defineProperty() 實現代理

這個時候咱們想一個問題,原來咱們能夠經過 obj.c = 'c'; 來定義 c 的值——即便 c 自己不在 obj 中。

但如何定義一個 getter/setter 呢?答:使用 Object.defineProperty()

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

例如咱們上面寫的 obj.c = 'c';,就能夠經過

const obj = {
  a: 'a',
  b: 'b'
}
Object.defineProperty(obj, 'c', {
  value: 'c'
})

Object.defineProperty() 接收三個參數:第一個是要定義屬性的對象;第二個是要定義或修改的屬性的名稱或 Symbol;第三個則是要定義或修改的屬性描述符。

在第三個參數中,能夠接收多個屬性,value 表明「值」,除此以外還有 configurable, enumerable, writable, get, set 一共六個屬性。

這裏咱們只看 getset

以前咱們說了,經過 getter/setter 咱們能夠把不想讓別人直接操做的數據「藏起來」。

但是本質上,咱們只是在前面加了一個 _ 而已,直接訪問是能夠繞過咱們的 getter/setter 的!

那麼咱們怎麼辦呢?

利用代理。這個代理不是 ES6 新增的 Proxy,而是設計模式的一種。

咱們剛剛爲何能夠去修改咱們「藏起來」的屬性值?

由於咱們知道它的名字呀!若是我不給他名字,天然別人就不可能修改了。

例如咱們寫一個函數,而後把數據傳進去:

proxy({ a: 'a' })

這樣一來咱們的 { a: 'a' } 就根本沒有名字了,無從改起!

接下來咱們在定義 proxy 函數時,能夠新建一個空對象,而後遍歷傳入的值,分別進行 Object.defineProperty()將傳入的對象的 keys 做爲 getter/setter 賦給新建的空對象

最後,咱們 return 這個對象便可。

let data = proxy({
  a: 'a',
  b: 'b'
});

function proxy(data) {
  const obj = {};
  const keys = Object.keys(data);
  for (let i = 0; i < keys.length; i++) {
    Object.defineProperty(obj, keys[i], {
      get() {
        return data[keys[i]];
      },
      set(value) {
        if (value < 0) return;
        data[keys[i]] = value;
      }
    });
  }
  return obj;
}

這樣一來,咱們一開始聲明的 data,就是咱們 return 的對象了。在這個對象裏,沒有原始的數據,別人沒法繞過 getter/setter 進行操做!

可是每每並無這麼簡單,若是我必定須要一個變量名呢?

const sourceData = {
  a: 'a',
  b: 'b'
};

let data = proxy(sourceData);

如此一來,經過直接操做 sourceData.a,時能夠直接繞過咱們在 proxy 中設置的 set a 進行賦值的。這個時候咱們怎麼處理?

很簡單嘛,當咱們遍歷傳入的數據時,咱們能夠對傳入的數據新增 getter/setter,此後原始的數據就會被 getter/setter 所替代

在剛剛的代碼中,咱們在循環的剛開始添加這樣一段代碼:

for(/*......*/) {
  const value = data[keys[i]];
  Object.defineProperty(data, keys[i], {
    get() {
      return value;
    },
    set(newValue) {
      if (newValue < 0) return;
      value = newValue;
    }
  });
  /*......*/
}

這是什麼意思呢?

咱們利用了閉包,將原始值單獨拎出來,每一次對原始屬性進行讀寫,其實都是 get 和 set 在讀取閉包時被拎出來的值。

那麼無論別人是操做咱們的 let data = proxy(sourceData); 的 data,仍是操做 sourceData,都會被咱們的 getter/setter 所攔截。

4 回到 Vue

咱們剛剛寫的代碼是這樣的:

let data = proxy({
  a: 'a'
})

function proxy(data) {

}

那若是我改爲這樣呢:

let data = proxy({
  data: {
    a: 'a'
  }
})

function proxy({ data }) {
  // 結構賦值
}

是否是和 Vue 就很是很是像了!

const vm = new Vue({ data: {} }) 也是讓 vm 成爲 data 的代理,而且就算你從外部將數據傳給 data,也會被 Vue 所捕捉。

而在每一次捕獲到你操做數據以後,就會對須要改變的 UI 進行從新渲染。

同理,Vue 對 computed 和 watch 也存在着各類偷偷的處理。

5 Vue 數據響應式的 Bug

若是咱們的數據是這樣:

data: {
  obj: {
    a: 'a'
  }
}

咱們在 Vue 的 template 裏卻寫了

{{ obj.b }}

會怎樣?

Vue 對於不存在或者爲 undefined 和 null 的數據是不予以顯示的。可是當咱們往 obj 中新增 b 的時候,他會顯示嗎?

寫法一:

const vm = new Vue({
  data: {
    obj: {
      a: 'a'
    }
  },
  methods: {
    changeObj() {
      this.obj.b = 'b';
    }
  }
})

咱們能夠給一個按鈕綁定 changeObj 事件,可是很遺憾,這樣並不能使視圖中的 obj.b 顯示出來。

回想一下剛剛咱們對於數據的處理,是否是隻遍歷了外層?這就是由於 Vue 並無對 b 進行監聽,他根本不知道你的 b 是如何變化的,天然也就不會去更新視圖層了。

寫法 2:

const vm = new Vue({
  data: {
    obj: {
      a: 'a'
    }
  },
  methods: {
    changeObj() {
      this.obj.a = 'a2'
      this.obj.b = 'b';
    }
  }
})

咱們僅僅只是新增了一行代碼,在改變 b 以前先改變了 a,竟然就讓 b 實現了更新!

這是爲何?

由於視圖更新實際上是異步的。

當咱們讓 a'a' 變成 'a2' 時,Vue 會監聽到這個變化,可是 Vue 並不能立刻更新視圖,由於 Vue 是使用 Object.defineProperty() 這樣的方式來監聽變化的,監聽到變化後會建立一個視圖更新任務到任務隊列裏。

因此在視圖更新以前,要先把餘下的代碼運行完才行,也就是會運行 b = 'b'

最後等到視圖更新的時候,因爲 Vue 會去作 diff 算法,因而 Vue 就會發現 a 和 b 都變了,天然會去更新相對應的視圖。

可是這並非咱們解決問題的辦法,寫法 2 充其量只能算是「反作用」。

Vue 其實提供了方法讓咱們來新增之前沒有生命的屬性:Vue.set() 或者 this.$set()

Vue.set(this.obj, 'b', 'b'); 會代替咱們進行 obj.b = 'b';,而後監聽 b 的變化,觸發視圖更新。

那數組怎麼響應呢?

每當咱們往數組裏新增元素的時候,數組就在不斷的變長。對於沒有聲明的數組下標,很明顯 Vue 不會給予監聽呀。

好比 a: [1, 2, 3],當我新增一個元素,讓 a === [1, 2, 3, 4] 的時候,a[3] 是不會被監聽的

總不能每次 push 數組,都要手寫剛剛說的 Vue.set 方法吧。

可實際操做中,咱們發現並無呀,Vue 監聽了新增的數據。

這是由於 Vue 又偷偷的幹了一件事兒,它把你本來的數組方法給改了一些。

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

在 Vue 中的數組所帶的這七個方法都不是原生的方法了。Vue 考慮到這些操做極爲經常使用,所在中間爲咱們添加了監聽。

講到這裏,相信你們對 Vue 的響應式原理應該有了更深的認識了。

(完)

相關文章
相關標籤/搜索