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>
從控制臺(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 = "馮楠";
可以看到其能夠?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;
}