我們上文說了,Vue 是通過 Object.defineProperty 和重寫數組的原型方法來達到監控數據的目的。但是,在某些情況下,上面兩種方案無法做到監控數據的變化,例如:
(1):當我們給對象設置一個新屬性的時候,obj.newProperty = xxxxx;
(2):當我們刪除對象中的某個屬性的時候,delete obj.oldProperty;
上面兩種情況,Vue 的響應式系統都監控不到,為了彌補這兩個缺陷,Vue 提供了 $set 和 $delete API,當我們想設置新的屬性,或者刪除某個屬性的時候,不要用 js 原生的語法操作,而是使用 $set 和 $delete API 來完成任務。
這兩個 API 的思路其實和重寫數組的原型方法是一樣的,都是對 JS 中的某些原生操作進行重寫,當我們調用這些重寫的方法對數據進行操作的時候,Vue 自然就能監控到我們對數據做了哪些事情,進而做相對應的處理就可以了,接下來看源碼。
1,這兩個 API 是如何掛載到 Vue 原型中的
1-1,首先看 src/core/instance/index.js
function Vue (options) { // 如果當前的環境不是生產環境,並且當前命名空間中的 this 不是 Vue 的實例的話, // 發出警告,Vue 必須通過 new Vue({}) 使用,而不是把 Vue 當做函數使用 if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } // 執行 vm 原型上的 _init 方法,該方法在 initMixin 方法中定義 this._init(options) } // 寫入 vm.$set、vm.$delete、vm.$watch stateMixin(Vue) export default Vue
掛載這兩個 API 到 Vue 原型上是在 stateMixin 方法中。
1-2,src/core/instance/state.js ==> stateMixin()
import { set, del } from '../observer/index' export function stateMixin (Vue: Class<Component>) { Vue.prototype.$set = set Vue.prototype.$delete = del }
從上面的代碼可知,$set 和 $del API 的實現定義在 ../observer/index 文件中
2,$set 的實現
Vue.set 或者說是$set 原理如下
因為響應式數據 我們給對象和數組本身都增加了__ob__屬性,代表的是 Observer 實例。當給對象新增不存在的屬性 首先會把新的屬性進行響應式跟蹤 然后會觸發對象__ob__的 dep 收集到的 watcher 去更新,當修改數組索引時我們調用數組本身的 splice 方法去更新數組
/** * vm.$set 的底層實現 */ export function set (target: Array<any> | Object, key: any, val: any): any { // 如果 target 是一個數組,並且 key 也是一個有效的數組索引值的話 if (Array.isArray(target) && isValidArrayIndex(key)) { // 設置數組的 length 屬性,設置的屬性值是 "數組原長度" 和 "key" 中的最大值 target.length = Math.max(target.length, key) // 然后通過數組原型上的方法,將 val 添加到數組中 // 在前面數組響應式源碼的閱讀中可以知道,通過數組原型的方法添加的元素,其是會被轉換成響應式的 target.splice(key, 1, val) return val } // 這里用於處理 key 已經存在於 target 中的情況 if (hasOwn(target, key)) { // 由於這個 key 已經存在於對象中了,也就是說這個 key 已經被偵測了變化,在這里,只不過是修改下屬性而已 // 所以在這里,直接修改屬性,並返回 val 即可 target[key] = val return val } const ob = (target: any).__ob__ // 如果 target 沒有 __ob__ 屬性的話,說明 target 並不是一個響應式的對象 // 所以在這里也不需要做什么額外的處理,將 val 設到 target 上,並且返回這個 val 即可 if (!ob) { target[key] = val return val } // 如果上面所有的判斷條件都不滿足的話,說明用戶是在響應式數據上新增了一個數據,這種情況需要跟蹤這個新增屬性的變化 // 在這里使用 defineReactive 將 val 變成 getter/setter 的形式 defineReactive(ob.value, key, val) // 因為新增了一個屬性,所以 ob.value 變化了,所以在這里需要出發依賴的更新 ob.dep.notify() return val }
3,$delete 的實現
Vue.set 或者說是$set 原理如下
因為響應式數據 我們給對象和數組本身都增加了__ob__屬性,代表的是 Observer 實例。當給對象刪除一個已經存在的屬性 直接觸發對象__ob__的 dep 收集到的 watcher 去更新,當修改數組索引時我們調用數組本身的 splice 方法去更新數組
/** * Delete a property and trigger change if necessary. */ // Vue 對數據的監控是通過 Object.defineProperty() 實現的,所以當用戶通過 delete 關鍵字刪除某個字段時,Vue 是檢測不到的, // 為了解決這個問題,Vue 提供了 vm.$delete 來解決這個問題 export function del (target: Array<any> | Object, key: any) { // 如果 target 是一個數組,並且 key 是一個下標值的話 if (Array.isArray(target) && isValidArrayIndex(key)) { // 執行數組原型上的 splice 方法,該方法會執行刪除的操作,並且會出發依賴的更新 target.splice(key, 1) return } const ob = (target: any).__ob__ // 如果 target 上面沒有 key 屬性的話,直接 return 即可,什么都不用干 if (!hasOwn(target, key)) { return } // 使用 js 中原生的 delete 關鍵字刪除指定的 key delete target[key] // 在這里判斷 target 是不是響應式的,如果不是的話,就不用出發依賴的更新操作了。在這里,直接 return if (!ob) { return } // 出發依賴的更新操作 ob.dep.notify() }