Skip to content

在注册副作用函数时,可以指定一些选项参数 options 来扩展响应式数据的功能,如计算属性,即懒执行的effect。
现在我们所实现的 effect 函数会立即执行传递给它的副作用函数

javascript
// 这个函数会立即执行
effect(()=>{
  console.log(obj.foo)
})
// 这个函数会立即执行
effect(()=>{
  console.log(obj.foo)
})

懒执行effect

但在有些场景下,我们不希望它立即执行,而是希望在需要的时候才执行,例如计算属性。这时我们可以通过在 options 中添加 lazy 属性来达到目的。

javascript
effect(()=>{
  console.log(obj.foo)
}, { lazy: true })
effect(()=>{
  console.log(obj.foo)
}, { lazy: true })

有了它,我们就可以修改 effect 函数的实现逻辑了,当 options.lazy 为 true 时,则不立即执行副作用函数:

javascript
function effect(fn, options: {}){
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(activeEffect)
    fn()
    effectStack.pop()
    activeEffect = effectStack.at(-1)
  }
  effectFn.options = options
  effectFn.deps = []
  if(!options.lazy){
    effectFn()
  }
  return effectFn
}
function effect(fn, options: {}){
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(activeEffect)
    fn()
    effectStack.pop()
    activeEffect = effectStack.at(-1)
  }
  effectFn.options = options
  effectFn.deps = []
  if(!options.lazy){
    effectFn()
  }
  return effectFn
}

通过这个判断,我们就实现了让副作用函数不立即执行的功能。但问题是,副作用函数应该什么时候执行呢?通过上面的代码可以看到,我们将副作用函数 effectFn 作为 effect 函数的返回值,这就意味着当调用 effect 函数时,通过其返回值能够拿到对应的副作用函数,这样我们就能手动执行该副作用函数了:

javascript
const effectFn = effect(() => {
  console.log(obj.foo)
}, { lazy: true })
// 手动执行副作用函数
effectFn()
const effectFn = effect(() => {
  console.log(obj.foo)
}, { lazy: true })
// 手动执行副作用函数
effectFn()

初期计算属性

如果仅仅能够手动执行副作用函数,其意义并不大。但如果我们把传递给 effect 的函数看作一个 getter,那么这个 getter 函数可以返回任何值,例如:

javascript
const effectFn = effect(
  // getter 返回 obj.foo 与 obj.bar 的和
  () => obj.foo + obj.bar,
  { lazy: true }
)
const effectFn = effect(
  // getter 返回 obj.foo 与 obj.bar 的和
  () => obj.foo + obj.bar,
  { lazy: true }
)

这样我们在手动执行副作用函数时,就能够拿到其返回值:

javascript
const effectFn = effect(
  // getter 返回 obj.foo 与 obj.bar 的和
  () => obj.foo + obj.bar,
  { lazy: true }
)
// value 是 getter 的返回值
const value = effectFn()
const effectFn = effect(
  // getter 返回 obj.foo 与 obj.bar 的和
  () => obj.foo + obj.bar,
  { lazy: true }
)
// value 是 getter 的返回值
const value = effectFn()

为了实现这个目标,我们需要再对 effect 函数做一些修改,如以下代码所示:

javascript
function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    // 将 fn 的执行结果存储到 res 中
    const res = fn()  
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 将 res 作为 effectFn 的返回值
    return res  
  }
  effectFn.options = options
  effectFn.deps = []
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}
function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    // 将 fn 的执行结果存储到 res 中
    const res = fn()  
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 将 res 作为 effectFn 的返回值
    return res  
  }
  effectFn.options = options
  effectFn.deps = []
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}

通过新增的代码可以看到,传递给 effect 函数的参数 fn 才是真正的副作用函数,而 effectFn 是我们包装后的副作用函数。为了通过 effectFn 得到真正的副作用函数 fn 的执行结果,我们需要将其保存到 res 变量中,然后将其作为 effectFn 函数的返回值。现在我们已经能够实现懒执行的副作用函数,并且能够拿到副作用函数的执行结果了,接下来就可以实现计算属性了,如下所示:

computed 基本实现

javascript
function computed(getter){
  // 把 getter 作为副作用函数,创建一个 lazy 的 effect
  const effectFn = effect(getter, { lazy: true })
  const obj = {
    // 当读取 value 时才执行 effectFn
    get value(){
      return effectFn()
    }
  }
  return obj
}
function computed(getter){
  // 把 getter 作为副作用函数,创建一个 lazy 的 effect
  const effectFn = effect(getter, { lazy: true })
  const obj = {
    // 当读取 value 时才执行 effectFn
    get value(){
      return effectFn()
    }
  }
  return obj
}

我们可以使用 computed 函数来创建一个计算属性:

javascript
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, { /* ... */ })
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value) // 3
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, { /* ... */ })
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value) // 3

可以看到它能够正确地工作。不过现在我们实现的计算属性只做到了懒计算,也就是说,只有当你真正读取 sumRes.value 的值时,它才会进行计算并得到值。但是还做不到对值进行缓存,即假如我们多次访问 sumRes.value 的值,会导致 effectFn 进行多次计算,即使 obj.foo 和 obj.bar 的值本身并没有变化:

javascript
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3

computed 缓存

上面的代码多次访问 sumRes.value 的值,每次访问都会调用 effectFn 重新计算。为了解决这个问题,就需要我们在实现 computed 函数时,添加对值进行缓存的功能,如以下代码所示:

javascript
function computed(getter){
  // value 用来缓存上一次计算的值
  let value
  // dirty 标志,用来标识是否需要重新计算值,为 true 表示“脏”,需要计算
  let dirty = true
  const effectFn = effect(getter, {
    lazy: true
  })
  const obj = {
    get value(){
      // 只有“脏”时才计算值,并将得到的值缓存到 value 中
      if(dirty){
        value = effectFn()
        dirty = false
      }
      return value
    }
  }
  return obj
}
function computed(getter){
  // value 用来缓存上一次计算的值
  let value
  // dirty 标志,用来标识是否需要重新计算值,为 true 表示“脏”,需要计算
  let dirty = true
  const effectFn = effect(getter, {
    lazy: true
  })
  const obj = {
    get value(){
      // 只有“脏”时才计算值,并将得到的值缓存到 value 中
      if(dirty){
        value = effectFn()
        dirty = false
      }
      return value
    }
  }
  return obj
}
vue
<script setup>
const data = { bar: 1, foo: 2 }
const bucket = new WeakMap()
let activeEffect
const effectStack = []
const obj = new Proxy(data, {
  get(target, key) {
    track(target, key)
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    trigger(target, key)
    return true
  }
})

function track(target, key) {
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key) {
  let depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  const effectsToRun = new Set()
  effects && effects.forEach(fn => {
    if (fn !== activeEffect) {
      effectsToRun.add(fn)
    }
  })
  effectsToRun.forEach(fn => {
    // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
    if (fn.options.scheduler) { // 新增
      fn.options.scheduler(fn) // 新增
    } else {
      // 否则直接执行副作用函数(之前的默认行为)
      fn() // 新增
    }
  })
}

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(activeEffect)
    // 将 fn 的执行结果存储到 res 中
    const res = fn()
    effectStack.pop()
    activeEffect = effectStack.at(-1)
    // 将 res 作为 effectFn 的返回值
    return res
  }
  effectFn.options = options
  effectFn.deps = []
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}

function computed(getter) {
  // value 用来缓存上一次计算的值
  let value
  // dirty 标志,用来标识是否需要重新计算值,为 true 表示“脏”,需要计算
  let dirty = true
  const effectFn = effect(getter, {
    lazy: true
  })
  const obj = {
    get value() {
      // 只有“脏”时才计算值,并将得到的值缓存到 value 中
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      return value
    }
  }
  return obj
}

const sumRes = computed(() => obj.foo + obj.bar)

console.log(sumRes.value)
</script>
<template></template>
<style scoped lang='scss'></style>
<script setup>
const data = { bar: 1, foo: 2 }
const bucket = new WeakMap()
let activeEffect
const effectStack = []
const obj = new Proxy(data, {
  get(target, key) {
    track(target, key)
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    trigger(target, key)
    return true
  }
})

function track(target, key) {
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key) {
  let depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  const effectsToRun = new Set()
  effects && effects.forEach(fn => {
    if (fn !== activeEffect) {
      effectsToRun.add(fn)
    }
  })
  effectsToRun.forEach(fn => {
    // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
    if (fn.options.scheduler) { // 新增
      fn.options.scheduler(fn) // 新增
    } else {
      // 否则直接执行副作用函数(之前的默认行为)
      fn() // 新增
    }
  })
}

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(activeEffect)
    // 将 fn 的执行结果存储到 res 中
    const res = fn()
    effectStack.pop()
    activeEffect = effectStack.at(-1)
    // 将 res 作为 effectFn 的返回值
    return res
  }
  effectFn.options = options
  effectFn.deps = []
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}

function computed(getter) {
  // value 用来缓存上一次计算的值
  let value
  // dirty 标志,用来标识是否需要重新计算值,为 true 表示“脏”,需要计算
  let dirty = true
  const effectFn = effect(getter, {
    lazy: true
  })
  const obj = {
    get value() {
      // 只有“脏”时才计算值,并将得到的值缓存到 value 中
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      return value
    }
  }
  return obj
}

const sumRes = computed(() => obj.foo + obj.bar)

console.log(sumRes.value)
</script>
<template></template>
<style scoped lang='scss'></style>