Vue.set的使用和原理(分析源碼系列)

分析源碼系列 - Vue.set / vm.$set 詳解

作用和概念描述

官方文檔:Vue-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.setvm.$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 獲取目標對象上的 getset 方法.
  • 1035 有個 shallow 變量,在我們前面的代碼中這個變量是不存在的 !shallow 即爲 true。所以執行了 observe 方法 查看 observe 分析 csdn 不支持 哈希導航,需要手動自己滑下去看下~
  • 看完上面的 observe 分析 我們知道了 observe 就是爲他的子屬性,循環添加監聽的。
  • 1039 行 綁定了獲取屬性值的事件,在獲取對應的值的時候, Dep.target 剛纔也看到了,是一個全局的觀察者watch。那如果存在 dep.target 爲當前的對象調用一個 depend 方法,這個方法是從 Dep 繼承過來的。相當於也是爲 watch 註冊一個回調事件把(這估計是爲 watchcomputed 埋下的一個伏筆)
  • 1052 行開始 set 方法 。也是先從自身的 getter 裏面獲取當前的值
  • 1069 行,如果我們新複製的是一個對象,他還得循環重新爲這些對象添加一個數據挾持,如果已經挾持過的就可以跳過,也就是上面的代碼了
  • 最後 1070 行,set 後,調用了 dep.notify()。這裏面存儲對應對象更新需要觸發的時間(訂閱觀察者模式)。既然值更新了,就觸發給相關的訂閱函數 (watch 也是這時候觸發了,視圖也是這時候更新了)。而且我們前面有拿到 set 之前的值,所以 watch 方法裏面newValoldVal就是這時候被記錄下來的
  • 最終就返回當前的值,$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.defindPropetygetset 方法,分別在這 2 個方法去觸發對應的事件

  • 由於 JS 和 Object.defindPropety的限制,以至於不能動態添加需要監聽的屬性,所以就要用到 Vue.set()方法

  • Vue.set() 方法內部是一個循環處理的過程,如果當前新增監聽的是一個對象,那就繼續調用自己形成一個遞歸,直到最後的子屬性是一個數組/非對象類型的參數後,遞歸結束,然後爲自己添加監聽,在監聽中又會觸發其他相關的方法(Dep 中訂閱的事件就會被觸發)。形成我們常見的雙向數據綁定

  • 由於 Object.defindPropety 只能監聽對象的變化,所以對於數組內某一個索引的值發生改變也是不能監聽到的,於是還要用到Vue.set 手動去觸發更新,這時候的Vue.set只會做值的更新,而不會重複新增監聽

原文首發:分析源碼系列 - Vue.set / vm.$set 詳解 這是新的博客地址,感興趣可以看看

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章