1. 程式人生 > >從vue源碼看Vue.set()和this.$set()

從vue源碼看Vue.set()和this.$set()

time his 觸發 inf mbo 原型 ins 處理對象 ()

前言

最近死磕了一段時間vue源碼,想想覺得還是要輸出點東西,我們先來從Vue提供的Vue.set()和this.$set()這兩個api看看它內部是怎麽實現的。

Vue.set()和this.$set()應用的場景

平時做項目的時候難免不會對數組或者對象進行這樣的騷操作操作,結果發現,咦~~,他喵的,怎麽頁面沒有重新渲染。

const vueInstance = new Vue({
  data: {
    arr: [1, 2],
    obj1: {
        a: 3
    }
  }
});

vueInstance.$data.arr[0] = 3;  // 這種騷操作頁面不會重新渲染
vueInstance.$data.obj1.b = 3;  // 這種騷操作頁面不會重新渲染

查了一下官方文檔,發現人家早就說過這種情況

Vue.set()向響應式對象中添加一個屬性,並確保這個新屬性同樣是響應式的,且觸發視圖更新。它必須用於向響應式對象上添加新屬性,因為 Vue 無法探測普通的新增屬性 (比如 this.myObject.newProperty = ‘hi‘)

所以按照官網的寫法,我們應該使用下面這種寫法:

Vue.set(vueInstance.$data.arr, 0, 3);  // 這樣操作數組可以讓頁面重新渲染
vueInstance.$set(vueInstance.$data.arr, 0, 3); // 這樣操作數組也可以讓頁面重新渲染
Vue.set(vueInstance.$data.obj1, b, 3);  // 這樣操作對象可以讓頁面重新渲染
vueInstance.$set(vueInstance.$data.obj1, b, 3); // 這樣操作對象也可以讓頁面重新渲染

Vue.set()和this.$set()實現原理

是時候看一波這兩個api的源碼了,我們先來看看Vue.set()的源碼:

import { set } from ‘../observer/index‘

...
Vue.set = set
...

再來看看this.$set()的源碼:

import { set } from ‘../observer/index‘

...
Vue.prototype.$set = set
...

結果我們發現Vue.set()和this.$set()這兩個api的實現原理基本一模一樣,都是使用了set函數。set函數是從 ../observer/index 文件中導出的,區別在於Vue.set()是將set函數綁定在Vue構造函數上,this.$set()是將set函數綁定在Vue原型上。

接下來我們根據 ../observer/index 中找出set函數:

function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== ‘production‘ &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== ‘production‘ && warn(
      ‘Avoid adding reactive properties to a Vue instance or its root $data ‘ +
      ‘at runtime - declare it upfront in the data option.‘
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

我們發現set函數接收三個參數分別為 target、key、val,其中target的值為數組或者對象,這正好和官網給出的調用Vue.set()方法時傳入的參數參數對應上。如下圖所示:
技術分享圖片

我們接著往下看:

if (process.env.NODE_ENV !== ‘production‘ &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }

我們先看isUndef和isPrimitive方法,從名字就可以看出,isUndef是判斷target是不是等於undefined或者null。isPrimitive是判斷target的數據類型是不是string、number、symbol、boolean中的一種。所以這裏的意思是如果當前環境不是生產環境並且 isUndef(target) || isPrimitive(target) 為真的時候,那麽就拋出錯誤警告。

數組的實現原理

接著向下看:

if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }

這裏實際就是修改數組時調用set方法時讓我們能夠觸發響應的代碼,不過在分析這段代碼之前我們來看看vue中的數組實際上是長什麽樣的。下圖分別是vue中的數組和普通的js數組:
技術分享圖片

技術分享圖片

vue中的數組我們命名為arrVue,js中的普通數組命名為arrJs。其實我們平時調用普通數組的push、pop等方法是調用的Array原型上面定義的方法, 從圖中我們可以看出arrJs的原型是指向Array.prototype,也就是說 arrJs.__proto__ == Array.prototype

但是在vue的數組中,我們發現arrVue的原型其實不是指向的Array.prototype,而是指向的一個對象(我們這裏給這個對象命名為arrayMethods)。arrayMethods上面只有7個push、pop等方法,並且arrayMethods的原型才是指向的Array.prototype。所以我們在vue中調用數組的push、pop等方法時其實不是直接調用的數組原型給我們提供的push、pop等方法,而是調用的arrayMethods給我們提供的push、pop等方法。vue為什麽要給數組的原型鏈上面加上這個arrayMethods呢?這裏涉及到了vue的數據響應的原理,我們這篇文章暫時不談論數據響應原理的具體實現。這裏你可以理解成vue在arrayMethods對象中做過了特殊處理,如果你調用了arrayMethods提供的push、pop等7個方法,那麽它會觸發當前收集的依賴(這裏收集的依賴可以暫時理解成渲染函數),導致頁面重新渲染。換句話說,對於數組的操作,我們只有使用arrayMethods提供的那7個方法才會導致頁面渲染,這也就解釋了為什麽我們使用 vueInstance.$data.arr[0] = 3;時不會導致頁面出現渲染。

搞清楚vue中的數組具體是怎麽實現了之後,我們再來看上面的代碼:

if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }

首先if判斷當前target是不是數組,並且key的值是有效的數組索引。然後將target數組的長度設置為target.length和key中的最大值,這裏為什麽要這樣做呢?是因為我們可能會進行下面這種騷操作:

arr1 = [1,3];
Vue.set(arr1,10,1)  // 如果不那樣做,這種情況就會出問題

接著向下看,我們發現這裏直接調用了target.splice(key, 1, val),在前面我們說過調用arrayMethods提供的push、pop等7個方法可以導致頁面重新渲染,剛好splice也是屬性arrayMethods提供的7個方法中的一種。

總結一下Vue.set數組實現的原理:其實Vue.set()對於數組的處理其實就是調用了splice方法,是不是發現其實很簡單~~

對象的實現原理

我們接著向下看代碼:

if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }

這裏先判斷如果key本來就是對象中的一個屬性,並且key不是Object原型上的屬性。說明這個key本來就在對象上面已經定義過了的,直接修改值就可以了,可以自動觸發響應。

關於對象的依賴收集和觸發原理我們本文也不做詳細解釋,你可以暫時先這樣理解。vue是使用的Object.defineProperty給對象做了一層攔截,當觸發get的時候就會進行依賴收集(這裏收集的依賴還是像數組那樣,理解成渲染函數),當觸發set的時候就會觸發依賴,導致渲染函數執行頁面重新渲染。那麽第一次是在哪裏觸發get的呢?其實是在首次加載頁面渲染的時候觸發的,這裏會進行遞歸將對象的屬性都依賴收集,所以我們修改對象已有屬性值得時候會導致頁面重新渲染。這也剛好解釋了我們使用 vueInstance.$data.obj1.b = 3; 的時候為什麽頁面不會重新渲染,因為這裏的屬性b不是對象的已有屬性,也就是說屬性b沒有進行過依賴收集,所以才會導致修改屬性b的值頁面不會重新渲染。

我們接著向下看代碼:

const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== ‘production‘ && warn(
      ‘Avoid adding reactive properties to a Vue instance or its root $data ‘ +
      ‘at runtime - declare it upfront in the data option.‘
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }

首先定義變量ob的值為 target.__ob__,這個__ob__屬性到底是什麽對象呢?vue給響應式對象都加了一個__ob__屬性,如果一個對象有這個__ob__屬性,那麽就說明這個對象是響應式對象,我們修改對象已有屬性的時候就會觸發頁面渲染。

target._isVue || (ob && ob.vmCount) 的意思是:當前的target對象是vue實例對象或者是根數據對象,那麽就會拋出錯誤警告。

if (!ob)為真說明當前的target對象不是響應式對象,那麽直接賦值返回即可。

接著向下看:

  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val

這裏其實才是vue.set()真正處理對象的地方。defineReactive(ob.value, key, val)的意思是給新加的屬性添加依賴,以後再直接修改這個新的屬性的時候就會觸發頁面渲染。

ob.dep.notify()這句代碼的意思是觸發當前的依賴(這裏的依賴依然可以理解成渲染函數),所以頁面就會進行重新渲染。

總結

從源碼層次看vue提供的vue.set()和this.$set()這兩個api還是很簡單的,由於本文沒有詳細解釋vue依賴收集和觸發,所以有的地方說的還是很模糊。

從vue源碼看Vue.set()和this.$set()