實現一個簡化版的Vue3數據偵測

前言

距離國慶假期尤大發布vue3前瞻版本發佈已經有一個月的時間,大家都知道在vue2x版本中的響應式數據更新是用的defineProperty這個API。

vue2中,針對ObjectArray兩種數據類型採用了兩種不同的處理方式。

對於Object類型,通過Object.defineProperty通過getter/setter遞歸偵測所有對象的key,實現深度偵測

對於Array類型,通過攔截Array原型上的幾個操作實現了對數組的響應式,但是存在一些問題。

總之,通過defineProperty這種方式存在一定的性能問題

爲了解決這個問題,從很早之前vue3就計劃將採用ES6 Proxy代理的方式實現數據的響應式。(IE不支持這個API,所以vue3也不支持IE11了,垃圾IE)

關於Proxy

可以先查看MDN Proxy詳細用法。
這裏主要講一下基本語法

const obj = new Proxy(target,{
    // 獲取對象屬性會走這裏
    get(target, key, receiver){},
    // 修改對象屬性會走這裏
    set(target, key, value, receiver){},
    // 刪除對象上的方法會走這裏
    deleteProperty(target,key){} 
})

嘗試使用一下Proxy這個API,嘗試幾種用法,發現一些問題

  • 代理普通對象
const obj = {
  name: 'ahwgs',
  age: 22,
}
const res = new Proxy(obj, {
  // 獲取對象屬性會走這裏
  get(target, key, receiver) {
    console.log('get', target, key)
    return target[key]
  },
  // 修改對象屬性會走這裏
  set(target, key, value, receiver) {
    console.log('set', target[key])
    target[key] = value
    return true
  },
  // 刪除對象上的方法會走這裏
  deleteProperty(target, key) {
    console.log('deleteProperty', target[key])
  },
})

const n = res.name
res.age = 23
console.log(obj)
// get { name: 'ahwgs', age: 22 } name
// set 22
// { name: 'ahwgs', age: 23 }
  • 代理數組
// const obj = {
//   name: 'ahwgs',
//   age: 22,
// }
const obj = [1, 2, 3]
const res = new Proxy(obj, {
  // 獲取對象屬性會走這裏
  get(target, key, receiver) {
    console.log('get', target, key)
    return target[key]
  },
  // 修改對象屬性會走這裏
  set(target, key, value, receiver) {
    console.log('set', target[key])
    target[key] = value
    return true
  },
  // 刪除對象上的方法會走這裏
  deleteProperty(target, key) {
    console.log('deleteProperty', target[key])
  },
})

res.push(4)
console.log(obj)
// get [ 1, 2, 3 ] push
// get [ 1, 2, 3 ] length
// set undefined
// set 4
// [ 1, 2, 3, 4 ]

代理數組的時候發現了一個問題,get調用的兩次,一次是push一次是length這兩個都是數組自身的屬性

那麼vue3中是如何解決這個問題的呢?

  • 代理深層次對象
const obj = {
  name: 'ahwgs',
  age: 22,
  arr: [1, 2, 3],
}
const res = new Proxy(obj, {
  // 獲取對象屬性會走這裏
  get(target, key, receiver) {
    console.log('get', target, key)
    return target[key]
  },
  // 修改對象屬性會走這裏
  set(target, key, value, receiver) {
    console.log('set', target, key)
    target[key] = value
    return true
  },
  // 刪除對象上的方法會走這裏
  deleteProperty(target, key) {
    console.log('deleteProperty', target[key])
  },
})

res.arr.push(4)
console.log(obj)
// get { name: 'ahwgs', age: 22, arr: [ 1, 2, 3 ] } arr
// { name: 'ahwgs', age: 22, arr: [ 1, 2, 3, 4 ] }

發現並沒有執行set邏輯,並沒有代理到第二層級的對象,那麼vue中是如何做到深層次的代理的呢?

解決問題

上面的代碼我們遇到了兩個問題:

  • 多次觸發了get/set
  • 無法代理深層級的對象

我們手寫一個簡單的vue3嘗試解決上面這些問題,具體看下述代碼:


const toProxy = new WeakMap() // 存放的是代理後的對象
const toRaw = new WeakMap() // 存放的是代理前的對象

function isObject(target) {
  // 這裏爲什麼!==null 因爲typeof null =object 這是js的一個bug
  return typeof target === 'object' && target !== null;
}

// 模擬UI更新
function trigger() {
  console.log('UI更新了!!');
}

// 判斷key是否是val的私有屬性
function hasOwn(val, key) {
  const { hasOwnProperty } = Object.prototype
  return hasOwnProperty.call(val, key)
}

// 數據代理
// target是要代理的對象,vue中data()return的那個對象
function reactive(target) {
  // 先判斷如果不是對象 不需要做代理 直接返回
  if (!isObject(target)) return target;

  // 如果代理表中已經存在 就不需要再次代理 直接返回已存在的代理對象
  const proxy = toProxy.get(target)
  if (proxy) return proxy
  // 如果傳入的對象被代理過
  if (toRaw.has(target)) return target

  const handler = {
    set(tar, key, value, receiver) {
      // 觸發更新
      // 如果觸發的是私有屬性的話纔去更新視圖 用以解決類似於數組操作中多次set的問題
      if (hasOwn(target, key)) {
        trigger()
      }
      // 這裏使用ES6 Reflect 爲Proxy設置一些屬性
      // 用於簡化自定義的一些方法
      return Reflect.set(tar, key, value, receiver)
    },
    get(tar, key, receiver) {
      const res = Reflect.get(tar, key, receiver)
      // 判斷當前修改的值是否是否是對象 如果是對象的話 遞歸再次代理 解決深層級代理的問題
      if (isObject(tar[key])) {
        return reactive(res)
      }
      return res
    },
    deleteProperty(tar, key) {
      return Reflect.deleteProperty(tar, key)
    },
  }

  // 被代理的對象
  const observed = new Proxy(target, handler)

  // 將代理過的對象 放入緩存中
  // 防止代理過的對象再次被代理
  // WeekMap因爲的key是弱引用關係,涉及到垃圾回收機制,要比Map的效率高
  toProxy.set(target, observed) // 源對象 : 代理後的結果
  toRaw.set(observed, target) //
  return observed
}


const data = {
  name: 'ahwgs',
  age: 22,
  list: [1, 2, 3],
}
let user = reactive(data)
user = reactive(data)
user = reactive(data)
user.list.push(4)

針對上面的幾個問題做以下解釋:

  • 多次觸發了get/set

通過hasOwn這個方法,判斷當前修改的屬性是否是私有屬性,如果是的話纔去更新視圖。

對於這一點,源碼中是這樣做的:

 // 判斷是否有
  const hadKey = hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver)
  // don't trigger if target is something up in the prototype chain of original
  if (target === toRaw(receiver)) {
    /* istanbul ignore else */
    if (__DEV__) {
      const extraInfo = { oldValue, newValue: value }
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key, extraInfo)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key, extraInfo)
      }
    } else {
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key)
      }
    }
  }

判斷要setkey是否是存在的,如果是存在的就去更新視圖(trigger方法),如果不是的話往視圖中新增

  • 無法代理深層級的對象

通過在get方法中判斷當前的值是否是對象,如果是對象的話再去代理一次,做一個遞歸的操作

對於源碼中是這樣的:

const res = Reflect.get(target, key, receiver)
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }
    if (isRef(res)) {
      return res.value
    }
    track(target, OperationTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }

總結

  • 整體是通過ES6 Proxy這個新特性去實現的響應式,並且還通過WeakWap去緩存的整個代理數據的保存,提高響應式數據的性能
  • 簡單版是這麼簡單處理的,但是源碼中對每一個細節處理的都很細緻,並且結構分明,具體可以查看https://github.com/vuejs/vue-next/tree/master/packages/reactivity/src

關於

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