使用Proxy實(shí)現(xiàn)Vue3—watchEffect

1.watchEffect

????watchEffect 是Vue3新增的一個(gè)api荸实,其功能與watch類似蔽莱,均可以在偵聽(tīng)到數(shù)據(jù)發(fā)生變化時(shí)執(zhí)行回調(diào)内颗。不同之處在于

  • 1 無(wú)需直接指定要監(jiān)聽(tīng)的數(shù)據(jù), 回調(diào)函數(shù)中使用到哪些數(shù)據(jù)就監(jiān)視哪些數(shù)據(jù)(必須是響應(yīng)式數(shù)據(jù))沛鸵。
  • 2 默認(rèn)初始時(shí)就會(huì)執(zhí)行一次, 收集所需要監(jiān)聽(tīng)的數(shù)據(jù)。
  • 3 無(wú)需配置灯谣,默認(rèn)深度偵聽(tīng)潜秋。
    下面是其基本使用
<script setup>
  import { reactive, ref, watchEffect } from "vue";
  let firstName = ref("李");
  let lastName = ref("云龍");
  let fullName = "";
  let obj = reactive({
    wife: {
      name: "秀芹",
    },
  });
  watchEffect(() => {
    fullName = firstName.value + lastName.value;
    console.log(fullName);
  });
  watchEffect(() => {
    console.log(obj.wife.name);
  });
  firstName.value = "趙";
  lastName.value = "剛";
  obj.wife.name = "馮楠";
</script>
watchEffect.jpg

從控制臺(tái)打印結(jié)果能夠看出,watchEffect默認(rèn)會(huì)執(zhí)行一次胎许,能夠自動(dòng)收集所需要偵聽(tīng)的數(shù)據(jù)峻呛。數(shù)據(jù)變化時(shí)觸發(fā)回調(diào),且能實(shí)現(xiàn)深度偵聽(tīng)辜窑。

2.實(shí)現(xiàn)思路

下面讓我們梳理一下實(shí)現(xiàn)以上功能的大致思路钩述。

  • 首先,watchEffect接收的必須是響應(yīng)式數(shù)據(jù)穆碎,即我們要確保數(shù)據(jù)在發(fā)生變化時(shí)能夠被偵聽(tīng)到牙勘。這一點(diǎn)我們可以參考Vue數(shù)據(jù)響應(yīng)式原理,使用觀察者模式所禀,對(duì)數(shù)據(jù)的get和set進(jìn)行攔截方面,觸發(fā)get時(shí)進(jìn)行依賴收集,即將當(dāng)前屬性與其在watchEffect中指定的回調(diào)函數(shù)進(jìn)行關(guān)聯(lián)色徘,觸發(fā)set時(shí)執(zhí)行相應(yīng)的回調(diào)函數(shù)恭金。以上操作可以使用ES6的Proxy來(lái)實(shí)現(xiàn)。

  • 接下來(lái)就是watchEffect具體要做的事褂策。由于watchEffect默認(rèn)會(huì)執(zhí)行一次蔚叨,因此要做的第一點(diǎn)就是,執(zhí)行該回調(diào)函數(shù)床蜘。前面講到watchEffect接收的是響應(yīng)式數(shù)據(jù)辙培,而執(zhí)行該回調(diào)函數(shù)則一定會(huì)訪問(wèn)到所需要的變量蔑水,因此就一定會(huì)觸發(fā)依賴收集。之前提到扬蕊,依賴收集是將當(dāng)前依賴與其對(duì)應(yīng)的回調(diào)函數(shù)進(jìn)行關(guān)聯(lián)搀别。那如何能訪問(wèn)到這個(gè)回調(diào)函數(shù)呢。顯然尾抑,我們需要一個(gè)全局變量歇父,用來(lái)存儲(chǔ)當(dāng)前正在被收集的依賴所對(duì)應(yīng)的回調(diào)函數(shù)。在依賴收集之前將其掛載到全局再愈,以確保觸發(fā)依賴收集時(shí)能夠訪問(wèn)到該回調(diào)榜苫。收集結(jié)束則踢出全局,讓出位置來(lái)給下一個(gè)被收集的依賴存儲(chǔ)其回調(diào)翎冲。
    總結(jié)一下垂睬,watchEffect要做的就是三點(diǎn)。

  • 1 全局掛載回調(diào)

  • 2 執(zhí)行回調(diào)

  • 3 踢出回調(diào)
    下面依照上述思路來(lái)實(shí)現(xiàn)抗悍。首先使用Proxy實(shí)現(xiàn)數(shù)據(jù)響應(yīng)化驹饺。

3.數(shù)據(jù)響應(yīng)化

3.1Proxy

????首先簡(jiǎn)單介紹一下Proxy。借阮一峰老師的話說(shuō)缴渊,Proxy 可以理解成在目標(biāo)對(duì)象之前架設(shè)一層“攔截”赏壹,外界對(duì)該對(duì)象的訪問(wèn),都必須先通過(guò)這層攔截衔沼,因此提供了一種機(jī)制可以對(duì)外界的訪問(wèn)進(jìn)行過(guò)濾和改寫(xiě)蝌借。其基本用法如下:

// target是目標(biāo)對(duì)象,handler參數(shù)也是一個(gè)對(duì)象,用來(lái)定制攔截行為指蚁。
// 返回的是代理對(duì)象,后面所有的操作都是操作代理對(duì)象而不是目標(biāo)對(duì)象
var proxy = new Proxy(target, handler);

下面演示如何定制基礎(chǔ)的攔截行為

const obj = {
  name: "xiaom",
};
const objProxy = new Proxy(obj, {
  get(target, key) {
    console.log("get", key);
    /* Reflect對(duì)象的方法與Proxy對(duì)象的方法一一對(duì)應(yīng)菩佑,只要是Proxy對(duì)象的方法,
    就能在Reflect對(duì)象上找到對(duì)應(yīng)的方法欣舵。這就讓Proxy對(duì)象可以方便地調(diào)用對(duì)應(yīng)的Reflect方法擎鸠,完成默認(rèn)行為  */
    return Reflect.get(target, key);
  },
  set(target, key, val) {
    console.log("set", key, val);
    const res = Reflect.set(target, key, val);
    return res;
  },
});

console.log(objProxy.name);
objProxy.name = "xiaom";
console.log(objProxy.name);

// 下面是控制臺(tái)打印結(jié)果
get name
xiaom
set name xiaom
get name
xiaom

可以看到,Proxy使用Reflect對(duì)象上對(duì)應(yīng)的方法來(lái)完成默認(rèn)行為缘圈,并返回代理對(duì)象劣光。關(guān)于Proxy的更多用法這里不做詳細(xì)介紹,后續(xù)會(huì)繼續(xù)分享糟把。下面使用Proxy來(lái)完成數(shù)據(jù)的響應(yīng)化

function observe(obj) {
  if (typeof obj !== "object") {
    return obj;
  }
  return new Proxy(obj, {
    get(target, key, receiver) {
      //console.log(key + '被收集');
      const res = Reflect.get(target, key, receiver); 
      // 依賴收集
      collect(target, key);
      // 深層響應(yīng)化
      return typeof obj === "object" ? observe(res) : res;
    },
    set(target, key, val, receiver) {
      //console.log("set", key);
      const res = Reflect.set(target, key, val, receiver);
      // 偵聽(tīng)到數(shù)據(jù)變化绢涡,執(zhí)行相應(yīng)回調(diào)
      update(target, key);
      return res;
    }
  });
}

接下來(lái)實(shí)現(xiàn)最關(guān)鍵的兩步:依賴收集 ,監(jiān)聽(tīng)執(zhí)行

3.2 實(shí)現(xiàn)依賴收集

????我們首先要明確一點(diǎn)遣疯,依賴與其對(duì)應(yīng)的回調(diào)函數(shù)是如何關(guān)聯(lián)起來(lái)的雄可?
????首先想到的是采用鍵值對(duì)的方式存入字典(Map)中。鍵名是被收集的依賴。鍵值是所對(duì)應(yīng)的回調(diào)函數(shù)数苫。顯然這樣是有缺陷的聪舒,那就是當(dāng)我們對(duì)多個(gè)對(duì)象進(jìn)行響應(yīng)式處理時(shí),這多個(gè)對(duì)象中可能存在同名屬性虐急,將會(huì)造成覆蓋箱残。因此我們應(yīng)對(duì)每一個(gè)響應(yīng)式對(duì)象單獨(dú)創(chuàng)建一個(gè)字典。并將這個(gè)對(duì)象作為鍵名止吁,所對(duì)應(yīng)的字典作為鍵值被辑。存入一個(gè)全局字典中。但是敬惦,我們知道字典是無(wú)法將對(duì)象作為鍵名的盼理。因此我們要用到ES6的WeakMap結(jié)構(gòu)。WeakMap只接受對(duì)象作為鍵名(null除外)俄删,不接受其他類型的值作為鍵名宏怔。同時(shí)還要注意,一個(gè)屬性(依賴)對(duì)應(yīng)的回調(diào)函數(shù)可能不止一個(gè)抗蠢【儆矗可能再多個(gè)watchEfect中都收集了該屬性。因此每個(gè)屬性對(duì)應(yīng)的鍵值應(yīng)當(dāng)是一個(gè)Set結(jié)構(gòu)迅矛。
????明確了以上所述妨猩,開(kāi)始實(shí)現(xiàn)依賴收集。首先創(chuàng)建創(chuàng)建一個(gè)全局的WeakMap秽褒,以及一個(gè)全局變量用于臨時(shí)存儲(chǔ)回調(diào)壶硅。

// 存儲(chǔ)回調(diào)的全局變量
let cb = null;
/* 
存儲(chǔ)依賴關(guān)系的數(shù)據(jù)結(jié)構(gòu)。它的整體結(jié)構(gòu)是销斟,以需要響應(yīng)化的對(duì)象作為鍵名庐椒,鍵值是一個(gè)map結(jié)構(gòu)。
該Map以對(duì)象的每個(gè)屬性名為鍵名蚂踊,鍵值為set結(jié)構(gòu)约谈。
該Set存儲(chǔ)了該屬性變化時(shí)需要觸發(fā)的所有回調(diào)。
   */
const targetMap = new WeakMap();

接下來(lái)實(shí)現(xiàn)依賴收集犁钟。主要過(guò)程是為響應(yīng)式對(duì)象的某個(gè)屬性所對(duì)應(yīng)的集合添加一個(gè)回調(diào)函數(shù)棱诱。

/**
 * @description 實(shí)現(xiàn)依賴收集,為屬性和其對(duì)應(yīng)的回調(diào)函數(shù)建立關(guān)聯(lián)
 * @param {Object} target 目標(biāo)對(duì)象
 * @param {any}  key  當(dāng)前被收集的屬性
 */
function collect(target, key) {
  const effect = cb; // 從全局獲取當(dāng)前依賴對(duì)應(yīng)的回調(diào)
  if (effect) {
    // 建立target涝动,key和effect之間映射關(guān)系
    let depMap = targetMap.get(target);
    // 初始化時(shí)迈勋,該對(duì)象所對(duì)應(yīng)的鍵值對(duì)還不存在,則創(chuàng)建一個(gè)map
    if (!depMap) {
      depMap = new Map();
      targetMap.set(target, depMap);
    }
    // 獲取該屬性對(duì)應(yīng)的的Set
    let deps = depMap.get(key);
    if (!deps) { 
      deps = new Set();
      depMap.set(key, deps);
    }
    deps.add(effect); // 給當(dāng)前屬性添加一個(gè)回調(diào)
  }
}
3.3 實(shí)現(xiàn)偵聽(tīng)執(zhí)行

????接下來(lái)實(shí)現(xiàn)監(jiān)聽(tīng)執(zhí)行醋粟。從全局WeakMap中取出當(dāng)前變更屬性所對(duì)應(yīng)的所有回調(diào)函數(shù)靡菇,依次執(zhí)行 重归,這一步比較簡(jiǎn)單。

function update(target, key) {
  // 根據(jù)target和key獲取對(duì)應(yīng)的set
  // 循環(huán)執(zhí)行
  const depMap = targetMap.get(target);
  if (!depMap) {
    return;
  }
  const deps = depMap.get(key);
  deps.forEach((dep) => dep());
}

至此厦凤,數(shù)據(jù)相應(yīng)化的工作已經(jīng)完成鼻吮,接下來(lái)實(shí)現(xiàn)watchEffect。

4.實(shí)現(xiàn)watchEffect

????前面已經(jīng)總結(jié)過(guò)泳唠,watchEffect只有三點(diǎn)狈网。全局掛載回調(diào),執(zhí)行回調(diào)笨腥,踢出回調(diào)。

function watchEffect(fn) {
  cb = fn;
  fn();// 觸發(fā)依賴收集
  cb = null;
}

至此所有工作都已完成勇垛,我們來(lái)測(cè)試一下脖母。

let firstName = observe({ value: "李" });
let lastName = observe({ value: "云龍" });
let fullName = "";
let obj = observe({
  wife: {
    name: "秀芹",
  },
});
watchEffect(() => {
  fullName = firstName.value + lastName.value;
  console.log(fullName);
});
watchEffect(() => {
  console.log(obj.wife.name);
});
firstName.value = "趙";
lastName.value = "剛";
obj.wife.name = "馮楠";
watchEffect.png

可以看到其能夠?qū)崿F(xiàn)首次執(zhí)行,依賴收集闲孤,偵聽(tīng)執(zhí)行谆级,深度偵聽(tīng)。行為與Vue的watchEffect一致讼积。
最后附上完整代碼

let cb = null;
const targetMap = new WeakMap();
// 數(shù)據(jù)響應(yīng)化
function observe(obj) {
  if (typeof obj !== "object") {
    return obj;
  }
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      collect(target, key);
      return typeof obj === "object" ? observe(res) : res;
    },
    set(target, key, val, receiver) {
      const res = Reflect.set(target, key, val, receiver);
      update(target, key);
      return res;
    },
  });
}
// 依賴收集
function collect(target, key) {
  const effect = cb;
  if (effect) {
    let depMap = targetMap.get(target);
    if (!depMap) {
      depMap = new Map();
      targetMap.set(target, depMap);
    }
    let deps = depMap.get(key);
    if (!deps) {
      deps = new Set();
      depMap.set(key, deps);
    }
    deps.add(effect);
  }
}
// 偵聽(tīng)執(zhí)行
function update(target, key) {
  const depMap = targetMap.get(target);
  if (!depMap) {
    return;
  }
  const deps = depMap.get(key);
  deps.forEach((dep) => dep());
}

function watchEffect(fn) {
  cb = fn;
  fn();
  cb = null;
}

參考:https://es6.ruanyifeng.com/#docs/proxy

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末肥照,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子勤众,更是在濱河造成了極大的恐慌舆绎,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件们颜,死亡現(xiàn)場(chǎng)離奇詭異吕朵,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)窥突,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門努溃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人阻问,你說(shuō)我怎么就攤上這事梧税。” “怎么了称近?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵第队,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我煌茬,道長(zhǎng)斥铺,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任坛善,我火速辦了婚禮晾蜘,結(jié)果婚禮上邻眷,老公的妹妹穿的比我還像新娘。我一直安慰自己剔交,他們只是感情好肆饶,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著岖常,像睡著了一般驯镊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上竭鞍,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天板惑,我揣著相機(jī)與錄音,去河邊找鬼偎快。 笑死冯乘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的晒夹。 我是一名探鬼主播裆馒,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼丐怯!你這毒婦竟也來(lái)了喷好?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤读跷,失蹤者是張志新(化名)和其女友劉穎梗搅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體舔亭,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡些膨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钦铺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片订雾。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖矛洞,靈堂內(nèi)的尸體忽然破棺而出洼哎,到底是詐尸還是另有隱情,我是刑警寧澤沼本,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布噩峦,位于F島的核電站,受9級(jí)特大地震影響抽兆,放射性物質(zhì)發(fā)生泄漏识补。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一辫红、第九天 我趴在偏房一處隱蔽的房頂上張望凭涂。 院中可真熱鬧祝辣,春花似錦、人聲如沸切油。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)澎胡。三九已至孕荠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間攻谁,已是汗流浹背稚伍。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留巢株,地道東北人槐瑞。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像阁苞,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子祠挫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容