likes
comments
collection
share

Vue.js设计与实现学习总结(第四章6)计算属性

作者站长头像
站长
· 阅读数 23

目前的副作用函数effect是立即执行的:

effect(() => {
  console.log(obj.foo)
})

在某些场景下并不希望effect立即执行, 因此就可以添加options添加属性:

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

这里的lazy就是前面文章介绍的调度, 当options.lazytrue时不立即执行副作用函数:

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  effectFn.options = options
  options.deps = []
  // 只有非懒加载时才执行副作用函数
  if (!options.lazy) effectFn()
  // 将副作用函数作为返回值返回
  return effectFn
}

由于最后一行将副作用函数暴露在了函数外部因此可以手动执行改副作用函数:

const effectFn = effect(() => {
  console.log(obj.foo)
},
// options
{
  lazy: true
})
// 手动执行
effectFn()

仅仅是这样的意义其实不太大, 但手动执行后如果可以拿到传入effect函数(fn)的返回值要好的多:

const effectFn = effect(() => obj.foo + obj.bar,
// options
{
  lazy: true
})

// val 是 传函数的返回值
const val = effectFn()

因此需要对effect函数进行修改:

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
}

其实传递给effectfn才是真正的副作用函数, 而effectFn是对fn的再包装, 也正因此effectFn执行后也应该返回fn得出来的值也就新增了const res = fn()return res

说句题外话, 根据前一段时间发的文章<闭包浅谈>, 在这里面新增的res变量是闭包哦, fn也正好是回调函数, 当然effectFn也是闭包

现在实现了懒执行的副作用函数并且可以拿到副作用函数的执行结果, 可实现计算属性了:

function computed (getter) {
  // 将 getter 作为副作用函数
  const effectFn = effect(getter, { lazy: true })

  const obj = {
    // 当读取 value 时才执行 effectFn
    get value () {
      return effectFn()
    }
  }

  return obj
}

现在可以使用computed函数创建一个计算属性:

const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, { /* ... */ })

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

console.log(sumRes.value)

到目前完整的代码是:

// 储存副作用函数的桶
const bucket = new WeakMap()

// 用于储存被注册的副作用的函数
let activeEffect = undefined
// 副作用函数栈
const effectStack = []

function cleanup (effectFn) {
  for (let itme of effectFn.deps) {
    itme.delete(effectFn)
  }
  effectFn.deps.length = []
}

function effect (fn, options = {}) {
  const effectFn = () => {
    console.log('effect');
    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
}


// const data = {
//   text: 'hello world',
//   ok: true
// }
const data = { foo: 1, bar: 2 }



const obj = new Proxy(data, {
  // 拦截读取操作
  get (target, key) {
    track(target, key)
    // 返回属性值
    return target[key]
  },

  // 拦截设置操作
  set (target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    trigger(target, key)
  }
})

function track (target, key) {
  // 没有 activeEffect, 直接 return
  if (!activeEffect) return target[key]

  // 根据 target 从'桶'中回去 depsMap, 它也是一个 Map 类型: key ---> effects
  let depsMap = bucket.get(target)
  // 如果 depsMap 不存在, 则新建一个 Map 并与 target 关联
  if (!depsMap) bucket.set(target, (depsMap = new Map()))
  // 再根据 key 从depsMap 中去的 deps, 它是一个 Set 类型
  // 里面存贮所有与当前 key 相关的副作用函数: effects
  let deps = depsMap.get(key)
  // 如果 deps 不存在, 同样新建一个 Set 并与 key 关联0
  if (!deps) depsMap.set(key, (deps = new Set()))
  // 最后将当前激活的副作用函数添加到'桶'里
  deps.add(activeEffect)
}

function trigger (target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    // 如果 trigger 触发执行的副作用函数与当前正在执行的函数相同则不触发执行
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })

  effectsToRun.forEach(effectFn => {
    // 如果一个副作用函数有调度器则调用改调度器, 并将副作用函数作为参数传递
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      // 否则直接执行副作用函数
      effectFn()
    }
  })
}

function computed (getter) {
  // 将 getter 作为副作用函数
  const effectFn = effect(getter, { lazy: true })

  const obj = {
    // 当读取 value 时才执行 effectFn
    get value () {
      return effectFn()
    }
  }

  return obj
}

const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3

在函数effectFn中打印了字符串'effect'会发现sumRes.value取了四次, 函数effectFn就执行了四次:Vue.js设计与实现学习总结(第四章6)计算属性因此在computed需要添加值得缓存:

function computed(getter) {
  // value 用来缓存上一次计算的值
  let value
  // dirty 标志, 用来标识是否需要重新计算, 为 true 标识需要计算
  let dirty = true

  const effectFn = effect(getter, { lazy: true })

  const obj = {
    get value () {
      if (dirty) {
        value = effectFn()
        // 将 dirty 设置为 false, 下一次直接访问 value 中存储的值
        dirty = false
      }
      return value
    }
  }

  return obj
}

此时确实只会计算一次, 且每次访问不会重新执行副作用函数, 但是相信聪明如你已经发现问题了, 如果我们改变obj中的值后在访问sumRes.value会发现访问的值没有变化, 这里就不做演示了. 解决方法就是当其中的某一个值放生改变时将dirty重新设为true就可以了, 这时我们可以添加调度器(请参看: 执行调度):

function computed(getter) {
  // value 用来缓存上一次计算的值
  let value
  // dirty 标志, 用来标识是否需要重新计算, 为 true 标识需要计算
  let dirty = true

  const effectFn = effect(getter, {
    lazy: true,
    // 添加调度器, 将 dirty 重置
    scheduler () {
      dirty = true
    }
  })

  const obj = {
    get value () {
      if (dirty) {
        value = effectFn()
        // 将 dirty 设置为 false, 下一次直接访问 value 中存储的值
        dirty = false
      }
      return value
    }
  }

  return obj
}

现在基本完美了, 只是在某些情况下出现一个缺陷:

const sumRes = computed(() => obj.foo + obj.bar)
effect(() => {
  // 在副作用函数中读取计算属性
  console.log(sumRes.value) // 3
})

obj.bar++

obj.bar++时期望是触发计算属性, 重新渲染, 但实际上并没有, 原因是因为计算属性是有自己的effect并且是懒执行的, 只有真正在读取计算属性的值才会执行. 对于计算属性的getter函数, 它里面访问的响应数据只会把计算属性函数内部的effect收集为依赖, 而在上面的例子中把计算属性用于另一个effect时就发生了effect嵌套, 且外层的effect不会被内层的effect中的响应式数据收集

computed函数中我们也可以看到里面重新顶一个了一个对象obj并手动赋予它get函数, 并没有像这样:

const data = { foo: 1, bar: 2 }



const obj = new Proxy(data, {
  // 拦截读取操作
  get (target, key) {
    track(target, key)
    // 返回属性值
    return target[key]
  },

  // 拦截设置操作
  set (target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    trigger(target, key)
  }
})

使用代理, 两个其实是完全分开的, 只是使用了同一个副作用函数解决方法很简单既然计算属性没有绑定跟踪那就手动调用:

function computed(getter) {
  // value 用来缓存上一次计算的值
  let value
  // dirty 标志, 用来标识是否需要重新计算, 为 true 标识需要计算
  let dirty = true

  const effectFn = effect(getter, {
    lazy: true,
    // 添加调度器, 将 dirty 重置
    scheduler () {
      dirty = true
      // 当计算属性依赖的响应式数据发生变化时, 手动触发响应
      trigger(obj, 'value')
    }
  })

  const obj = {
    get value () {
      if (dirty) {
        value = effectFn()
        // 将 dirty 设置为 false, 下一次直接访问 value 中存储的值
        dirty = false
      }
      // 当读取 value 时, 手动调用 track 函数进行跟踪
      track(obj, 'value')
      return value
    }
  }

  return obj
}

到此就完成了!

里面有许多的闭包...., 及其常见的闭包

目前的完整代码为:

// 储存副作用函数的桶
const bucket = new WeakMap()

// 用于储存被注册的副作用的函数
let activeEffect = undefined
// 副作用函数栈
const effectStack = []

function cleanup (effectFn) {
  for (let itme of effectFn.deps) {
    itme.delete(effectFn)
  }
  effectFn.deps.length = []
}

function effect (fn, options = {}) {
  const effectFn = () => {
    console.log('effect');
    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
}


// const data = {
//   text: 'hello world',
//   ok: true
// }
const data = { foo: 1, bar: 2 }



const obj = new Proxy(data, {
  // 拦截读取操作
  get (target, key) {
    track(target, key)
    // 返回属性值
    return target[key]
  },

  // 拦截设置操作
  set (target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    trigger(target, key)
  }
})

function track (target, key) {
  // 没有 activeEffect, 直接 return
  if (!activeEffect) return target[key]

  // 根据 target 从'桶'中回去 depsMap, 它也是一个 Map 类型: key ---> effects
  let depsMap = bucket.get(target)
  // 如果 depsMap 不存在, 则新建一个 Map 并与 target 关联
  if (!depsMap) bucket.set(target, (depsMap = new Map()))
  // 再根据 key 从depsMap 中去的 deps, 它是一个 Set 类型
  // 里面存贮所有与当前 key 相关的副作用函数: effects
  let deps = depsMap.get(key)
  // 如果 deps 不存在, 同样新建一个 Set 并与 key 关联0
  if (!deps) depsMap.set(key, (deps = new Set()))
  // 最后将当前激活的副作用函数添加到'桶'里
  deps.add(activeEffect)
}

function trigger (target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    // 如果 trigger 触发执行的副作用函数与当前正在执行的函数相同则不触发执行
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })

  effectsToRun.forEach(effectFn => {
    // 如果一个副作用函数有调度器则调用改调度器, 并将副作用函数作为参数传递
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      // 否则直接执行副作用函数
      effectFn()
    }
  })
}

function computed(getter) {
  // value 用来缓存上一次计算的值
  let value
  // dirty 标志, 用来标识是否需要重新计算, 为 true 标识需要计算
  let dirty = true

  const effectFn = effect(getter, {
    lazy: true,
    // 添加调度器, 将 dirty 重置
    scheduler () {
      dirty = true
      // 当计算属性依赖的响应式数据发生变化时, 手动触发响应
      trigger(obj, 'value')
    }
  })

  const obj = {
    get value () {
      if (dirty) {
        value = effectFn()
        // 将 dirty 设置为 false, 下一次直接访问 value 中存储的值
        dirty = false
      }
      // 当读取 value 时, 手动调用 track 函数进行跟踪
      track(obj, 'value')
      return value
    }
  }

  return obj
}

const sumRes = computed(() => obj.foo + obj.bar)
effect(() => {
  // 在副作用函数中读取计算属性
  console.log(sumRes.value) // 3
})

obj.bar++

// effect(() => {
//   console.log('effect run');
//   document.body.innerText = obj.ok ? obj.text : 'not'
// })




// setTimeout(() => {
//   obj.ok = false
// }, 2000)