Vue3 响应式数据设计(三)computed 实现

Vue3 响应式数据设计相关文章:

# Vue3响应式数据设计(一)4个步骤写出一个响应式数据系统

# Vue3 响应式数据设计(二)从4个方面思考完善响应式式数据设计

在上一篇中我们实现了一个比较完善的响应式数据系统。利用调度器可以控制副作用函数的执行时机,执行次数等。本篇将基于之前的实现,实现一个Vue中一个重要的功能computed。

在深入讲解计算属性之前,需要先了解懒执行。现在我们实现的effect 函数会立即执行传递给它的副作用函数,例如 :

js 复制代码
effect(
  // 这个函数会立即执行
  () => {
    console.log(obj.count)
  }
)

懒执行的effect

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

js 复制代码
effect(
  // 指定了lazy选项,这个函数不会立即执行
  () => {
    console.log(obj.count)
  },{
    lazy: true
  }
)

有了lazy 属性之后我们就可以修改effect函数的实现了,当options.lazy为true时,则不立即执行函数:

js 复制代码
function effect(fn, options) {
  function effectFn() {
    clearUp(effectFn)
    acctiveEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压入栈中。
    effectStack.push(effectFn)
    fn()
    // 在当前副作用函数执行完毕之后,将当前副作用函数弹出栈,并把activeEffect还原为之前的值。
    effectStack.pop()
    acctiveEffect = effectStack[effectStack.length - 1]
  }
  effectFn.options = options
  // 
  effectFn.deps = []
  // 只有非lazy的时候才立即执行
  if (!options.lazy) {
    effectFn()
  }
  // 将副作用函数作为返回值返回。
  return effectFn
}

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

js 复制代码
const effectFn = effect(
  // 指定了lazy选项,这个函数不会立即执行
  () => {
    console.log(obj.count)
  },{
    lazy: true
  }
)
// 手动执行副作用函数
effectFn()

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

js 复制代码
const effectFn = effect(
  // getter返回proxy.count 和proxy.num的和
  () => proxy.count + proxy.num,
  {
    lazy: true
  }
)
// 手动执行副作用函数
const value = effectFn()

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

js 复制代码
function effect(fn, options) {
  function effectFn() {
    clearUp(effectFn)
    acctiveEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压入栈中。
    effectStack.push(effectFn)
    const res = fn()
    // 在当前副作用函数执行完毕之后,将当前副作用函数弹出栈,并把activeEffect还原为之前的值。
    effectStack.pop()
    acctiveEffect = effectStack[effectStack.length - 1]

    return res
  }
  effectFn.options = options
  // 
  effectFn.deps = []
  // 只有非lazy的时候才立即执行
  if (!options.lazy) {
    effectFn()
  }
  // 将副作用函数作为返回值返回。
  return effectFn
}

computed 的初步实现

上面代码我们已经能实现懒执行的副作用函数,并且能拿到副作用函数执行的结果了,接下来就可以实现一下计算属性了,如下所示:

js 复制代码
function computed (getter) {
  const effectFn = effect(getter, {
    lazy: true
  })
  const obj = {
    // 当读取value时才执行effectFn
    get value() {
      return effectFn()
    }
  }

  return obj
}

现在我们用实现的computed 创建一个计算属性:

js 复制代码
const data = {
  name: 'jame',
  age: 30
}

const proxy = new Proxy(data, {
  get (target, key) {
    track(target, key)
    return target[key]
  },
  set (target, key, newVal) {
    target[key] = newVal
    trigger(target, key)
    return true
  }
})
js 复制代码
const sumres = computed(() => proxy.name + ':' + proxy.age + '岁')
proxy.age = 18
console.log(sumres.value)

computed缓存功能的实现

代码运行之后能得到我们想要的结果。但是我们现在实现的计算属性只实现了懒计算,并没有实现缓存功能。我们现在如果多次读取sumres.value ,effect 会多次执行。所以现在我们还需要添加缓存的功能。来看下具体代码:

js 复制代码
function computed (getter) {
  // 用来保存上一次的值
  let value
  // 用来保存是否需要重新计算
  let dirty = true

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

  const obj = {
    get value() {
      // 只有dirty为真时才需要重新计算。
      if (dirty) {
        value = effectFn()
        // 将dirty 设置为false, 下一次读取时就会读取缓存中的值。
        dirty = false
      }
      return value
    }
  }

  return obj
}

现在我们已经实现了值的缓存功能。但是如果认真思考一下,就会发现问题。如果我们修改proxy.name 的值,再次读取sumres.value的值会发现值没发生改变。为什么会这样呢?因为当第一次读取sumres.value的值后,变量dirty会设置为false,代表不需要计算。即使我们修改了obj.foo的值,但只要dirty的值为false 就不会重新计算,所以就导致了修改之后重新读取计算属性的值也不会变。

解决计算属性更新的问题

怎么解决上面这个问题呢?其实很简单,当proxy.name 或proxy.age的值发生变化时,将dirty的值重置为true 就可以了。这可以用到之前说到的调度器来实现,代码如下:

js 复制代码
function computed (getter) {
  // 用来保存上一次的值
  let value
  // 用来保存是否需要重新计算
  let dirty = true

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

  const obj = {
    get value() {
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      return value
    }
  }

  return obj
}

现在我们再来运行下代码,结果就会符合预期了。

以下是目前我们响应式数据设计的完整代码:

js 复制代码
const data = {
  name: 'jame',
  age: 30
}

const proxy = new Proxy(data, {
  get (target, key) {
    track(target, key)
    return target[key]
  },
  set (target, key, newVal) {
    target[key] = newVal
    trigger(target, key)
    return true
  }
})
let acctiveEffect = null
const effectStack = []
function effect(fn, options = {}) {
  function effectFn() {
    clearUp(effectFn)
    // 当effectFn执行时,将其设置为当前激活的副作用函数
    acctiveEffect = effectFn
    effectStack.push(effectFn)
    const res = fn()
    effectStack.pop()
    acctiveEffect = effectStack[effectStack.length - 1]
    return res
  }
  effectFn.options = options
  // 用来存储所有与该副作用相关的依赖集合
  effectFn.deps = []
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}

let bucket = new WeakMap()
/**
 * desc 读取属性值时和副作用函数建立联系
 * @param {代理的目标对象} target 
 * @param {属性,键} key 
 * @returns 
 */
function track(target, key) {
  if (!acctiveEffect) 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中
  deps.add(acctiveEffect)
  // deps 就是一个与当前副作用函数存在联系的集合
  // 将其添加到activeEffect.deps数组中
  acctiveEffect.deps.push(deps)
}

/**
 * desc 当修改属性值时触发副作用函数处理逻辑
 * @param {目标对象} target 
 * @param {键, 属性} key 
 * @returns 
 */
function trigger (target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  // 用一个新变量是为了避免无限循环
  const newEffects = new Set()
  effects && effects.forEach(effectFn => {
    if (effectFn !== acctiveEffect) {
      newEffects.add(effectFn)
    }
  })
  newEffects && newEffects.forEach(fn => {
    if (fn.options && fn.options.scheduler) {
      fn.options.scheduler(fn)
    } else {
      fn()
    }
  })
}

function clearUp (effectFn) {
  for (var i = 0; i < effectFn.deps.length; i++ ) {
    // deps 是依赖的集合
    const deps = effectFn.deps[i]
    // 将effectFn从依赖集合中移除
    deps.delete(effectFn)
  }
  // 最后需要重置effectFn.deps数组
  effectFn.deps.length = 0
}


const jobQueue = new Set()
const p = Promise.resolve()

let isFlushing = false
function flushJop () {
  if (isFlushing) {
    return
  }
  isFlushing = true
  p.then(() => {
    jobQueue.forEach(job => job())
  }).finally(() => {
    isFlushing = false
  })
}


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

  const effectFn = effect(getter, {
    lazy: true,
    scheduler () {
      if (!dirty) {
        dirty = true
        trigger(obj, 'value')
      }
    }
  })

  const obj = {
    get value() {
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      track(obj, 'value')
      return value
    }
  }

  return obj
}

Vue3 响应式数据设计(三)computed 实现 就分享到这里了,感谢收看,一起学习一起进步

相关推荐
木头没有瓜40 分钟前
vscode离线安装插件
ide·vue.js·vscode
伍哥的传说1 小时前
鸿蒙系统(HarmonyOS)应用开发之手势锁屏密码锁(PatternLock)
前端·华为·前端框架·harmonyos·鸿蒙
yugi9878381 小时前
前端跨域问题解决Access to XMLHttpRequest at xxx from has been blocked by CORS policy
前端
浪裡遊2 小时前
Sass详解:功能特性、常用方法与最佳实践
开发语言·前端·javascript·css·vue.js·rust·sass
旧曲重听12 小时前
最快实现的前端灰度方案
前端·程序人生·状态模式
默默coding的程序猿3 小时前
3.前端和后端参数不一致,后端接不到数据的解决方案
java·前端·spring·ssm·springboot·idea·springcloud
夏梦春蝉3 小时前
ES6从入门到精通:常用知识点
前端·javascript·es6
归于尽3 小时前
useEffect玩转React Hooks生命周期
前端·react.js
G等你下课3 小时前
React useEffect 详解与运用
前端·react.js
我想说一句3 小时前
当饼干遇上代码:一场HTTP与Cookie的奇幻漂流 🍪🌊
前端·javascript