Vue3 响应式系统 - 2 - Computed实现思路

文章介绍了响应式系统中可调度性的重要性,以及如何通过scheduler控制副作用函数的执行。通过jobQueue实现了连续变更只触发一次的效果。接着讨论了lazy执行和computed函数的实现,以及计算属性的缓存策略,确保数据不变时避免重复计算。最后提到了在非Proxy对象上实现响应式更新的必要性。
摘要由CSDN通过智能技术生成

1. 调度执行

可调度性是响应式系统非常重要的一个特性,所谓可调度性就是当trigger触发时 控制副作用函数的执行时机和执行次数的能力。比如连续更改data.a时,如果按照之前写的逻辑就会发现data.a改变了几次,副作用函数就触发了几次,这显然不是我们想要的结果。

data.a++

data.a++

data.a++

我们改进一下effect方法,多接收一个option对象,并挂在efectFn上方便拓展一些能力。

const effect = (fn, options = {}) => {
  const effectFn = () => {
    effectFn.deps.forEach(bucket => bucket.delete(effectFn));
    effectFn.deps = []
    effectStack.push(effectFn)

    activeFuction = effectFn
    const res = fn()
    effectStack.pop()
    activeFuction = effectStack[effectStack.length - 1]
    return res
  }
  effectFn.deps = []
  effectFn.options = options
  effectFn()
}

调用efeect方法传入option,option中有一个scheduler函数,然后在trigger方法内判断当前依赖是否有scheduler,有就执行scheduler并把当前依赖传入,否则执行依赖。

effect(() => {
  console.log(data.a)
}, {
  scheduler(fn) {
    fn()
  }
})
const trigger = (target, key) => {
  let targetMap = bucketMap.get(target)
  if (!targetMap) return
  let deps = targetMap.get(key)
  if (!deps) return
  new Set(deps).forEach(f => {
    if (f !== activeFuction) {
      f.options?. scheduler? f.options.scheduler(f) : f()
    }
  });
}

这么写就能做到 连续改变data.a 只触发一次依赖吗?当然不能,目前只是套了一层scheduler,但最终还是直接执行了依赖。我们需要在scheduler中,把所有的依赖都添加到一个队列里,然后一次性触发。

定义全局变量jobQueue = new Set(),在scheduler中将fn压入jobQueue中,然后flushQueue,flushQueue就是在Promise.then中遍历jobQueue执行其中的方法。在Promise.then中遍历jobQueue是为了让jobQueue充分一次事件循环中的所有依赖统一执行,最后将jobQueue给clear掉。

let jobQueue = new Set()
let p = Promise.resolve()
let isFlushing = false
const flushQueue = () => {
  if (isFlushing) return
  isFlushing = true
  p.then(() => {
    jobQueue.forEach((fn) => fn())
  }).finally(() => {
    isFlushing = false
    jobQueue.clear()
  })
}
effect(() => {
  console.log(data.a)
}, {
  scheduler(fn) {
    jobQueue.add(fn)
    flushQueue()
  }
})

2. lazy与computed

在目前的代码基础上,调用effect传入的fn会立即执行一次,有时我不希望将传入的fn立即执行,而是在使用的时候才去执行,可以在options中加个lazy属性,然后在effect中做点手脚,以达到“懒执行”的期望。

const effect = (fn, options = {}) => {
  const effectFn = () => {
    effectFn.deps.forEach(bucket => bucket.delete(effectFn));
    effectFn.deps = []
    effectStack.push(effectFn)

    activeFuction = effectFn
    const res = fn()
    effectStack.pop()
    activeFuction = effectStack[effectStack.length - 1]
    return res
  }
  effectFn.deps = []
  effectFn.options = options
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}

const test = effect(() => {
    return data.a + data.b
}, {
  lazy: true,
  scheduler(fn) {
    jobQueue.add(fn)
    flushQueue()
  }
})

let val = test()

effect最终返回一个函数,可以手动去执行它,其实单纯手动执行意义并不大,但可以通过执行它来得到fn的返回值。

基于目前的代码,定义一个computed函数,接收一个getter函数,在函数内部声明effectRes等于effect返回的函数,执行effectRes的结果就等于副作用函数的返回值了,定义obj对象,obj.value是个访问属性,只有当读取value的值时,才会执行getter并将其结果作为返回值返回。

const computed = (getter) => {
  const effectRes = effect(getter, {
    lazy: true,
    scheduler(fn) {
      jobQueue.add(fn)
      flushQueue()
    }
  })

  const obj = {
    get value() {
      return effectRes()
    }
  }

  return obj
}

let res = computed(() => {
  console.log('computed')
  return data.a + data.b
})

console.log(res.value)

除了data.a 和 data.b的变动和访问res.value 的时候都会重新计算一遍data.a + data.b,其实懒加载和computed就已经成型了。

3. 计算属性的缓存

那么data.a 和 data.b的变动一定会触发副作用函数这个没有问题,但当data.a和data.b不变的时候访问res.value不需要每次都重新计算吧,也就是说现在需要做计算所得值的缓存。

const computed = (getter) => {
  let value = null
  let dirty = true
  const effectRes = effect(getter, {
    lazy: true,
    scheduler(fn) {
      dirty = true
      jobQueue.add(fn)
      flushQueue()
    }
  })
  const obj = {
    get value() {
      if (dirty) {
        value = effectRes()
        dirty = false
      }
      return value
    }
  }
  return obj
}
console.log(res.value)
data.a++
console.log(res.value)

在computed函数内声明value来做计算所得值的缓存,dirty来标志value是否需要重新计算,函数首次调用和data.a或data.b改变的时候需要将dirty置为true,访问res.value时判断dirty如果是true则重新计算获得结果赋值给value,最后返回value的值,这样就能完成计算属性的缓存了。

当我们执行以下这段代码的时候,data.a++后会重新打印res.value吗?

effect(() => {
  console.log('effect:', res.value)
})
data.a++

答案是不行,原因很简单res并不是一个Proxy对象,没有对它进行数据拦截。那就需要在computed中手动的去调用track和tigger函数从而拦截value属性加入响应式的逻辑。

const computed = (getter) => {
  let value = null
  let dirty = true
  const effectRes = effect(getter, {
    lazy: true,
    scheduler(fn) {
      dirty = true
      jobQueue.add(fn)
      flushQueue()
      trigger(obj, 'value')
    }
  })
  const obj = {
    get value() {
      if (dirty) {
        value = effectRes()
        dirty = false
      }
      track(obj, 'value')
      return value
    }
  }
  return obj
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值