[vue面試專問]Vue.set 和 Vue.delete 的實現

小為子發表於2019-04-10

Vue.set($set) 

Vue.delete($delete)


我們發現 $set$delete 定義在 stateMixin 函式中,如下程式碼:

export function stateMixin (Vue: Class<Component>) {
  // flow somehow has problems with directly declared definition object  
  // when using Object.defineProperty, so we have to procedurally build up  
  // the object here.  
  const dataDef = {}  
  dataDef.get = function () { return this._data }  
  const propsDef = {}  
  propsDef.get = function () { return this._props }  
if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function () {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',        this      )    
}    
  propsDef.set = function () {
        warn(`$props is readonly.`, this)    
  }  
}
  Object.defineProperty(Vue.prototype, '$data', dataDef)  
  Object.defineProperty(Vue.prototype, '$props', propsDef)
  Vue.prototype.$set = set  
  Vue.prototype.$delete = del   
  Vue.prototype.$watch = function (
       expOrFn: string | Function,    
       cb: any,    
       options?: Object  ): Function {
   const vm: Component = this    
   if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)    
   }    
   options = options || {}   
   options.user = true    
   const watcher = new Watcher(vm, expOrFn, cb, options)
   if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }}複製程式碼

是不是太長,太複雜?看不懂?

不急我們慢慢往下看,逐步介紹:

上面定義常量和環境判斷就不說了直接看核心:

Vue.prototype.$set = set  
Vue.prototype.$delete = del複製程式碼

可以看到 $set$delete 的值分別是是 setdel

其實我們發現initGlobalAPI 函式中定義了:

 Vue.set = set 
 Vue.delete = del複製程式碼

不難看出其實 Vue.set == $set ,Vue.delete == $delete

 接下來看看Vue.set程式碼:

export 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分別是要新增屬性的鍵名和值。

if判斷中isUndef函式用來判斷一個值是否是 undefinednull

isPrimitive 函式用來判斷一個值是否是原始型別值

ECMAScript 有 5 種原始型別(primitive type),即 Undefined、Null、Boolean、Number 和 String

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

這個判斷主要是對target與key做了校驗判斷是否是個陣列和key是否為有效的陣列索引

target.length = Math.max(target.length, key)  
target.splice(key, 1, val)複製程式碼

這就涉及到上篇部落格講的(陣列變異處理)

target.length = Math.max(target.length, key)複製程式碼

將陣列的長度修改為 target.lengthkey 中的較大者,否則如果當要設定的元素的索引大於陣列長度時 splice 無效。

target.splice(key, 1, val)複製程式碼

陣列的 splice 變異方法能夠完成陣列元素的刪除、新增、替換等操作。而 target.splice(key, 1, val) 就利用了替換元素的能力,將指定位置元素的值替換為新值,同時由於 splice 方法本身是能夠觸發響應的

然後接下來一個if:

if (key in target && !(key in Object.prototype)) {
  target[key] = val
  return val
}複製程式碼

如果 target 不是一個陣列,那麼必然就是純物件了,當給一個純物件設定屬性的時候,假設該屬性已經在物件上有定義了,那麼只需要直接設定該屬性的值即可,這將自動觸發響應,因為已存在的屬性是響應式的

key in target複製程式碼

判斷keytarget 物件上,或在 target 的原型鏈上

!(key in Object.prototype)複製程式碼

同時必須不能在 Object.prototype

const ob = (target: any).__ob__
複製程式碼

定義了 ob 常量,它是資料物件 __ob__ 屬性的引用

  defineReactive(ob.value, key, val)  ob.dep.notify()複製程式碼

defineReactive 函式設定屬性值,這是為了保證新新增的屬性是響應式的。

 __ob__.dep.notify() 從而觸發響應。這就是新增全新屬性觸發響應的原理

  if (!ob) {    target[key] = val    return val  }複製程式碼

target 也許原本就是非響應的,這個時候 target.__ob__是不存在的,所以當發現 target.__ob__ 不存在時,就簡單的賦值即可

 if (target._isVue || (ob && ob.vmCount)) {
複製程式碼

Vue 例項物件擁有 _isVue 屬性,所以當第一個條件成立時,那麼說明你正在使用 Vue.set/$set 函式為 Vue 例項物件新增屬性,為了避免屬性覆蓋的情況出現,Vue.set/$set 函式不允許這麼做,在非生產環境下會列印警告資訊

(ob && ob.vmCount)複製程式碼

這個就涉及比較深:主要是觀測一個資料物件是否為根資料物件,所以所謂的根資料物件就是 data 物件

當使用 Vue.set/$set 函式為根資料物件新增屬性時,是不被允許的

因為這樣做是永遠觸發不了依賴的。原因就是根資料物件的 Observer 例項收集不到依賴(觀察者)

set講完了 講講delete


 Vue.delete/$delete

還是一樣先看原始碼:

export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&(isUndef(target) || isPrimitive(target))  ) {
    warn(`Cannot delete reactive property on undefined, 
      null, or primitive value: ${(target: any)}`)  
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)    return  
  }  
   const ob = (target: any).__ob__  
   if (target._isVue || (ob && ob.vmCount)) {
      process.env.NODE_ENV !== 'production' && warn( 
     'Avoid deleting properties on a Vue instance or its root $data ' +   
    '- just set it to null.'    )
     return  
    }  
   if (!hasOwn(target, key)) {
    return  
   }
   delete target[key]  
  if (!ob) {
    return  
  }
  ob.dep.notify()
}
複製程式碼

del 函式接收兩個引數,分別是將要被刪除屬性的目標物件 target 以及要刪除屬性的鍵名 key

第一個if判斷和set一樣 就不講了

  if (Array.isArray(target) && isValidArrayIndex(key)) {
       target.splice(key, 1)    
    return  
}複製程式碼

第二個判斷其實和set也差不多。。。刪除陣列索引(同樣是變異陣列方法,觸發響應)

if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +  
    '- just set it to null.'    )
    return  
}複製程式碼

其實也不用說了 判斷都一樣(不能刪除Vue 例項物件或根資料的屬性)

  if (!hasOwn(target, key)) {    return  }複製程式碼

檢測key 是否是 target 物件自身擁有的屬性

 if (!ob) {    return  }複製程式碼

判斷ob物件是否存在如果不存在說明 target 物件原本就不是響應的,所以直接返回(return)即可

如果 ob 物件存在,說明 target 物件是響應的,需要觸發響應才行,即執行 ob.dep.notify()

進行觀測和依賴收集。


相關文章