Vue3s offizielle Dokumentation enthält diese Beschreibung für berechnete Eigenschaften:
responsive Daten enthält, sollten wir berechnete Eigenschaften verwenden.
wenn sich die damit verbundene reaktive Abhängigkeit ändert.
Berechnete Attribute berechnen Reaktionsdaten (erfüllen Beschreibung 1) und die Berechnungsergebnisse sollte Caching sein (entspricht Beschreibung 2) . Lassen Sie uns es nacheinander implementieren, indem wir zunächst computed
verwenden, um eine berechnete Eigenschaft zu erstellen.
function effect(fn) { // 副作用函数 const effectFn = () => { cleanup(effectFn) activeEffect = effectFn effectStack.push(effectFn) fn() effectStack.pop() activeEffect = effectStack[effectStack.length - 1] } effectFn.deps = [] effectFn() } ... const data = { foo: 1, bar: 2 } const obj = new Proxy(data, { // 响应式对象 get(target, key) { track(target, key) return target[key] }, set(target, key, newValue) { target[key] = newValue trigger(target, key) return true } }) ... const sumRes = computed(() => obj.foo + obj.bar) // (1) console.log(sumRes.value)
computed
创建一个计算属性。function computed(getter) { const effectFn = effect(getter) const obj = { get value() { return effectFn() } } return obj }
在(1)处,我们简单写了一个计算属性的功能,为了实现通过sumRes.value
读取计算属性值功能,在实现计算属性时,需要返回一个对象,通过对象内的get
触发副作用函数。
function effect(fn, options = {}) { const effectFn = () => { cleanup(effectFn) activeEffect = effectFn effectStack.push(effectFn) const res = fn() // (1) effectStack.pop() activeEffect = effectStack[effectStack.length - 1] return res // (2) } effectFn.deps = [] effectFn.options = options // (3) if (!options.lazy) { // (4) effectFn() } return effectFn // (5) }
但这个函数显然是无法执行的,这是因为前面我们在实现effect
时,需要直接执行副作用函数,不需要提供返回值。没有返回值,computed
自然无法获取到effect
的执行结果。因此,当在计算属性中使用effect
时,需要将副作用函数返回给计算属性,由计算属性决定何时执行,而不再由effect
立即执行(即懒执行)。
为了实现这点,就需要向effect
中添加一个开关lazy
,考虑到我们可能将来还需要对effect
配置其它特性,我们使用一个对象options
来封装这个开关。
function computed(getter) { const effectFn = effect(getter, { lazy: true }) const obj = { get value() { // (6) return effectFn() } } return obj } const sumRes = computed(() => obj.foo + obj.bar)
我们在(4)处放置了lazy
开关,不需要懒执行的副作用函数同样会自动执行。在(1)(2)(5)处返回了副作用函数的结果,供懒执行使用。同时在(3)处向下传递了options
,保证在effect
发生嵌套时,也使得副作用函数执行预期的行为。基于上述effect
的修改,我们在computed
中设置lazy
开关。
function computed(getter) { let value let dirty = true const effectFn = effect(getter, { lazy: true }) const obj = { get value() { if(dirty) { value = effectFn() dirty = false } return value } } return obj }
从上图中可以看出,我们已经实现了描述1,即使用计算属性进行响应式数据的计算,当响应式数据的值发生变化时,计算属性的值也会随之改变。但观察上文代码的(6)处,不难发现,无论什么情况下,只要读取sumRes.value
的值,就会触发一次副作用函数,使其重新进行可能不必要的执行。所以接着,我们尝试实现描述2,缓存计算属性的结果。
先从最简单的入手,我们用一个变量value
来缓存上次计算的值,并添加一个dirty
开关,记录是否需要重新触发副作用函数。
const obj = new Proxy(data, { get(target, key) { track(target, key) return target[key] }, set(target, key, newValue) { target[key] = newValue trigger(target, key) return true } })
修改之后,缓存的值就能生效了。但这样做产生了一个明显的BUG,当dirty
的值被置为false
时,无法再变为true
,这也就意味着,无论响应式数据obj.bar
与obj.foo
如何变化,计算属性的值永远都只能是缓存的值value
,如下图所示。
为了解决这个问题,我们需要一种方式,能够在obj.bar
或obj.foo
的值变化时,在获取sumRes.value
之前,将dirty
开关的值置为true
。受前面懒加载的启发,我们尝试能不能通过配置options
来实现这个功能。
function trigger(target, key) { const propsMap = objsMap.get(target) if(!propsMap) return const fns = propsMap.get(key) const otherFns = new Set() fns && fns.forEach(fn => { if(fn !== activeEffect) { otherFns.add(fn) } }) otherFns.forEach(fn => { if(fn.options.scheduler) { // (7) fn.options.scheduler() } else { fn() } }) }
再来回忆一下响应式对象的整个流程,当响应式对象中的数据被修改时,执行了trigger
去触发收集的副作用函数。而在计算属性中,我们不再需要自动的触发副作用函数。所以自然会想到,能否在这个地方将dirty
置为true
呢?按照这个思路,我们先对trigger
进行修改。
function computed(getter) { let value let dirty = true const effectFn = effect(getter, { lazy: true, scheduler() { // (8) dirty = true } }) const obj = { get value() { if(dirty) { value = effectFn() dirty = false } return value } } return obj }
按照前文的思路,我们在(7)处增加了一个判断,如果副作用函数fn
的配置项options
中含有scheduler
函数,我们就执行scheduler
而非副作用函数fn
。我们称这里的scheduler
Bei (1) haben wir einfach eine Funktion zum Berechnen des Attributs geschrieben, um bei der Implementierung die Funktion zum Lesen des berechneten Attributwerts über sumRes.value
zu realisieren Für das berechnete Attribut müssen Sie ein Objekt zurückgeben und die Nebeneffektfunktion über get
im Objekt auslösen. const sumRes = computed(() => obj.foo + obj.bar)
effect(() => console.log('sumRes =', sumRes.value))
effect
die Nebeneffektfunktion direkt ausführen mussten, ohne einen Rückgabewert anzugeben. Ohne einen Rückgabewert kann berechnet
natürlich nicht das Ausführungsergebnis von effekt
erhalten. Daher muss bei Verwendung von effect
in einer berechneten Eigenschaft die Nebeneffektfunktion an die berechnete Eigenschaft zurückgegeben werden, und die berechnete Eigenschaft bestimmt, wann sie ausgeführt wird, anstatt sofort von effect ausgeführt zu werden
(Das ist
lazy
zu effect
hinzufügen, da wir möglicherweise auch effect< benötigen In der Zukunft /code>Konfigurieren Sie andere Funktionen. Wir verwenden ein Objekt <code>Optionen
, um diesen Schalter zu kapseln. #🎜🎜#function computed(getter) { let value let dirty = true const effectFn = effect(getter, { lazy: true, scheduler() { dirty = true trigger(obj, 'value') // (9) } }) const obj = { get value() { if(dirty) { value = effectFn() dirty = false } track(obj, 'value') // (10) return value } } return obj }
lazy
auf (4) gesetzt, und Nebeneffektfunktionen, die keine verzögerte Ausführung erfordern, werden ebenfalls automatisch ausgeführt. Das Ergebnis der Nebeneffektfunktion wird bei (1) (2) (5) für die verzögerte Ausführung zurückgegeben. Gleichzeitig werden options
bei (3) weitergegeben, um sicherzustellen, dass bei der Verschachtelung von effect
auch die Nebeneffektfunktion das erwartete Verhalten ausführt. Basierend auf der obigen Modifikation von effect
setzen wir den Schalter lazy
in computed
. #🎜🎜#effect(() => console.log(obj.foo)) for(let i = 0; i < 1e5; i++) { obj.foo++ }
sumRes.value
gelesen wird , sodass eine erneute Ausführung nicht erforderlich ist. Als nächstes versuchen wir, Beschreibung 2 zu implementieren und die Ergebnisse des berechneten Attributs zwischenzuspeichern. #🎜🎜##🎜🎜# Beginnen wir mit dem einfachsten. Wir verwenden einen variablen value
, um den zuletzt berechneten Wert zwischenzuspeichern, und fügen einen dirty
-Schalter hinzu, um aufzuzeichnen, ob Side Effektfunktionen müssen erneut ausgelöst werden. #🎜🎜#const jobQueue = new Set() // (11) const p = Promise.resolve() // (12) let isFlushing = false // (13) function flushJob() { // (14) if (isFlushing) return isFlushing = true p.then(() => { jobQueue.forEach(job => { job() }) }).finally(() => { isFlushing = false }) }
dirty
auf false
gesetzt ist, kann er nicht mehr in true
geändert werden Das bedeutet, dass unabhängig davon, wie sich die Antwortdaten obj.bar
und obj.foo
ändern, der Wert des berechneten Attributs immer der zwischengespeicherte Wert value
sein kann >, wie in der Abbildung unten gezeigt. #🎜🎜##🎜🎜## 🎜 🎜##🎜🎜#Um dieses Problem zu lösen, brauchen wir eine Möglichkeit, abzurufen, wenn sich der Wert von <code>obj.bar
oder obj.foo
ändert. Setzen Sie vor sumRes.value den Wert des Schalters dirty
auf true
. Inspiriert durch das vorherige Lazy Loading haben wir versucht herauszufinden, ob wir diese Funktion durch die Konfiguration von options
erreichen können. #🎜🎜#effect(() => { console.log(obj.foo) }, { scheduler(fn) { jobQueue.add(fn) flushJob() } })
trigger
ausgeführt, um die Funktion für gesammelte Nebeneffekte auszulösen. Bei berechneten Eigenschaften müssen wir Nebeneffektfunktionen nicht mehr automatisch auslösen. Es liegt also nahe, zu denken, dass dirty
an dieser Stelle auf true
gesetzt werden kann? Dieser Idee folgend modifizieren wir zunächst trigger
. #🎜🎜#rrreee#🎜🎜#Gemäß der vorherigen Idee haben wir bei (7) eine Beurteilung hinzugefügt, ob das Konfigurationselement options
der Nebeneffektfunktion fn
enthält < code>scheduler-Funktion ausführen, führen wir scheduler
anstelle der Nebeneffektfunktion fn
aus. Wir nennen den scheduler
hier #🎜🎜#scheduler#🎜🎜#. Dementsprechend fügen wir den Scheduler im berechneten Attribut hinzu. #🎜🎜#function computed(getter) { let value let dirty = true const effectFn = effect(getter, { lazy: true, scheduler() { // (8) dirty = true } }) const obj = { get value() { if(dirty) { value = effectFn() dirty = false } return value } } return obj }
在(8)处我们添加了调度器。添加调度器后,让我们再来串一下整个流程,当响应式数据被修改时,才会执行trigger
函数。由于我们传入了调度器,因此trigger
函数在执行时不再触发副作用函数,转而执行调度器,此时dirty
开关的值变为了true
。当程序再往下执行时,由于dirty
已经变为true
,副作用函数就可以正常被手动触发。效果如下图所示。
到这里为止,计算属性在功能上已经实现完毕了,让我们尝试完善它。在Vue中,当计算属性中的响应式数据被修改时,计算属性值会同步更改,进而重新渲染,并在页面上更新。渲染函数也会执行effect
,为了说明问题,让我们使用effect
简单的模拟一下。
const sumRes = computed(() => obj.foo + obj.bar) effect(() => console.log('sumRes =', sumRes.value))
这里我们的预期是当obj.foo
或obj.bar
改变时,effect
会自动重新执行。这里存在的问题是,正常的effect
嵌套可以被自动触发(这点我们在上一篇博客中已经实现了),但sumRes
包含的effect
仅会在被读取value
时才会被get
触发执行,这就导致响应式数据obj.foo
与obj.bar
无法收集到外部的effect
,收集不到的副作用函数,自然无法被自动触发。
function computed(getter) { let value let dirty = true const effectFn = effect(getter, { lazy: true, scheduler() { dirty = true trigger(obj, 'value') // (9) } }) const obj = { get value() { if(dirty) { value = effectFn() dirty = false } track(obj, 'value') // (10) return value } } return obj }
在(10)处我们手动收集了副作用函数,并当响应式数据被修改时,触发它们。
在设计调度器时,我们忽略了一个潜在的问题。
还是先来看一个例子:
effect(() => console.log(obj.foo)) for(let i = 0; i < 1e5; i++) { obj.foo++ }
随着响应式数据obj.foo
被不停修改,副作用函数也被不断重复执行,在明显的延迟之后,控制台打印出了大量的数据。但在前端的实际开发中,我们往往只关心最终结果,拿到结果显示在页面上。在这种条件下,我们如何避免副作用函数被重复执行呢?
const jobQueue = new Set() // (11) const p = Promise.resolve() // (12) let isFlushing = false // (13) function flushJob() { // (14) if (isFlushing) return isFlushing = true p.then(() => { jobQueue.forEach(job => { job() }) }).finally(() => { isFlushing = false }) }
这里我们的思路是使用微任务队列来进行优化。(11)处我们定义了一个Set
作为任务队列,(12)处我们定义了一个Promise
来使用微任务。精彩的部分来了,我们的思路是将副作用函数放入任务队列中,由于任务队列是基于Set
实现的,因此,重复的副作用函数仅会保留一个,这是第一点。接着,我们执行flushJob()
,这里我们巧妙的设置了一个isFlushing
开关,这个开关保证了被微任务包裹的任务队列在一次事件循环中只会执行一次,而执行的这一次会在宏任务完成之后触发任务队列中所有不重复的副作用函数,从而直接拿到最终结果,这是第二点。按照这个思路,我们在effect
中添加调度器。
effect(() => { console.log(obj.foo) }, { scheduler(fn) { jobQueue.add(fn) flushJob() } })
需要注意的是,浏览器在执行微任务时,会依次处理微任务队列中的所有微任务。因此,微任务在执行时会阻塞页面的渲染。因此,在实践中需避免在微任务队列中放置过于繁重的任务,以免导致性能问题。
Das obige ist der detaillierte Inhalt vonSo implementieren Sie von Vue3 berechnete Eigenschaften. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!