分析源碼系列 - Vue.set / vm.$set
詳解
文章目錄
作用和概念描述
向響應式對象中添加一個 property,並確保這個新 property 同樣是響應式的,且觸發視圖更新。它必須用於向響應式對象上添加新 property,因爲 Vue 無法探測普通的新增 property (比如 this.myObject.newProperty = ‘hi’)
侷限性
不允許動態添加根級響應式屬性。比如:
// 錯誤寫法
this.$set(this, 'newkey', 1111)
// 正確寫法
this.$set(this.obj, 'newkey', 111)
// 取值: this.obj.newkey => 111
小結
Vue.set
和vm.$set
方法其實都是同一個,只是寫法上不太一樣- 效果都是爲頁面動態添加屬性,並且動態添加的屬性也是響應式的屬性
爲什麼用過 set 添加的就是響應式屬性
先看下 vue 響應式原理:vue 雙向數據綁定原理
可以看到響應式其實依賴於一個 Object.defineProperty
而 Object.defineProperty
只監聽某個對象下的一個屬性,如果有多個屬性需要分別監聽
看個 demo 理解下:
var data = {}
Object.defineProperty(data, 'data1', {
get: function() {
console.log('get data1')
return this.value
},
set: function(newVal) {
console.log('set data1')
this.value = newVal
}
})
data.data1 = 111 // 將會打印 set data1
console.log(data.data1) // 先打印 get data1 然後纔是 111
data.data2 = 222 // 無打印,無報錯
console.log(data.data2) // 直接打印222。表示沒有進過 `Object.defineProperty`
開始看源碼
來一個調試的 demo
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
addData: {}
}
})
debugger
app.$set(app.addData, 'newkey', 1111)
</script>
第一個進入 $set
方法的時候,默認先來到了取值部分,這時候 return 了一個空值,在 return 後,再次進入 $set
纔是我們需要調試的部分
調試模式下,進入 set 方法
1081 行 2 個方法用於檢測目標節點的數據類型,也就是 $set
的第一個參數的檢測
- 1085 - 1089 判斷的是數組類型,畢竟由於特殊的類型
Object.defindPropety
無法檢測數組的變化,所以數組變化是手動更新的。 - 1090 - 1093 便是判斷屬性是否原先就已經存在了,存在就沒必要重複監聽了
- 1095 - 1100 就是判斷不能在根節點
this.data
直接去添加數據了 - 注意 1094 行變量
ob
就是 observed(觀察者) 的簡稱。也是用於判斷該變量是否已經被監聽過了 - 監聽過無須重複重複監聽的值都會直接返回對應的
val
。這也是$set
返回當前值的一個 return - 如果是新增的對象,那就走到了 1106 行
defineReactive$$1
方法中
添加監聽 defineReactive$$1
方法
- 1021 行 初始化
new Dep
查看 dep 的分析 看 dep 分析還是很有必要的。這涉及後面許多的流程 csdn 不支持 哈希導航,需要手動自己滑下去看下~ - 1023 - 1026 判斷該對象是否可讀寫
- 1019 - 1033 獲取目標對象上的
get
和set
方法. - 1035 有個 shallow 變量,在我們前面的代碼中這個變量是不存在的
!shallow
即爲 true。所以執行了observe
方法 查看 observe 分析 csdn 不支持 哈希導航,需要手動自己滑下去看下~ - 看完上面的
observe
分析 我們知道了observe
就是爲他的子屬性,循環添加監聽的。 - 1039 行 綁定了獲取屬性值的事件,在獲取對應的值的時候,
Dep.target
剛纔也看到了,是一個全局的觀察者watch
。那如果存在dep.target
爲當前的對象調用一個depend
方法,這個方法是從Dep
繼承過來的。相當於也是爲 watch 註冊一個回調事件把(這估計是爲watch
和computed
埋下的一個伏筆) - 1052 行開始
set
方法 。也是先從自身的getter
裏面獲取當前的值 - 1069 行,如果我們新複製的是一個對象,他還得循環重新爲這些對象添加一個數據挾持,如果已經挾持過的就可以跳過,也就是上面的代碼了
- 最後 1070 行,set 後,調用了
dep.notify()
。這裏面存儲對應對象更新需要觸發的時間(訂閱觀察者模式)。既然值更新了,就觸發給相關的訂閱函數 (watch 也是這時候觸發了,視圖也是這時候更新了)。而且我們前面有拿到 set 之前的值,所以watch
方法裏面newVal
和oldVal
就是這時候被記錄下來的 - 最終就返回當前的值,
$set
方法也就執行結束了
dep 分析
dep 代碼不是很長。dep 就相當於是一個訂閱中心
- 717 行 可以看到每一個 dep 都有對應的 ID,並且自增的
- 718 行可以看做是一個事件中心,所有的監聽都存儲到了這裏
- 可以看到 dep 原型上有幾個方法
addSub
removeSub
depend
notify
。都是用於操作對應的監聽,添加/刪除,找到對應的依賴,通知這幾個方法 - 725 行
Dep.target
有非常詳細的註釋,全局唯一的觀察者
最核心的就是記住幾個 addSub
removeSub
depend
notify
方法。然後接着剛纔的代碼繼續看
observe 分析
尤大貼心的註釋
Attempt to create an observer instance for a value,
嘗試爲值創建觀察者實例
returns the new observer if successfully observed,
如果成功觀察到,則返回新的觀察者
or the existing observer if the value already has one.
或現有的觀察者(如果值已包含一個)
-
看來
value.__ob__
如果存在,那這個屬性就已經有觀察者了,這也是我們$set
第一步中的一個判斷,判斷__ob__
的 -
然後區分了數組的,數組並沒有觀察者
-
主要看 1003 行,創建一個新的觀察者
new Observe
注意是大寫的,不是當前的對象了 -
new Observe
在下面圖二,就不分開講了 -
926 行。爲
__ob__
添加屬性,不要搞錯了,並且把__ob__
設置爲不可枚舉類型。具體可以看下 926 行進去的代碼 -
927-933 都是爲數組的操作了
-
935 行,當前對象的 walk 方法。直接貼上代碼,可以看到是一個循環,把我們的對象遍歷了一次,並且爲每個對象都調用了
defineReactive$$1
。可以看到備註,只有值是對象類型,纔會調用這個方法。 -
既然調用了
defineReactive$$1
。那這裏就形成了一個遞歸,遞歸的頭部,就是當屬性,不再是對象的時候,就停止調用defineReactive$$1
。 -
看到這裏之後,下一步應該回到 添加監聽 defineReactive$$1 方法 的 第 1035 行
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
Observer.prototype.walk = function walk(obj) {
var keys = Object.keys(obj)
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i])
}
}
總結
-
vue 響應式原理依賴於
Object.defindPropety
的get
和set
方法,分別在這 2 個方法去觸發對應的事件 -
由於 JS 和
Object.defindPropety
的限制,以至於不能動態添加需要監聽的屬性,所以就要用到Vue.set()
方法 -
Vue.set()
方法內部是一個循環處理的過程,如果當前新增監聽的是一個對象,那就繼續調用自己形成一個遞歸,直到最後的子屬性是一個數組/非對象類型
的參數後,遞歸結束,然後爲自己添加監聽,在監聽中又會觸發其他相關的方法(Dep 中訂閱的事件就會被觸發)。形成我們常見的雙向數據綁定 -
由於
Object.defindPropety
只能監聽對象的變化,所以對於數組內某一個索引的值發生改變也是不能監聽到的,於是還要用到Vue.set
手動去觸發更新,這時候的Vue.set
只會做值的更新,而不會重複新增監聽
原文首發:分析源碼系列 - Vue.set / vm.$set
詳解 這是新的博客地址,感興趣可以看看