溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Vue3的響應(yīng)式機(jī)制怎么實(shí)現(xiàn)

發(fā)布時(shí)間:2022-12-27 09:24:53 來源:億速云 閱讀:129 作者:iii 欄目:編程語言

這篇文章主要介紹了Vue3的響應(yīng)式機(jī)制怎么實(shí)現(xiàn)的相關(guān)知識(shí),內(nèi)容詳細(xì)易懂,操作簡單快捷,具有一定借鑒價(jià)值,相信大家閱讀完這篇Vue3的響應(yīng)式機(jī)制怎么實(shí)現(xiàn)文章都會(huì)有所收獲,下面我們一起來看看吧。

一、什么是響應(yīng)式?

在javascript中的變量是沒有響應(yīng)式這么一個(gè)概念的,代碼的執(zhí)行邏輯都是自上而下的,而在Vue框架中,響應(yīng)式是特色功能之一。我們先看個(gè)例子

let num = 1;
let double = num * 2;
console.log(double); // 2
num = 2;
console.log(double); // 2

可以很明顯看出來double這個(gè)變量和num這個(gè)變量的關(guān)系并不是響應(yīng)式的,如果我們將計(jì)算double的邏輯封裝成一個(gè)函數(shù),當(dāng)num這個(gè)變量的值改變,我們就重新執(zhí)行這個(gè)函數(shù),這樣double的值就會(huì)隨著num的改變而改變,也就是我們俗稱的響應(yīng)式的。

let num = 1;
// 將計(jì)算過程封裝成一個(gè)函數(shù)
let getDouble = (n) => n * 2;
let double = getDouble(num);
console.log(double); // 2

num = 2;
// 重新計(jì)算double,這里當(dāng)然也沒有實(shí)現(xiàn)響應(yīng)式,只是說明響應(yīng)式實(shí)現(xiàn)的時(shí)候這個(gè)函數(shù)應(yīng)該再執(zhí)行一次
double = getDouble(num);
console.log(double); // 4

雖然實(shí)際開發(fā)的過程中會(huì)比現(xiàn)在這樣簡單的情況復(fù)雜很多,但是就是可以封裝成一個(gè)函數(shù)去實(shí)現(xiàn),現(xiàn)在的問題就在于我們?nèi)绾问沟胐ouble的值會(huì)根據(jù)num變量的改變而重新計(jì)算呢?

如果每一次修改num變量的值,getDouble這個(gè)函數(shù)都能知道并且執(zhí)行,根據(jù)num變量的改變而給double也相應(yīng)的發(fā)生改變,這樣就是一個(gè)響應(yīng)式的雛形了。

二、響應(yīng)式原理

在Vue中使用過三種響應(yīng)式解決方案,分別是definePropertyProxyvalue setter。在Vue2中是使用了 defineProperty API,在這之前的文章中有過較為詳細(xì)的描述,想了解Vue2響應(yīng)式的小伙伴戳這里--->vue響應(yīng)式原理 | vue2篇

defineProperty API

在 Vue2 中核心部分就在于 defineProperty 這個(gè)數(shù)據(jù)劫持 API ,當(dāng)我們定義一個(gè)對(duì)象obj,使用 defineProperty 代理 num 屬性,讀取 num 屬性時(shí)執(zhí)行了 get 函數(shù),修改num屬性時(shí)執(zhí)行了 set 函數(shù),我們只需要將計(jì)算 double 的邏輯寫在 set 函數(shù)中,就可以使得每次 num 改變時(shí), double 被相應(yīng)的賦值,也就是響應(yīng)式。

let num = 1;
let detDouble = (n) => n * 2;
let obj = {}
let double = getDouble(num)

Object.defineProperty(obj,'num',{
    get() {
        return num;
    }
    set(val){
        num = val;
        double = getDouble(val)
    }
})
console.log(double); // 2
obj.num = 2;
console.log(double); // 4

defineProperty缺陷:當(dāng)我們刪除obj.num屬性時(shí),set函數(shù)不會(huì)執(zhí)行,所以在Vue2中我們需要一個(gè)$delete 一個(gè)專門的函數(shù)去刪除數(shù)據(jù)。并且obj對(duì)象中不存在的屬性無法被劫持,并且修改數(shù)組上的length屬性也是無效的。

Proxy

單從 Proxy 的名字我們可以看出它是代理的意思,而 Proxy 的重要意義是解決了 Vue2 響應(yīng)式的缺陷。

Proxy用法:

var proxy = new Proxy(target, handler);

Proxy 對(duì)象的所有用法,都是上面這種形式,不同的只是handler參數(shù)的寫法。其中,new Proxy() 表示生成一個(gè)Proxy實(shí)例,target參數(shù)表示所要攔截的目標(biāo)對(duì)象,handler參數(shù)也是一個(gè)對(duì)象,用來定制攔截行為。

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

proxy.time // 35
proxy.name // 35
proxy.title // 35

在 Proxy 身上支持13種定制攔截

  • get(target, propKey, receiver) :攔截對(duì)象屬性的讀取,比如proxy.fooproxy['foo']。

  • set(target, propKey, value, receiver) :攔截對(duì)象屬性的設(shè)置,比如proxy.foo = vproxy['foo'] = v,返回一個(gè)布爾值。

  • has(target, propKey) :攔截propKey in proxy的操作,返回一個(gè)布爾值。

  • deleteProperty(target, propKey) :攔截delete proxy[propKey]的操作,返回一個(gè)布爾值。

  • ownKeys(target) :攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循環(huán),返回一個(gè)數(shù)組。該方法返回目標(biāo)對(duì)象所有自身的屬性的屬性名,而Object.keys()的返回結(jié)果僅包括目標(biāo)對(duì)象自身的可遍歷屬性。

  • getOwnPropertyDescriptor(target, propKey) :攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對(duì)象。

  • defineProperty(target, propKey, propDesc) :攔截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一個(gè)布爾值。

  • preventExtensions(target) :攔截Object.preventExtensions(proxy),返回一個(gè)布爾值。

  • getPrototypeOf(target) :攔截Object.getPrototypeOf(proxy),返回一個(gè)對(duì)象。

  • isExtensible(target) :攔截Object.isExtensible(proxy),返回一個(gè)布爾值。

  • setPrototypeOf(target, proto) :攔截Object.setPrototypeOf(proxy, proto),返回一個(gè)布爾值。如果目標(biāo)對(duì)象是函數(shù),那么還有兩種額外操作可以攔截。

  • apply(target, object, args) :攔截 Proxy 實(shí)例作為函數(shù)調(diào)用的操作,比如proxy(...args)proxy.call(object, ...args)、proxy.apply(...)。

  • construct(target, args) :攔截 Proxy 實(shí)例作為構(gòu)造函數(shù)調(diào)用的操作,比如new proxy(...args)。

Reflect

在ES6中官方新定義了 Reflect 對(duì)象,在ES6之前對(duì)象上的所有的方法都是直接掛載在對(duì)象這個(gè)構(gòu)造函數(shù)的原型身上,而未來對(duì)象可能還會(huì)有很多方法,如果全部掛載在原型上會(huì)顯得比較臃腫,而 Reflect 對(duì)象就是為了分擔(dān) Object的壓力。

(1) 將Object對(duì)象的一 些明顯屬于語言內(nèi)部的方法(比如Object.defineProperty),放到Reflect對(duì)象上現(xiàn)階段,某些方法同時(shí)在ObjectReflect對(duì)象上部署,未來的新方法將只部署在Reflect對(duì)象上。也就是說,從Reflect對(duì)象上可以拿到語言內(nèi)部的方法。

(2) 修改某些Object方法的返回結(jié)果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時(shí),會(huì)拋出一個(gè)錯(cuò)誤,而Reflect.defineProperty(obj, name, desc)則會(huì)返回false

// 老寫法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新寫法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

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

// 老寫法
'assign' in Object // true

// 新寫法
Reflect.has(Object, 'assign') // true

(4)Reflect對(duì)象的方法與Proxy對(duì)象的方法一一對(duì)應(yīng),只要是Proxy對(duì)象的方法,就能在Reflect對(duì)象上找到對(duì)應(yīng)的方法。這就讓Proxy對(duì)象可以方便地調(diào)用對(duì)應(yīng)的Reflect方法,完成默認(rèn)行為,作為修改行為的基礎(chǔ)。也就是說,不管Proxy怎么修改默認(rèn)行為,你總可以在Reflect上獲取默認(rèn)行為。

Proxy(target, {
  set: function(target, name, value, receiver) {
    var success = Reflect.set(target, name, value, receiver);
    if (success) {
      console.log('property ' + name + ' on ' + target + ' set to ' + value);
    }
    return success;
  }
});

所以我們?cè)谶@里會(huì)使用到 Proxy 和 Reflect 對(duì)象的方與 Proxy 一一對(duì)應(yīng)這一特性,來實(shí)現(xiàn)Vue3的響應(yīng)式原理。

三、Vue3響應(yīng)式原理的實(shí)現(xiàn)

在Vue3中響應(yīng)式的核心方法是

function reactive (target){
    // 返回一個(gè)響應(yīng)式對(duì)象
    return createReactiveObject(target); 
}

根據(jù)我們前面所做的鋪墊,所以我們會(huì)使用 Proxy 代理我們所需要的相應(yīng)的對(duì)象,同時(shí)使用 Reflect 對(duì)象來映射。所以我們先初步實(shí)現(xiàn)一下,再慢慢優(yōu)化,盡可能全面。

判斷是否為對(duì)象(方法不唯一,有多種方法)

function isObject(val){
    return typeof val === 'object' && val !== null
}

盡可能采用函數(shù)式編程,讓每一個(gè)函數(shù)只做一件事,邏輯更加清晰。

初步實(shí)現(xiàn)

function createReactiveObject (target) {
    // 首先由于Proxy所代理的是對(duì)象,所以我們需要判斷target,若是原始值直接返回
    if(!isObject(target)) {
        return target;
    }
    
    let handler = {
        get(target, key, receiver) {
            let res = Reflect.get(target, key, receiver); // 使用Reflect對(duì)象做映射,不修改原對(duì)象
            console.log('獲取');
            return res;
        },
        set(target, key, value, receiver) {
            let res = Reflect.set(target, key, value, receiver);
            console.log('修改');
            return res
        },
        deleteProperty(target, key) {
            let res = Reflect.deleteProperty(target, key)
            console.log('刪除');
            return res;
        }
    }
    let ProxyObj = new Proxy(target,handler); // 被代理過的對(duì)象
    return ProxyObj;
}

Vue3的響應(yīng)式機(jī)制怎么實(shí)現(xiàn)

但是這樣會(huì)有一個(gè)問題,如果我需要代理的對(duì)象是深層嵌套的對(duì)象呢?我們先看看效果

Vue3的響應(yīng)式機(jī)制怎么實(shí)現(xiàn)

當(dāng)我們深層代理時(shí),我們直接修改深層對(duì)象中的屬性并不會(huì)觸發(fā) Proxy 對(duì)象中的 set 方法,那為什么我們可以修改呢?其實(shí)就是直接訪問原對(duì)象中深層對(duì)象的值并修改了,那我們?nèi)绾蝺?yōu)化這個(gè)問題呢?

那也需要用到遞歸操作,判斷深層對(duì)象是否被代理了,如果沒有再執(zhí)行reactive將內(nèi)部未被代理的對(duì)象代理。

那么我們?cè)?get 方法內(nèi)部就不能直接將映射之后的 res 返回出去了

解決代理對(duì)象內(nèi)部有嵌套對(duì)象

get(target, key, receiver) {
            let res = Reflect.get(target, key, receiver); // 使用Reflect對(duì)象做映射,不修改原對(duì)象
            console.log('獲取');
            // 判斷代理之后的對(duì)象是否內(nèi)部含有對(duì)象,如果有的話就遞歸一次
            return isObject(res) ? reactive(res) : res;
        }

解決對(duì)象重復(fù)代理(多次代理、多層代理)

這樣我們就實(shí)現(xiàn)了對(duì)象的深層代理,并且只有當(dāng)我們?cè)L問到內(nèi)部嵌套的對(duì)象時(shí)我們才 會(huì)去遞歸調(diào)用reactive ,這樣不僅可以實(shí)現(xiàn)深層代理,并且節(jié)約了性能,但是其實(shí)我們還沒有徹底完善,我們來看看下面這段代碼

let proxy = reactive({name: '寒月十九', message: { like: 'coding' }});
reactive(proxy);
reactive(proxy);
reactive(proxy);

這樣是不是合法的,當(dāng)然是合法的,但是沒有必要也沒有意義,所以為了避免被代理過的對(duì)象,再次被代理,太浪費(fèi)性能,所以我們需要將被代理的對(duì)象打上標(biāo)記,這樣當(dāng)帶被代理過的對(duì)象訪問到時(shí),直接將被代理過的對(duì)象返回,不需要再次代理。

在 Vue3 中,使用了hash表做映射,來記錄是否已經(jīng)被代理了。

// WeakMap-弱引用對(duì)象,一旦弱引用對(duì)象未被使用,會(huì)被垃圾回收機(jī)制回收
let toProxy = new WeakMap();  // 存放形式 { 原對(duì)象(key): 代理過的對(duì)象(value)}
let toRow = new WeakMap();  // 存放形式 { 代理過的對(duì)象(key): 原對(duì)象(value)}

let ProxyObj = new Proxy(target,handler); // 被代理過的對(duì)象
toProxy.set(target,ProxyObj);
toRow.set(ProxyObj.target);
return ProxyObj;
let ByProxy = toProxy.get(target);
// 防止多次代理
if(ByProxy) { // 如果在WeakMap中可以取到值,則說明已經(jīng)被代理過了,直接返回被代理過的對(duì)象
    return ByProxy;
}
// 防止多層代理
if(toRow.get(target)) {
    return target
}
// 為了防止下面這種寫法(多層代理)
// let proxy2 = reactive(proxy);
// let proxy3 = reactive(proxy2);
// 其實(shí)本質(zhì)上與下面這種寫法沒有區(qū)別(多次代理)
// reactive(proxy);
// reactive(proxy);
// reactive(proxy);

數(shù)組相應(yīng)問題

let arr = [1 ,2 ,3 ,4];
let proxy = reactive(arr);
proxy.push(5);
// 在set內(nèi)部其實(shí)會(huì)干兩件事,首先會(huì)將5這個(gè)值添加到數(shù)組下標(biāo)4的地方,并且會(huì)修改length的值

Vue3的響應(yīng)式機(jī)制怎么實(shí)現(xiàn)

與 Vue2 的數(shù)據(jù)劫持相比,Vue3 中的 Proxy 可以直接修改數(shù)組的長度,但是這樣我們需要在 set 方法中判斷我們是要在代理對(duì)象身上添加屬性還是修改屬性。

因?yàn)楦乱晥D的函數(shù)會(huì)在set函數(shù)中調(diào)用,我們向數(shù)組中進(jìn)行操作會(huì)觸發(fā)兩次更新視圖,所以我們需要做一些優(yōu)化。

// 判斷屬性是否原本存在
function hasOwn(target,key) {
    return target.hasOwnProperty(key);
}

set(target, key, value, receiver) {
    let res = Reflect.set(target, key, value, receiver);
    // 判斷是新增屬性還是修改屬性
    let hadKey = hasOwn(target,key);
    let oldValue = target[key];
    if(!hadKey) { // 新增屬性
        console.log('新增屬性');
    }else if(oldValue !== value){
        console.log('修改屬性');
    }
    return res
 },

避免多次更新視圖,比如修改的值與原來一致就不更新視圖,在上面兩個(gè)判斷條件中添加更新視圖的函數(shù),就不會(huì)多次更新視圖。

完整版代碼

function isObject(val) {
  return typeof val === 'object' && val !== null
}

function reactive(target) {
  // 返回一個(gè)響應(yīng)式對(duì)象
  return createReactiveObject(target);
}

// 判斷屬性是否原本存在
function hasOwn(target, key) {
  return target.hasOwnProperty(key);
}

// WeakMap-弱引用對(duì)象,一旦弱引用對(duì)象未被使用,會(huì)被垃圾回收機(jī)制回收
let toProxy = new WeakMap();  // 存放形式 { 原對(duì)象(key): 代理過的對(duì)象(value)}
let toRow = new WeakMap();  // 存放形式 { 代理過的對(duì)象(key): 原對(duì)象(value)}

function createReactiveObject(target) {
  // 首先由于Proxy所代理的是對(duì)象,所以我們需要判斷target,若是原始值直接返回
  if (!isObject(target)) {
    return target;
  }

  let ByProxy = toProxy.get(target);
  // 防止多次代理
  if (ByProxy) { // 如果在WeakMap中可以取到值,則說明已經(jīng)被代理過了,直接返回被代理過的對(duì)象
    return ByProxy;
  }
  // 防止多層代理
  if (toRow.get(target)) {
    return target
  }

  let handler = {
    get(target, key, receiver) {
      let res = Reflect.get(target, key, receiver); // 使用Reflect對(duì)象做映射,不修改原對(duì)象
      console.log('獲取');
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receiver) {
      let res = Reflect.set(target, key, value, receiver);
      // 判斷是新增屬性還是修改屬性
      let hadKey = hasOwn(target, key);
      let oldValue = target[key];
      if (!hadKey) { // 新增屬性
        console.log('新增屬性');
      } else if (oldValue !== value) {
        console.log('修改屬性');
      }
      return res
    },
    deleteProperty(target, key) {
      let res = Reflect.deleteProperty(target, key)
      console.log('刪除');
      return res;
    }
  }
  let ProxyObj = new Proxy(target, handler); // 被代理過的對(duì)象
  return ProxyObj;
}

// let proxy = reactive({name: '寒月十九'});
// proxy.name = '十九';
// console.log(proxy.name);
// delete proxy.name;
// console.log(proxy.name);

// let proxy = reactive({name: '寒月十九', message: { like: 'coding' }});
// proxy.message.like = 'writing';
// console.log('====================================');
// console.log(proxy.message.like);
// console.log('====================================');

let arr = [1, 2, 3, 4];
let proxy = reactive(arr);
proxy.push(5)

Proxy的缺陷

在IE11以下的瀏覽器都不兼容,所以如果使用 Vue3 開發(fā)一個(gè)單頁應(yīng)用的項(xiàng)目,需要考慮到兼容性問題,需要我們做額外的很多操作,才能使得IE11 以下的版本能夠兼容。

關(guān)于“Vue3的響應(yīng)式機(jī)制怎么實(shí)現(xiàn)”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對(duì)“Vue3的響應(yīng)式機(jī)制怎么實(shí)現(xiàn)”知識(shí)都有一定的了解,大家如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI