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 实现 就分享到这里了,感谢收看,一起学习一起进步

相关推荐
从兄5 分钟前
vue 使用docx-preview 预览替换文档内的特定变量
javascript·vue.js·ecmascript
凉辰1 小时前
设计模式 策略模式 场景Vue (技术提升)
vue.js·设计模式·策略模式
清灵xmf1 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
大佩梨1 小时前
VUE+Vite之环境文件配置及使用环境变量
前端
GDAL2 小时前
npm入门教程1:npm简介
前端·npm·node.js
小白白一枚1112 小时前
css实现div被图片撑开
前端·css
薛一半3 小时前
PC端查看历史消息,鼠标向上滚动加载数据时页面停留在上次查看的位置
前端·javascript·vue.js
@蒙面大虾3 小时前
CSS综合练习——懒羊羊网页设计
前端·css
过期的H2O23 小时前
【H2O2|全栈】JS进阶知识(四)Ajax
开发语言·javascript·ajax
MarcoPage3 小时前
第十九课 Vue组件中的方法
前端·javascript·vue.js