再也不怕面試官問watch、computed、watchEffect的區(qū)別了
在Vue中,數(shù)據(jù)響應式是一個核心概念,它使得當數(shù)據(jù)變化時,相關的視圖會自動更新。為了更靈活地處理數(shù)據(jù)的變化,Vue提供了多種方式,其中包括watch、computed和watchEffect。
watch
watch是Vue中一個非常強大的特性,它允許你監(jiān)聽數(shù)據(jù)的變化并做出相應的反應。它有兩種用法:一是監(jiān)聽一個具體的數(shù)據(jù)變化,二是監(jiān)聽多個數(shù)據(jù)的變化。
// 監(jiān)聽單個數(shù)據(jù)
watch('someData', (newVal, oldVal) => {
// 做一些事情
});
// 監(jiān)聽多個數(shù)據(jù)
watch(['data1', 'data2'], ([newVal1, newVal2], [oldVal1, oldVal2]) => {
// 做一些事情
});
watch的實現(xiàn)原理
Vue中watch的實現(xiàn)主要依賴于Watcher這個核心類。當調(diào)用watch時,實際上是創(chuàng)建了一個Watcher實例,并將回調(diào)函數(shù)和需要監(jiān)聽的數(shù)據(jù)傳遞給這個實例。
// 簡化版的watch實現(xiàn)
function watch(source, cb) {
const watcher = new Watcher(source, cb);
}
Watcher類的構造函數(shù)接收兩個參數(shù),分別是需要監(jiān)聽的數(shù)據(jù)(可以是一個字符串,也可以是一個返回值的函數(shù))和回調(diào)函數(shù)。在構造函數(shù)中,會對數(shù)據(jù)進行求值,然后將這個Watcher實例添加到數(shù)據(jù)的依賴列表中。
class Watcher {
constructor(source, cb) {
this.getter = typeof source === 'function' ? source : () => this.vm[source];
this.cb = cb;
this.value = this.get();
}
get() {
pushTarget(this); // 將當前Watcher實例壓棧
const value = this.getter.call(this.vm); // 觸發(fā)數(shù)據(jù)的getter,將當前Watcher實例添加到依賴列表中
popTarget(); // 將當前Watcher實例出棧
return value;
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb(this.value, oldValue);
}
}
簡單來說,watch的實現(xiàn)原理就是通過Watcher實例來監(jiān)聽數(shù)據(jù)的變化,當數(shù)據(jù)變化時,觸發(fā)update方法執(zhí)行回調(diào)函數(shù)。
computed
computed用于計算派生數(shù)據(jù)。它依賴于其他響應式數(shù)據(jù),并且只有在相關數(shù)據(jù)發(fā)生變化時才會重新計算。
computed(() => {
return someData * 2;
});
computed的實現(xiàn)原理
computed的實現(xiàn)原理相對于watch更為復雜,它依賴于getter和setter的機制。在Vue中,computed的定義是一個包含get和set方法的對象。
const computed = {
get() {
return someData * 2;
},
set(value) {
someData = value / 2;
}
};
在computed的實現(xiàn)中,當訪問計算屬性時,實際上是執(zhí)行了get方法,而在數(shù)據(jù)變化時,會執(zhí)行set方法。這里主要使用了Object.defineProperty這個JavaScript的特性。
function createComputedGetter() {
return function computedGetter() {
const value = getter.call(this); // 執(zhí)行計算屬性的get方法
track(target, TrackOpTypes.GET, 'value'); // 添加依賴
return value;
};
}
function createComputedSetter() {
return function computedSetter(newValue) {
setter.call(this, newValue); // 執(zhí)行計算屬性的set方法
trigger(target, TriggerOpTypes.SET, 'value'); // 觸發(fā)更新
};
}
function computed(getterOrOptions) {
const getter =
typeof getterOrOptions === 'function'
? getterOrOptions
: getterOrOptions.get;
const setter = getterOrOptions.set;
const cRef = new ComputedRefImpl(
getter,
setter,
isFunction(getterOrOptions) || !getterOrOptions.get
);
return cRef;
}
class ComputedRefImpl {
// 構造函數(shù)
constructor(getter, setter, isReadonly) {
// ...
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true;
triggerRef(this);
}
},
});
}
// ...
}
在上述代碼中,createComputedGetter和createComputedSetter用于創(chuàng)建計算屬性的getter和setter。computed函數(shù)接收一個getter函數(shù),并通過Object.defineProperty將getter和setter添加到計算屬性的引用對象中。
當計算屬性被訪問時,會觸發(fā)getter,此時會將當前計算屬性添加到依賴列表中。當計算屬性的依賴數(shù)據(jù)發(fā)生變化時,會觸發(fā)setter,并通過triggerRef觸發(fā)計算屬性的更新。
watchEffect
watchEffect是Vue 3新增的特性,它用于監(jiān)聽一個函數(shù)內(nèi)部的響應式數(shù)據(jù)變化,當變化時,函數(shù)會被重新執(zhí)行。
watchEffect(() => {
// 依賴于響應式數(shù)據(jù)的操作
});
watchEffect的實現(xiàn)原理
watchEffect是Vue 3中引入的響應式API,它用于執(zhí)行一個響應式函數(shù),并在函數(shù)中響應式地追蹤其依賴。與watch不同,watchEffect不需要顯式地指定依賴,它會自動追蹤函數(shù)內(nèi)部的響應式數(shù)據(jù),并在這些數(shù)據(jù)變化時觸發(fā)函數(shù)重新執(zhí)行。
以下是watchEffect的簡單用法:
import { watchEffect, reactive } from 'vue';
const state = reactive({
count: 0,
});
watchEffect(() => {
console.log(state.count);
});
在這個例子中,watchEffect內(nèi)部的函數(shù)會自動追蹤state.count的變化,并在其變化時觸發(fā)函數(shù)執(zhí)行。
現(xiàn)在,讓我們來探討watchEffect的實現(xiàn)原理。
首先,watchEffect的核心是依賴追蹤和觸發(fā)。Vue 3中的響應式系統(tǒng)使用ReactiveEffect類來表示一個響應式的函數(shù)。
class ReactiveEffect {
constructor(fn, scheduler = null) {
// ...
this.deps = [];
this.scheduler = scheduler;
}
run() {
// 執(zhí)行響應式函數(shù)
this.active && this.getter();
}
stop() {
// 停止追蹤
cleanupEffect(this);
}
}
export function watchEffect(effect, options = {}) {
// 創(chuàng)建ReactiveEffect實例
const runner = effect;
const job = () => {
if (!runner.active) {
return;
}
if (cleanup) {
cleanup();
}
// 執(zhí)行響應式函數(shù)
return runner.run();
};
// 執(zhí)行響應式函數(shù)
job();
// 返回停止函數(shù)
return () => {
stop(runner);
};
}
在上述代碼中,ReactiveEffect類表示一個響應式的函數(shù)。watchEffect函數(shù)接收一個響應式函數(shù),并創(chuàng)建一個ReactiveEffect實例。在執(zhí)行時,該實例會追蹤函數(shù)內(nèi)部的響應式數(shù)據(jù),并在這些數(shù)據(jù)變化時觸發(fā)函數(shù)重新執(zhí)行。
watchEffect返回一個停止函數(shù),用于停止對響應式數(shù)據(jù)的追蹤。
實際開發(fā)當中該怎么去選擇
watch
watch主要用于監(jiān)聽特定的數(shù)據(jù)變化并執(zhí)行回調(diào)函數(shù)。它可以監(jiān)聽數(shù)據(jù)的變化,并在滿足一定條件時執(zhí)行相應的操作。常見的使用場景包括:
- 異步操作觸發(fā):當某個數(shù)據(jù)發(fā)生變化后,需要進行異步操作,比如發(fā)起一個網(wǎng)絡請求或執(zhí)行一段耗時的操作。
watch(() => state.data, async (newData, oldData) => {
// 異步操作
await fetchData(newData);
});
- 深度監(jiān)聽:監(jiān)聽對象或數(shù)組的變化,并在深層次的數(shù)據(jù)變化時執(zhí)行回調(diào)。
watch(() => state.user.address.city, (newCity, oldCity) => {
console.log(`City changed from ${oldCity} to ${newCity}`);
});
computed
computed用于創(chuàng)建一個計算屬性,它依賴于其他響應式數(shù)據(jù),并且只有在依賴數(shù)據(jù)發(fā)生變化時才重新計算。常見的使用場景包括:
- 派生數(shù)據(jù):根據(jù)現(xiàn)有的數(shù)據(jù)計算出一些派生的數(shù)據(jù),而不必每次都重新計算。
const fullName = computed(() => `${state.firstName} ${state.lastName}`);
- 性能優(yōu)化:避免不必要的重復計算,提高性能。
const result = computed(() => {
// 避免重復計算
if (someCondition) {
return heavyCalculation();
} else {
return defaultResult;
}
});
watchEffect
watchEffect用于執(zhí)行一個響應式函數(shù),并在函數(shù)內(nèi)部自動追蹤依賴。它適用于不需要顯式指定依賴,而是在函數(shù)內(nèi)部自動追蹤所有響應式數(shù)據(jù)變化的場景。常見的使用場景包括:
- 自動依賴追蹤:函數(shù)內(nèi)部的所有響應式數(shù)據(jù)都被自動追蹤,無需顯式指定。
watchEffect(() => {
console.log(`Count changed to ${state.count}`);
});
- 動態(tài)數(shù)據(jù)處理:處理動態(tài)變化的數(shù)據(jù),無需手動管理依賴。
watchEffect(() => {
// 處理動態(tài)變化的數(shù)據(jù)
handleDynamicData();
});
總體而言,watch適用于需要有條件地監(jiān)聽數(shù)據(jù)變化的場景,computed適用于創(chuàng)建派生數(shù)據(jù)和性能優(yōu)化,而watchEffect適用于自動追蹤依賴的場景。在實際應用中,根據(jù)具體需求選擇合適的API可以更好地發(fā)揮Vue的響應式能力。