>  기사  >  웹 프론트엔드  >  Vue3 계산 속성을 구현하는 방법

Vue3 계산 속성을 구현하는 방법

WBOY
WBOY앞으로
2023-05-26 18:36:021781검색

계산된 속성

Vue3의 공식 문서에는 계산된 속성에 대한 설명이 있습니다:

  • 반응형 데이터를 포함하는 복잡한 로직의 경우 계산된 속성을 사용해야 합니다.

  • 계산된 속성은 관련 반응 종속성이 변경될 때만 재평가됩니다.

위 설명에서 계산된 속성에 대한 요구 사항을 명확하게 볼 수 있습니다. 계산된 속성은 반응형 데이터를 계산하고(설명 1 충족) 계산 결과를 캐시해야 합니다(설명 2 충족). computed를 사용하여 계산된 속성을 생성하는 것부터 시작해 하나씩 구현해 보겠습니다. 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)

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

function computed(getter) {
  const effectFn = effect(getter)
  const obj = {
    get value() {
      return effectFn()
    }
  }
  return obj
}

但这个函数显然是无法执行的,这是因为前面我们在实现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)
}

我们在(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)

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
}

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

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
  }
})

再来回忆一下响应式对象的整个流程,当响应式对象中的数据被修改时,执行了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()
    }
  })
}

按照前文的思路,我们在(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
}
(1)에서는 단순히 속성을 계산하는 함수를 작성했는데, sumRes.value를 통해 계산된 속성값을 읽어오는 기능을 구현하기 위해 계산된 속성을 구현할 때 객체를 구현했습니다. 개체 내에서 get을 통해 부작용 함수를 트리거해야 합니다.
const sumRes = computed(() => obj.foo + obj.bar)
effect(() => console.log('sumRes =', sumRes.value))

하지만 이 함수는 분명히 실행될 수 없습니다. 왜냐하면 앞서 효과를 구현했을 때 반환 값을 제공하지 않고 부작용 함수를 직접 실행해야 했기 때문입니다. 반환 값이 없으면 computed는 당연히 효과의 실행 결과를 얻을 수 없습니다. 따라서 계산된 속성에 효과를 사용할 경우 부작용 함수를 계산된 속성에 반환해야 하며, 계산된 속성은 효과에 의해 즉시 실행되는 것이 아니라 언제 실행할지 결정합니다. (즉 🎜지연 실행🎜)입니다. 🎜🎜이를 달성하려면 효과에 대한 다른 기능을 구성해야 할 수도 있다는 점을 고려하여 효과lazy 스위치를 추가해야 합니다. 앞으로는 options 개체를 사용하여 이 스위치를 캡슐화합니다. 🎜
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
}
🎜(4)에 lazy 스위치를 배치했는데, 지연 실행이 필요하지 않은 부작용 기능도 자동으로 실행됩니다. 지연 실행을 위해 부작용 함수의 결과가 (1)(2)(5)에 반환됩니다. 동시에 options는 (3)에서 전달되어 효과가 중첩될 때 부작용 함수도 예상된 동작을 수행하도록 합니다. 위의 효과 수정을 기반으로 computed에서 lazy 스위치를 설정했습니다. 🎜
effect(() => console.log(obj.foo))
for(let i = 0; i < 1e5; i++) {
  obj.foo++
}
🎜Vue3 계산 속성 구현 방법🎜🎜에서 확인할 수 있습니다. 위 그림에서는 설명 1, 즉 계산된 속성을 사용하여 반응형 데이터의 값이 변경되면 계산된 속성의 값도 그에 따라 변경되는 것을 볼 수 있습니다. 하지만 위 코드의 (6)번을 보면 상황에 관계없이 sumRes.value의 값만 읽으면 부작용 함수가 발생한다는 것을 어렵지 않게 알 수 있습니다. , 다시 실행할 필요가 없습니다. 다음으로 계산된 속성의 결과를 캐싱하는 설명 2를 구현하려고 합니다. 🎜🎜 가장 간단한 것부터 시작해 보겠습니다. 변수 value를 사용하여 마지막으로 계산된 값을 캐시하고 dirty 스위치를 추가하여 부작용 기능이 필요한지 기록합니다. 다시 트리거되었습니다. 🎜
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
  })
}
🎜수정 후에는 캐시된 값이 적용됩니다. 그러나 이는 명백한 BUG를 생성합니다. dirty 값이 false로 설정되면 더 이상 true로 변경할 수 없습니다. 이는 반응형 데이터 obj.barobj.foo가 어떻게 변경되더라도 계산된 속성의 값은 항상 캐시된 값 value일 수 있음을 의미합니다. > 아래 그림과 같습니다. 🎜🎜Vue3 계산 속성 구현 방법🎜🎜이를 해결하려면 문제는 obj.bar 또는 obj.foo의 값이 변경될 때 sumRes.value를 가져오기 전에 변경하는 방법이 필요합니다. . >dirty 스위치의 값은 true로 설정됩니다. 이전 지연 로딩에서 영감을 받아 옵션을 구성하여 이 기능을 구현할 수 있는지 확인하려고 했습니다. 🎜
effect(() => { console.log(obj.foo) }, {
  scheduler(fn) {
    jobQueue.add(fn)
    flushJob()
  }
})
🎜 리액티브 객체의 전체 프로세스를 떠올려보겠습니다. 리액티브 객체의 데이터가 수정되면 trigger가 실행되어 수집된 부작용 함수가 트리거됩니다. 계산된 속성에서는 더 이상 부작용 함수를 자동으로 트리거할 필요가 없습니다. 그렇다면 여기서 dirtytrue로 설정할 수 있을까?라고 생각하는 것은 당연합니다. 이 아이디어에 따라 먼저 trigger를 수정합니다. 🎜rrreee🎜이전 아이디어에 따르면, 부작용 함수 fn의 구성 항목 optionsscheduler가 포함되어 있는지 (7)에 판단을 추가했습니다. code >함수에서는 부작용 함수 fn 대신 scheduler를 실행합니다. 여기서는 scheduler를 🎜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
}

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

Vue3 계산 속성을 구현하는 방법

到这里为止,计算属性在功能上已经实现完毕了,让我们尝试完善它。在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)处我们手动收集了副作用函数,并当响应式数据被修改时,触发它们。

Vue3 계산 속성을 구현하는 방법

使用微任务优化调度器

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

还是先来看一个例子:

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

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
  })
}

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

effect(() => { console.log(obj.foo) }, {
  scheduler(fn) {
    jobQueue.add(fn)
    flushJob()
  }
})

Vue3 계산 속성을 구현하는 방법

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

위 내용은 Vue3 계산 속성을 구현하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제