深入響應式原理


目錄


#如何追蹤變更


當你把一個普通的JavaScript 物件傳入Vue 實例作為data 選項,Vue 將遍歷此物件所有的屬性,並使用Object.defineProperty 把這些屬性全部轉為getter/setterObject.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是 Vue 不支援 IE8 以及更低版本瀏覽器的原因。

這些 getter/setter 對使用者來說是看不見的,但是在內部它們讓 Vue 能夠追蹤依賴,在屬性被存取和修改時通知變更。這裡要注意的是不同瀏覽器在控制台列印資料物件時對 getter/setter 的格式化並不同,所以建議安裝 vue-devtools 來取得對檢查資料更友善的使用者介面。

每個元件實例都對應一個 watcher 實例,它會在元件渲染的過程中把「接觸」過的資料屬性記錄為依賴。之後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的元件重新渲染。

1568871314181366.png


檢測變化的注意事項


#受現代JavaScript 的限制(而且Object.observe 也已經被廢棄),Vue 無法偵測到物件屬性的新增或刪除。由於 Vue 會在初始化實例時對屬性執行 getter/setter 轉化,所以屬性必須在 data 物件上存在才能讓 Vue 將它轉換為響應式的。例如:

var vm = new Vue({
  data:{
    a:1
  }
})

// `vm.a` 是响应式的

vm.b = 2
// `vm.b` 是非响应式的

對於已建立的實例,Vue 不允許動態新增根層級的響應式屬性。但是,可以使用 Vue.set(object, propertyName, value) 方法來為嵌套物件新增響應式屬性。例如,對於:

Vue.set(vm.someObject, 'b', 2)

您也可以使用 vm.$set 實例方法,這也是全域 Vue.set 方法的別名:

this.$set(this.someObject,'b',2)

有時你可能需要為已有物件賦值多個新屬性,例如使用 Object.assign()_.extend()。但是,這樣新增到物件上的新屬性不會觸發更新。在這種情況下,你應該用原始物件與要混合進去的物件的屬性一起建立一個新的物件。

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

也有一些陣列相關的注意事項,之前已經在清單渲染中講過。


聲明響應式屬性


#由於Vue 不允許動態新增根級響應式屬性,所以你必須在初始化實例前宣告所有根級響應式屬性,哪怕只是一個空值:

var vm = new Vue({
  data: {
    // 声明 message 为一个空值字符串
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'

如果你未在data 選項中宣告 message,Vue 將警告你渲染函數正在試圖存取不存在的屬性。

這樣的限制在背後是有其技術原因的,它消除了在依賴項追蹤系統中的一類邊界情況,也使 Vue 實例能更好地配合類型檢查系統工作。但同時在程式碼可維護性方面也有一點重要的考量:data 物件就像元件狀態的結構 (schema)。事先聲明所有的響應式屬性,可以讓元件程式碼在未來修改或給其他開發人員閱讀時更易於理解。


非同步更新佇列


#可能你還沒注意到,Vue 在更新DOM時是異步執行的。只要偵聽到資料變化,Vue 將開啟一個佇列,並緩衝在同一事件循環中發生的所有資料變更。如果同一個 watcher 被多次觸發,只會被推入到佇列中一次。這種在緩衝時去除重複資料對於避免不必要的計算和 DOM 操作是非常重要的。然後,在下一個的事件循環「tick」中,Vue 刷新佇列並執行實際 (已去重的) 工作。 Vue 在內部對非同步佇列嘗試使用原生的Promise.thenMutationObserversetImmediate,如果執行環境不支持,則會採用setTimeout( fn, 0) 代替。

例如,當你設定 vm.someData = 'new value',該元件不會立即重新渲染。當刷新佇列時,元件會在下一個事件循環“tick”中更新。多數情況我們不需要關心這個過程,但是如果你想基於更新後的 DOM 狀態來做點什麼,這可能會有些棘手。雖然 Vue.js 通常鼓勵開發人員使用「資料驅動」的方式思考,避免直接接觸 DOM,但有時我們必須這麼做。為了在資料變更之後等待 Vue 完成更新 DOM,可以在資料變更之後立即使用 Vue.nextTick(callback)。這樣回調函數將在 DOM 更新完成後被呼叫。例如:

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

在元件內使用 vm.$nextTick() 實例方法特別方便,因為它不需要全域 Vue,且回呼函數中的 this#將自動綁定到目前的Vue 實例:

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})

因為$nextTick() 傳回一個Promise 對象,所以你可以使用新的ES2016 async/await 語法完成相同的事情:

methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}