乐趣区

关于前端:Vuejs设计与实现学习总结第四章6计算属性

目前的副作用函数 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 就执行了四次:

因而在 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)
退出移动版