原理詳解:Vue3中reactive和ref的區別

2022-09-27 22:00:22
一文帶你看懂vue3中最重要的API——ref和reactive,還在糾結用哪個麼,想把Vue3用好快來看

前端(vue)入門到精通課程:進入學習
API 檔案、設計、偵錯、自動化測試一體化共同作業工具:

vue2的響應式是通過Object.defineProperty 方法,劫持物件的gettersetter,在getter中收集依賴,在setter中觸發依賴,但是這種方式存在一些缺點:

  • 由於是遍歷遞迴監聽屬性,當屬性過多或巢狀層級過深時會影響效能

  • 無法監聽物件新增的屬性和刪除屬性,只能監聽物件本身存在的屬性,所以設計了$set$delete

  • 如果監聽陣列的話,無法監聽陣列元素的增減,只能監聽通過下標可以存取到的陣列中已有的屬性,由於使用Object.defineProperty遍歷監聽陣列原有元素過於消耗效能,vue放棄使用Object.defineProperty監聽陣列,而採用了重寫陣列原型方法的方式來監聽對陣列資料的操作,並用$setsplice 方法來更新陣列,$setsplice會呼叫重寫後的陣列方法。

【相關推薦:】

vue3響應式的實現

Proxy物件

針對Object.defineProperty的弊病, 在 ES6 中引入了一個新的物件——Proxy(物件代理)

Proxy 物件:

用於建立一個物件的代理,主要用於改變物件的某些預設行為,Proxy 可以理解成,在目標物件之前架設一層「攔截」,外界對該物件的存取,都必須先通過這層攔截,因此提供了一種機制,可以對外界的存取進行過濾和改寫。基本語法如下:

/*
 * target: 目標物件
 * handler: 設定物件,用來定義攔截的行為
 * proxy: Proxy構造器的範例
 */
 var proxy = new Proxy(target,handler)
登入後複製

攔截get,取值操作

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35
登入後複製

可以攔截的操作有:

函數操作
get讀取一個值
set寫入一個值
hasin操作符
deletePropertyObject.getPrototypeOf()
getPrototypeOfObject.getPrototypeOf()
setPrototypeOfObject.setPrototypeOf()
isExtensibleObject.isExtensible()
preventExtensionsObject.preventExtensions()
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor()
definePropertyObject.defineProperty
ownKeysObject.keys() Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()
apply呼叫一個函數
constructnew一個函數

那麼使用Proxy可以解決Vue2中的哪些問題,總結一下:

  • Proxy是對整個物件的代理,而Object.defineProperty只能代理某個屬性。
  • 物件上新增屬性,Proxy可以監聽到,Object.defineProperty不能。
  • 陣列新增修改,Proxy可以監聽到,Object.defineProperty不能。
  • 若物件內部屬性要全部遞迴代理,Proxy可以只在呼叫的時候遞迴,而Object.definePropery需要一次完成所有遞迴,Proxy相對更靈活,提高效能。

遞迴代理

var target = {
  a:1,
  b:{
    c:2,
    d:{e:3}
  }
}
var handler = {
  get:function(target, prop, receiver){
    console.log('觸發get:',prop)
    return Reflect.get(target,prop)
  },
  set:function(target,key,value,receiver){
    console.log('觸發set:',key,value)
    return Reflect.set(target,key,value,receiver)
  }
}
var proxy = new Proxy(target,handler)
 
proxy.b.d.e = 4 
// 輸出  觸發get:b , 由此可見Proxy僅代理了物件外層屬性。
登入後複製

以上寫法只代理了物件的外層屬性,所以要想深層代理整個物件的所有屬性,需要進行遞迴處理:

var target = {
  a:1,
  b:{
    c:2,
    d:{e:3}
  },
  f: {z: 3}
}
var handler = {
  get:function(target, prop, receiver){
    var val = Reflect.get(target,prop)
    console.log('觸發get:',prop)
    if(val !== null && typeof val==='object') {
        return new Proxy(val,handler) // 代理內層屬性
    }
    return Reflect.get(target,prop)
  },
  set:function(target,key,value,receiver){
    console.log('觸發set:',key,value)
    return Reflect.set(target,key,value,receiver)
  }
}
var proxy = new Proxy(target,handler)
 
proxy.b.d.e = 4 
// 輸出  觸發get b,get d, get e
登入後複製

從遞迴代理可以看出,如果要代理物件的內部屬性,Proxy可以只在屬性被呼叫時去設定代理(惰性),存取了e,就僅遞迴代理b下面的屬性,不會額外代理其他沒有用到的深層屬性,如z

關於 Reflect 的作用和意義

  • 規範語言內部方法的所屬物件,不全都堆放在Object物件或Function等物件的原型上。如
Function.prototype.apply
Object.defineProperty
登入後複製
  • 修改某些Object方法的返回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會丟擲一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false

  • Object操作都變成函數行為。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)讓它們變成了函數行為。

  • Reflect物件的方法與Proxy物件的方法一一對應,只要是Proxy物件的方法,就能在Reflect物件上找到對應的方法。這就讓Proxy物件可以方便地呼叫對應的Reflect方法,完成預設行為,作為修改行為的基礎。也就是說,不管Proxy怎麼修改預設行為,你總可以在Reflect上獲取預設行為。

vue3的reativeref

Vue3 的 reactive 和 ref 正是藉助了Proxy來實現。

reactive

作用:建立原始物件的響應式副本,即將「參照型別」資料轉換為「響應式」資料

引數: reactive引數必須是物件或陣列

reative函數實現:

// 判斷是否為物件
const isObject = val => val !== null && typeof val === 'object';
// 判斷key是否存在
const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key);

export function reactive(target) {
    // 首先先判斷是否為物件
    if (!isObject(target)) return target;

    const handler = {
        get(target, key, receiver) {
            console.log(`獲取物件屬性${key}值`)
            // 收集依賴 ...
            const result = Reflect.get(target, key, receiver)
            // 深度監聽(惰性)
            if (isObject(result)) {
                return reactive(result);
            }
            return result;
        },

        set(target, key, value, receiver) {
            console.log(`設定物件屬性${key}值`)

            // 首先先獲取舊值
            const oldValue = Reflect.get(target, key, reactive)

            let result = Reflect.set(target, key, value, receiver);
            
            if (result && oldValue !== value) {
                // 更新操作 ...
            }
            return result
        },

        deleteProperty(target, key) {
            console.log(`刪除物件屬性${key}值`)

            // 先判斷是否有key
            const hadKey = hasOwn(target, key)
            const result = Reflect.deleteProperty(target, key)

            if (hadKey && result) {
                // 更新操作 ...
            }

            return result
        },
        
        // 其他方法
        // ...
    }
    return new Proxy(target, handler)
}

const obj = { a: { b: { c: 6 } } };
const proxy = reactive(obj);

proxy.a.b.c = 77;

// 獲取物件屬性a值
// 獲取物件屬性b值
// 設定物件屬性c值 77
登入後複製

至此,參照型別的物件我們已經可以把它轉化成響應式物件了,Proxy物件只能代理參照型別的物件,對於基本資料型別如何實現響應式呢?

vue的解決方法是把基本資料型別變成一個物件:這個物件只有一個value屬性,value屬性的值就等於這個基本資料型別的值。然後,就可以用reative方法將這個物件,變成響應式的Proxy物件。

實際上就是: ref(0) --> reactive( { value:0 })

ref

作用:把基本型別的資料變為響應式資料。

引數:

1.基本資料型別

2.參照型別

3.DOM的ref屬性值

ref 實現 Vue3 原始碼

export function ref(value?: unknown) {
  return createRef(value, false)
}

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal)
    }
  }
}
登入後複製

大體思路就是,呼叫ref函數時會new 一個類,這個類監聽了value屬性的 get 和 set ,實現了在get中收集依賴,在set中觸發依賴,而如果需要對傳入引數深層監聽的話,就會呼叫我們上面提到的reactive方法。

即:

ref(0); // 通過監聽物件(類)的value屬性實現響應式
ref({a: 6}); // 呼叫reactive方法對物件進行深度監聽
登入後複製

根據上面的思路我們可以自己來簡單實現下:

// 自定義ref
function ref(target) {
  const result = { // 這裡在原始碼中體現為一個類 RefImpl
    _value: reactive(target), // target傳給reactive方法做響應式處理,如果是物件的話就變成響應式
    get value () {
      return this._value
    },
    set value (val) {
      this._value = val
      console.log('set value 資料已更新, 去更新介面')
    }
  }
 
  return result
}
 
// 測試
const ref = ref(9);
ref.value = 6;

const ref = ref({a: 4});
ref.value.a = 6;
登入後複製

ref 方法包裝的資料,需要使用.value 來存取,但在模板中不需要,Vue解析時會自動新增。

總結

  • reactive 將參照型別值變為響應式,使用Proxy實現
  • ref 可將基本型別和參照型別都變成響應式,通過監聽類的value屬性的getset實現,但是當傳入的值為參照型別時實際上內部還是使用reactive方法進行的處理
  • 推薦基本型別使用ref,參照型別使用 reactive

(學習視訊分享:、)

以上就是原理詳解:Vue3中reactive和ref的區別的詳細內容,更多請關注TW511.COM其它相關文章!