Heim  >  Artikel  >  Web-Frontend  >  So implementieren Sie von Vue3 berechnete Eigenschaften

So implementieren Sie von Vue3 berechnete Eigenschaften

WBOY
WBOYnach vorne
2023-05-26 18:36:021781Durchsuche

Berechnete Eigenschaften

Vue3s offizielle Dokumentation enthält diese Beschreibung für berechnete Eigenschaften:

  • #🎜 🎜#Für alle Für eine komplexe Logik, die

    responsive Daten enthält, sollten wir berechnete Eigenschaften verwenden.

  • Berechnete Eigenschaften werden nur neu ausgewertet,

    wenn sich die damit verbundene reaktive Abhängigkeit ändert.

Aus der obigen Beschreibung können die Anforderungen für berechnete Attribute klar verstanden werden.

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
}

So implementieren Sie von Vue3 berechnete Eigenschaften

从上图中可以看出,我们已经实现了描述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.barobj.foo如何变化,计算属性的值永远都只能是缓存的值value,如下图所示。

So implementieren Sie von Vue3 berechnete Eigenschaften

为了解决这个问题,我们需要一种方式,能够在obj.barobj.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。我们称这里的schedulerBei (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))
Aber diese Funktion kann offensichtlich nicht ausgeführt werden. Dies liegt daran, dass wir bei der vorherigen Implementierung von 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

faule Ausführung#🎜🎜#). #🎜🎜##🎜🎜#Um dies zu erreichen, müssen wir einen Schalter lazy zu effect hinzufügen, da wir möglicherweise auch effectKonfigurieren 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, &#39;value&#39;) // (9)
    } 
  })
  const obj = {
    get value() {
      if(dirty) {
        value = effectFn()
        dirty = false
      }
      track(obj, &#39;value&#39;) // (10)
      return value
    }
  }
  return obj
}
#🎜🎜#Wir haben den Schalter 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++
}
#🎜🎜#So implementieren Sie berechnete Vue3-Eigenschaften # 🎜🎜##🎜🎜#Wie Sie der obigen Abbildung entnehmen können, haben wir Beschreibung 1 implementiert, d. h. die Verwendung berechneter Eigenschaften zur Berechnung von Reaktionsdaten. Wenn sich der Wert der Reaktionsdaten ändert, ändert sich auch der Wert der berechneten Eigenschaften auch ändern. Wenn Sie jedoch Punkt (6) des obigen Codes beobachten, ist es nicht schwer festzustellen, dass unabhängig von der Situation eine Nebeneffektfunktion ausgelöst wird, solange der Wert von 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
  })
}
#🎜🎜#Nach der Änderung wird der zwischengespeicherte Wert wirksam. Dies führt jedoch zu einem offensichtlichen Fehler. Wenn der Wert von 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. #🎜🎜##🎜🎜#So implementieren Sie berechnete Vue3-Eigenschaften# 🎜 🎜##🎜🎜#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()
  }
})
#🎜🎜# Erinnern wir uns an den gesamten Prozess des reaktionsfähigen Objekts. Wenn die Daten im reaktionsfähigen Objekt geändert werden, wird 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 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,副作用函数就可以正常被手动触发。效果如下图所示。

So implementieren Sie von Vue3 berechnete Eigenschaften

到这里为止,计算属性在功能上已经实现完毕了,让我们尝试完善它。在Vue中,当计算属性中的响应式数据被修改时,计算属性值会同步更改,进而重新渲染,并在页面上更新。渲染函数也会执行effect,为了说明问题,让我们使用effect简单的模拟一下。

const sumRes = computed(() => obj.foo + obj.bar)
effect(() => console.log(&#39;sumRes =&#39;, sumRes.value))

这里我们的预期是当obj.fooobj.bar改变时,effect会自动重新执行。这里存在的问题是,正常的effect嵌套可以被自动触发(这点我们在上一篇博客中已经实现了),但sumRes包含的effect仅会在被读取value时才会被get触发执行,这就导致响应式数据obj.fooobj.bar无法收集到外部的effect,收集不到的副作用函数,自然无法被自动触发。

function computed(getter) {
  let value
  let dirty = true
  const effectFn = effect(getter, { 
    lazy: true,
    scheduler() {
      dirty = true
      trigger(obj, &#39;value&#39;) // (9)
    } 
  })
  const obj = {
    get value() {
      if(dirty) {
        value = effectFn()
        dirty = false
      }
      track(obj, &#39;value&#39;) // (10)
      return value
    }
  }
  return obj
}

在(10)处我们手动收集了副作用函数,并当响应式数据被修改时,触发它们。

So implementieren Sie von Vue3 berechnete Eigenschaften

使用微任务优化调度器

在设计调度器时,我们忽略了一个潜在的问题。

还是先来看一个例子:

effect(() => console.log(obj.foo))
for(let i = 0; i < 1e5; i++) {
  obj.foo++
}

So implementieren Sie von Vue3 berechnete Eigenschaften

随着响应式数据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()
  }
})

So implementieren Sie von Vue3 berechnete Eigenschaften

需要注意的是,浏览器在执行微任务时,会依次处理微任务队列中的所有微任务。因此,微任务在执行时会阻塞页面的渲染。因此,在实践中需避免在微任务队列中放置过于繁重的任务,以免导致性能问题。

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!

Stellungnahme:
Dieser Artikel ist reproduziert unter:yisu.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen