首頁  >  文章  >  web前端  >  Vue3計算屬性怎麼實現

Vue3計算屬性怎麼實現

WBOY
WBOY轉載
2023-05-26 18:36:021784瀏覽

計算屬性

Vue3的官方文件中,對於計算屬性有這樣的描述:

  • ##對於任何包含

    回應式資料的複雜邏輯,我們都應該使用來計算屬性

  • 計算屬性只會在

    相關響應式依賴發生改變時重新求值

從上面的描述可以明確計算屬性的需求,

計算屬性計算的是響應式資料(滿足描述1),且計算結果應被快取(滿足描述2 )。讓我們一個一個來實現,先使用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
}

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

Vue3計算屬性怎麼實現

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

const sumRes = computed(() => obj.foo + obj.bar)
effect(() => console.log('sumRes =', 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, 'value') // (9)
    } 
  })
  const obj = {
    get value() {
      if(dirty) {
        value = effectFn()
        dirty = false
      }
      track(obj, 'value') // (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刪除