Maison > interface Web > Voir.js > Comment implémenter les propriétés calculées de Vue3

Comment implémenter les propriétés calculées de Vue3

WBOY
Libérer: 2023-05-26 18:36:02
avant
1862 Les gens l'ont consulté

Propriétés calculées

La documentation officielle de Vue3 contient cette description des propriétés calculées :

  • Pour toute logique complexe contenant des données réactives, nous devrions utiliser des propriétés calculées.

  • Les propriétés calculées ne seront réévaluées que lorsque la dépendance réactive associée changera.

D'après la description ci-dessus, nous pouvons voir clairement les exigences relatives aux attributs calculés. Les attributs calculés calculent les données réactives (description satisfaisante 1) et les résultats du calcul doivent être mis en cache (description satisfaisante 2). Implémentons-le un par un, en utilisant d'abord computed pour créer une propriété calculée. computed创建一个计算属性。

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)
Copier après la connexion

在(1)处,我们简单写了一个计算属性的功能,为了实现通过sumRes.value读取计算属性值功能,在实现计算属性时,需要返回一个对象,通过对象内的get触发副作用函数。

function computed(getter) {
  const effectFn = effect(getter)
  const obj = {
    get value() {
      return effectFn()
    }
  }
  return obj
}
Copier après la connexion

但这个函数显然是无法执行的,这是因为前面我们在实现effect时,需要直接执行副作用函数,不需要提供返回值。没有返回值,computed自然无法获取到effect的执行结果。因此,当在计算属性中使用effect时,需要将副作用函数返回给计算属性,由计算属性决定何时执行,而不再由effect立即执行(即懒执行)。

为了实现这点,就需要向effect中添加一个开关lazy,考虑到我们可能将来还需要对effect配置其它特性,我们使用一个对象options来封装这个开关。

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)
}
Copier après la connexion

我们在(4)处放置了lazy开关,不需要懒执行的副作用函数同样会自动执行。在(1)(2)(5)处返回了副作用函数的结果,供懒执行使用。同时在(3)处向下传递了options,保证在effect发生嵌套时,也使得副作用函数执行预期的行为。基于上述effect的修改,我们在computed中设置lazy开关。

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)
Copier après la connexion

Comment implémenter les propriétés calculées de Vue3

从上图中可以看出,我们已经实现了描述1,即使用计算属性进行响应式数据的计算,当响应式数据的值发生变化时,计算属性的值也会随之改变。但观察上文代码的(6)处,不难发现,无论什么情况下,只要读取sumRes.value的值,就会触发一次副作用函数,使其重新进行可能不必要的执行。所以接着,我们尝试实现描述2,缓存计算属性的结果。

先从最简单的入手,我们用一个变量value来缓存上次计算的值,并添加一个dirty开关,记录是否需要重新触发副作用函数。

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
}
Copier après la connexion

修改之后,缓存的值就能生效了。但这样做产生了一个明显的BUG,当dirty的值被置为false时,无法再变为true,这也就意味着,无论响应式数据obj.barobj.foo如何变化,计算属性的值永远都只能是缓存的值value,如下图所示。

Comment implémenter les propriétés calculées de Vue3

为了解决这个问题,我们需要一种方式,能够在obj.barobj.foo的值变化时,在获取sumRes.value之前,将dirty开关的值置为true。受前面懒加载的启发,我们尝试能不能通过配置options来实现这个功能。

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
  }
})
Copier après la connexion

再来回忆一下响应式对象的整个流程,当响应式对象中的数据被修改时,执行了trigger去触发收集的副作用函数。而在计算属性中,我们不再需要自动的触发副作用函数。所以自然会想到,能否在这个地方将dirty置为true呢?按照这个思路,我们先对trigger进行修改。

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()
    }
  })
}
Copier après la connexion

按照前文的思路,我们在(7)处增加了一个判断,如果副作用函数fn的配置项options中含有scheduler函数,我们就执行scheduler而非副作用函数fn。我们称这里的scheduler

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
}
Copier après la connexion
Copier après la connexion
En (1), nous avons simplement écrit une fonction pour calculer l'attribut. Afin de réaliser la fonction de lecture de la valeur de l'attribut calculée via sumRes.value, lors de l'implémentation de l'attribut calculé, un objet. doit être renvoyé. Déclenchez les fonctions d'effets secondaires via get dans l'objet.
const sumRes = computed(() => obj.foo + obj.bar)
effect(() => console.log('sumRes =', sumRes.value))
Copier après la connexion
Copier après la connexion

Mais cette fonction ne peut évidemment pas être exécutée. En effet, lorsque nous avons implémenté effect plus tôt, nous devions exécuter directement la fonction d'effet secondaire sans fournir de valeur de retour. Sans valeur de retour, calculed ne peut naturellement pas obtenir le résultat de l'exécution de effect. Par conséquent, lors de l'utilisation de effect dans une propriété calculée, la fonction d'effet secondaire doit être renvoyée à la propriété calculée, et la propriété calculée détermine quand l'exécuter, au lieu d'être exécutée immédiatement par effect (C'est 🎜exécution paresseuse🎜). 🎜🎜Pour y parvenir, nous devons ajouter un commutateur lazy à effect, étant donné que nous devrons peut-être configurer d'autres fonctionnalités pour effect à l'avenir, nous utiliserons un objet options pour encapsuler ce commutateur. 🎜
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
}
Copier après la connexion
Copier après la connexion
🎜Nous avons placé le commutateur lazy en (4), et les fonctions d'effets secondaires qui ne nécessitent pas d'exécution paresseuse seront également exécutées automatiquement. Le résultat de la fonction d'effet secondaire est renvoyé en (1) (2) (5) pour une exécution paresseuse. Dans le même temps, options est transmis en (3) pour garantir que lorsque effect est imbriqué, la fonction d'effet secondaire effectuera également le comportement attendu. Sur la base de la modification ci-dessus de effect, nous définissons le commutateur lazy en calculé. 🎜
effect(() => console.log(obj.foo))
for(let i = 0; i < 1e5; i++) {
  obj.foo++
}
Copier après la connexion
Copier après la connexion
🎜Comment implémenter les propriétés calculées de Vue3🎜🎜Vous pouvez voir depuis l'image ci-dessus On peut voir que nous avons implémenté la description 1, c'est-à-dire utiliser des propriétés calculées pour calculer des données réactives. Lorsque la valeur des données réactives change, la valeur des propriétés calculées change également en conséquence. Mais en observant le point (6) du code ci-dessus, il n'est pas difficile de constater que quelle que soit la situation, tant que la valeur de sumRes.value est lue, une fonction d'effet secondaire sera déclenchée. , ce qui rend inutile sa réexécution. Nous essayons ensuite d'implémenter la description 2, en mettant en cache les résultats de l'attribut calculé. 🎜🎜 Commençons par le plus simple. Nous utilisons une variable value pour mettre en cache la dernière valeur calculée, et ajoutons un commutateur dirty pour enregistrer si la fonction d'effet secondaire doit être redéclenché. 🎜
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
  })
}
Copier après la connexion
Copier après la connexion
🎜Après modification, la valeur mise en cache prendra effet. Mais cela crée un BUG évident. Lorsque la valeur de dirty est définie sur false, elle ne peut plus être modifiée en true. signifie que peu importe la façon dont les données réactives obj.bar et obj.foo changent, la valeur de l'attribut calculé peut toujours être la valeur mise en cache value</code >, comme le montre la figure ci-dessous. 🎜🎜<img src="https://img.php.cn/upload/article/000/887/227/168509736457213.png" alt="Comment implémenter les propriétés calculées de Vue3" />🎜🎜Afin de résoudre ce problème problème, nous avons besoin d'un moyen de changer le <code>sumRes.value lorsque la valeur de obj.bar ou obj.foo change. le commutateur >dirty est défini sur true. Inspirés par le chargement paresseux précédent, nous avons essayé de voir si nous pouvions réaliser cette fonction en configurant les options. 🎜
effect(() => { console.log(obj.foo) }, {
  scheduler(fn) {
    jobQueue.add(fn)
    flushJob()
  }
})
Copier après la connexion
Copier après la connexion
🎜 Rappelons l'ensemble du processus de l'objet réactif lorsque les données de l'objet réactif sont modifiées, trigger est exécuté pour déclencher la fonction d'effet secondaire collectée. Dans les propriétés calculées, nous n'avons plus besoin de déclencher automatiquement les fonctions d'effets secondaires. Il est donc naturel de se demander : puis-je définir dirty sur true à cet endroit ? Suivant cette idée, nous modifions d'abord trigger. 🎜rrreee🎜Selon l'idée précédente, nous avons ajouté un jugement en (7), si l'élément de configuration options de la fonction d'effet secondaire fn contient scheduler</ code >fonction, nous exécutons <code>scheduler au lieu de la fonction d'effet secondaire fn. Nous appelons ici le scheduler 🎜scheduler🎜. En conséquence, nous ajoutons le planificateur dans l'attribut calculé. 🎜
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
}
Copier après la connexion
Copier après la connexion

在(8)处我们添加了调度器。添加调度器后,让我们再来串一下整个流程,当响应式数据被修改时,才会执行trigger函数。由于我们传入了调度器,因此trigger函数在执行时不再触发副作用函数,转而执行调度器,此时dirty开关的值变为了true。当程序再往下执行时,由于dirty已经变为true,副作用函数就可以正常被手动触发。效果如下图所示。

Comment implémenter les propriétés calculées de Vue3

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

const sumRes = computed(() => obj.foo + obj.bar)
effect(() => console.log(&#39;sumRes =&#39;, sumRes.value))
Copier après la connexion
Copier après la connexion

这里我们的预期是当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
}
Copier après la connexion
Copier après la connexion

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

Comment implémenter les propriétés calculées de Vue3

使用微任务优化调度器

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

还是先来看一个例子:

effect(() => console.log(obj.foo))
for(let i = 0; i < 1e5; i++) {
  obj.foo++
}
Copier après la connexion
Copier après la connexion

Comment implémenter les propriétés calculées de Vue3

随着响应式数据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
  })
}
Copier après la connexion
Copier après la connexion

这里我们的思路是使用微任务队列来进行优化。(11)处我们定义了一个Set作为任务队列,(12)处我们定义了一个Promise来使用微任务。精彩的部分来了,我们的思路是将副作用函数放入任务队列中,由于任务队列是基于Set实现的,因此,重复的副作用函数仅会保留一个,这是第一点。接着,我们执行flushJob(),这里我们巧妙的设置了一个isFlushing开关,这个开关保证了被微任务包裹的任务队列在一次事件循环中只会执行一次,而执行的这一次会在宏任务完成之后触发任务队列中所有不重复的副作用函数,从而直接拿到最终结果,这是第二点。按照这个思路,我们在effect中添加调度器。

effect(() => { console.log(obj.foo) }, {
  scheduler(fn) {
    jobQueue.add(fn)
    flushJob()
  }
})
Copier après la connexion
Copier après la connexion

Comment implémenter les propriétés calculées de Vue3

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:yisu.com
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal