Vue3 computed如何初始化并获取设置值?

2026-04-02 20:461阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计2440个文字,预计阅读时间需要10分钟。

Vue3 computed如何初始化并获取设置值?

目录 + computed + 用法 + 实现 + 初始化 + 获取值的实现 + 值的展示 + 缓存功能 + 设置值实现 + 用法 + Vue3 中 computed API 的实现 + 大家常看 Vue3 的官网

目录
  • computed 用法
  • computed 实现
    • computed 初始化
    • computed 获取值的实现
      • 值的展示
      • 缓存功能
    • computed 设置值实现

    computed 用法

    本文给大家带来的是vue3 中 computed API的实现。

    大家看过vue3的官网,应该都知道,在vue3 的组合式API中,computed这个功能与以往的有所不同了。

    以往vue2 的 computed 用法:

    export default { components: { }, computed: { people() { return '这个人' + this.age + '岁' } }, watch() { }, data() { return { age: 1 } } }

    现在我们大多数情况使用的是组合式 API,可以直接使用computed函数来进行属性的监听。
    比如官网的这种案例写法:

    const count = ref(1) const plusOne = computed(() => count.value + 1) // 只读属性 console.log(plusOne.value) // 2

    vue3 中的 computed 接受一个 getter 函数,返回一个响应式的ref对象,并且该对象是只读属性。读取该属性通过.value的形式来对外暴露该属性的值。
    此外,如果想要修改该对象,可以传入一个带有getset函数的对象,通过getset分别读取和设置值。
    我们来看看这样的一个简单的computed用法:

    <div id="app"></div> <!-- <script src="./reactivity.global.js"></script> --> <script src="../../../../node_modules/@vue/reactivity/dist//reactivity.global.js"></script> <script> const { reactive, effect, computed } = VueReactivity const author = reactive({ name: 'clying', sex: '女' }) // 用法一: // const introduction = computed({ // 调用的是defineProperty 中的 // // getter // get() { // console.log('run get'); // return author.name + '是个' + author.sex + '程序员!' // }, // // setter // set(newValue) { // // console.log(newValue); // // 注意:我们这里使用的是解构赋值语法 // [author.name, author.sex] = newValue.split(' ') // } // }) // 用法二: // 一个计算属性 ref const introduction = computed(() => { console.log('run get'); return author.name + '是个' + author.sex + '程序员!' }) effect(() => { app.innerHTML = introduction.value // computed通过 .value 读取 }) introduction.value introduction.value introduction.value </script>

    在上述案例中,我们可以很容易就知道,不管读取introduction几次,只要值未发生改变,始终只会输出一次run get

    computed 实现

    那么既然使用vue可以实现,是不是我们也可以自己去搞一个叻?

    其实,computed 也是基于effect这个功能函数来实现的。我们可以在我们完成的effect功能上继续扩展。

    computed 初始化

    一步步来,我们先来实现computed的读取值功能。

    const introduction = computed({ // 调用的是defineProperty 中的 // getter get() { console.log('run get'); return author.name + '是个' + author.sex + '程序员!' }, // setter set(newValue) { console.log(newValue); // 注意:我们这里使用的是解构赋值语法 [author.name, author.sex] = newValue.split(' ') } })

    使用原有的 computed API可以发现,计算属性introduction其实是一个ComputedRefImpl类。

    那么我们要做的就是先初始化一个类,然后对外暴露其实例。computed可以接收两种写法,那么我们在接收参数的时候,需要判断下用户传入的是对象还是一个回调函数。

    • 如果接收的是一个回调函数,那么就只存在取值的get,无法设置值;
    • 如果接收的是一个对象,那么就应该存在取值的get和设置值的set

    export const computed = (getterOrOptions) => { let onlyGetter = isFunction(getterOrOptions) // 用户传入的回调 let getter let setter if (onlyGetter) { getter = getterOrOptions setter = () => { console.warn('no set'); } } else { // 用户传入的包含get set函数的对象 getter = getterOrOptions.get setter = getterOrOptions.set } // ref 引用类型 return new ComputedRefImpl(getter, setter) }

    对外暴露的是ComputedRefImpl实例,那么我们初始化还需要在创建一个ComputedRefImpl的类。
    ComputedRefImpl类中应该是一个effect,并且应该可以设置个读取相关的属性。在属性访问器get和set中,它们操作的需要是同一个值,所以我们还需要两个属性访问器操作的同一个值_value,且是私有属性。
    ps:类中的属性访问器get和set,底层调用的是Object.defineProperty

    Vue3 computed如何初始化并获取设置值?

    class ComputedRefImpl { public readonly effect private _value// get和set需要使用的同一个值 constructor(getter, private readonly _setter) { } get value() { return this._value } set value(newValue) { this._setter(newValue) } }

    computed 获取值的实现

    值的展示

    在我们完成初始化之后,页面上其实是不会存在我们期望的内容的。它其实是这样的一个页面:

    必然。我们还没有将用户传入的逻辑进行处理,所以根本看不到呀!

    那我们现在要做的就是先将数据展示到页面上。那我们就先将其effect出来,在读取值的时候执行effect。

    class ComputedRefImpl { public readonly effect private _value// get和set需要使用的同一个值 constructor(getter, private readonly _setter) { this.effect = new ReactiveEffect(getter, () => { }) } get value() { this._value = this.effect.run() // 执行 return this._value } set value(newValue) { this._setter(newValue) } }

    这样页面的数据就可以正常展示了。

    but,我们却忽略了一个问题,在多次读取时,其实并没有computed的缓存功能,只是effect正常获取某个值,获取一次执行一次。

    缓存功能

    那么,我们要做的就是继续完善computed的缓存功能。

    既然computed也是响应式的,那么它是一个effect,同时肯定也需要有一个缓存标识,控制它的缓存特性。如果依赖的属性发生变化,那么这个缓存标识会更新,重新执行get,没有就不重新执行。

    定义一个_dirty,默认应该取值的时候进行计算。一开始为true,默认为新值,更新。当执行完更新的时候应该将其置称false,为false时就默认不执行更新。

    那么除了需要在get读取值的时候进行判断,我们还需要依赖属性发生变化的时候,再去进行判断重新渲染,即在构造函数传入我们的一个调度回调scheduler(我们上篇文章实现的调度函数,在依赖属性发生变化的时候,会执行我们传入的回调函数)。

    class ComputedRefImpl { public readonly effect private _value// get和set需要使用的同一个值 public _dirty = true // 默认应该取值的时候进行计算 constructor(getter, private readonly _setter) { this.effect = new ReactiveEffect(getter, () => { //稍后依赖的属性变化 就会执行此调度函数 if (!this._dirty) { this._dirty = true } }) } get value() { if (this._dirty) { // 脏数据 this._dirty = false this._value = this.effect.run() // 执行 } return this._value } set value(newValue) { this._setter(newValue) } }

    我们可以看到,此时introduction值未发生变化,就只会执行一次effet。

    computed 设置值实现

    修改值,我们可以通过上述用法一来传入一个带有set和get函数的对象,在设置值的时候通过解构的方式进行赋值。

    在 computed 中的set时,我们可以拿到computed的新值,但并没有重新渲染更新页面。此时我们需要做的就是将computed相关的属性进行依赖的收集,并在发生变化的时候进行相应的依赖触发。与effect类似。

    既然类似effect,那我们就需要一个存放依赖的dep(在类中定义public dep = undefined),get时收集,属性变化时触发。

    // ComputedRefImpl 类 get value() { trackEffects(this.dep || (this.dep = new Set())) // 收集 if (this._dirty) { // 脏数据 this._dirty = false this._value = this.effect.run() // 执行 } return this._value }

    // ComputedRefImpl 类 constructor(getter, private readonly _setter) { this.effect = new ReactiveEffect(getter, () => { //稍后依赖的属性变化 就会执行此调度函数 if (!this._dirty) { this._dirty = true // 触发更新 triggerEffects(this.dep) } }) }

    其中收集trackEffects和触发triggerEffects就是将effect中的tracktrigger中部分共用功能提取出来了。

    比如收集的这个功能trackEffects。与effect类似,我们都需要将记录相应的依赖属性和依赖属性的effect,那我们就可以将其独立成一个新的功能函数,降低耦合度。

    export function track(target, type, key) { // 收集effect中 属性对应的effect if (!activeEffect) return let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { depsMap.set(key, (dep = new Set())) } // 判断去重 set中是否存在activeEffect trackEffects(dep) } // 将set存起来 export function trackEffects(dep) { if (!activeEffect) return let shouldTrack = !dep.has(activeEffect) // 不存在 if (shouldTrack) { dep.add(activeEffect) // 属性记录effect // 反向记录 effect记录哪些属性收集过 activeEffect.deps.push(dep) // 让activeEffect 记录住对应的dep 稍后清理会用到 } }

    triggerEffects也是如此:

    export function trigger(target, type, key, value, oldValue) { // 判断targetMap是否存在target // 不存在 直接返回 不需要收集 // 存在 取depsMap中对应key的effect 执行run const depsMap = targetMap.get(target) if (!depsMap) return let effects = depsMap.get(key) if (effects) { triggerEffects(effects) } } export function triggerEffects(effects) { effects = [...effects] // effects 中 set结构删除再添加会导致死循环 effects.forEach(effect => { // 在执行effect时,又要执行自己,需要屏蔽自己的effect if (effect !== activeEffect) { if (effect.scheduler) effect.scheduler() // 如果存在自己的调度函数就执行自己的scheduler else effect.run() // 否则就执行run } }); }

    上述只是vue3中computed的简版实现方式,源码中比我们实现的考虑的要多很多,有兴趣的可以自己去看看(源码路径:packages/reactivity/src/computed.ts):

    最后,我们也可以看到,页面上过了1秒之后,相应的属性变化,页面也同样发生了变化

    本文共计2440个文字,预计阅读时间需要10分钟。

    Vue3 computed如何初始化并获取设置值?

    目录 + computed + 用法 + 实现 + 初始化 + 获取值的实现 + 值的展示 + 缓存功能 + 设置值实现 + 用法 + Vue3 中 computed API 的实现 + 大家常看 Vue3 的官网

    目录
    • computed 用法
    • computed 实现
      • computed 初始化
      • computed 获取值的实现
        • 值的展示
        • 缓存功能
      • computed 设置值实现

      computed 用法

      本文给大家带来的是vue3 中 computed API的实现。

      大家看过vue3的官网,应该都知道,在vue3 的组合式API中,computed这个功能与以往的有所不同了。

      以往vue2 的 computed 用法:

      export default { components: { }, computed: { people() { return '这个人' + this.age + '岁' } }, watch() { }, data() { return { age: 1 } } }

      现在我们大多数情况使用的是组合式 API,可以直接使用computed函数来进行属性的监听。
      比如官网的这种案例写法:

      const count = ref(1) const plusOne = computed(() => count.value + 1) // 只读属性 console.log(plusOne.value) // 2

      vue3 中的 computed 接受一个 getter 函数,返回一个响应式的ref对象,并且该对象是只读属性。读取该属性通过.value的形式来对外暴露该属性的值。
      此外,如果想要修改该对象,可以传入一个带有getset函数的对象,通过getset分别读取和设置值。
      我们来看看这样的一个简单的computed用法:

      <div id="app"></div> <!-- <script src="./reactivity.global.js"></script> --> <script src="../../../../node_modules/@vue/reactivity/dist//reactivity.global.js"></script> <script> const { reactive, effect, computed } = VueReactivity const author = reactive({ name: 'clying', sex: '女' }) // 用法一: // const introduction = computed({ // 调用的是defineProperty 中的 // // getter // get() { // console.log('run get'); // return author.name + '是个' + author.sex + '程序员!' // }, // // setter // set(newValue) { // // console.log(newValue); // // 注意:我们这里使用的是解构赋值语法 // [author.name, author.sex] = newValue.split(' ') // } // }) // 用法二: // 一个计算属性 ref const introduction = computed(() => { console.log('run get'); return author.name + '是个' + author.sex + '程序员!' }) effect(() => { app.innerHTML = introduction.value // computed通过 .value 读取 }) introduction.value introduction.value introduction.value </script>

      在上述案例中,我们可以很容易就知道,不管读取introduction几次,只要值未发生改变,始终只会输出一次run get

      computed 实现

      那么既然使用vue可以实现,是不是我们也可以自己去搞一个叻?

      其实,computed 也是基于effect这个功能函数来实现的。我们可以在我们完成的effect功能上继续扩展。

      computed 初始化

      一步步来,我们先来实现computed的读取值功能。

      const introduction = computed({ // 调用的是defineProperty 中的 // getter get() { console.log('run get'); return author.name + '是个' + author.sex + '程序员!' }, // setter set(newValue) { console.log(newValue); // 注意:我们这里使用的是解构赋值语法 [author.name, author.sex] = newValue.split(' ') } })

      使用原有的 computed API可以发现,计算属性introduction其实是一个ComputedRefImpl类。

      那么我们要做的就是先初始化一个类,然后对外暴露其实例。computed可以接收两种写法,那么我们在接收参数的时候,需要判断下用户传入的是对象还是一个回调函数。

      • 如果接收的是一个回调函数,那么就只存在取值的get,无法设置值;
      • 如果接收的是一个对象,那么就应该存在取值的get和设置值的set

      export const computed = (getterOrOptions) => { let onlyGetter = isFunction(getterOrOptions) // 用户传入的回调 let getter let setter if (onlyGetter) { getter = getterOrOptions setter = () => { console.warn('no set'); } } else { // 用户传入的包含get set函数的对象 getter = getterOrOptions.get setter = getterOrOptions.set } // ref 引用类型 return new ComputedRefImpl(getter, setter) }

      对外暴露的是ComputedRefImpl实例,那么我们初始化还需要在创建一个ComputedRefImpl的类。
      ComputedRefImpl类中应该是一个effect,并且应该可以设置个读取相关的属性。在属性访问器get和set中,它们操作的需要是同一个值,所以我们还需要两个属性访问器操作的同一个值_value,且是私有属性。
      ps:类中的属性访问器get和set,底层调用的是Object.defineProperty

      Vue3 computed如何初始化并获取设置值?

      class ComputedRefImpl { public readonly effect private _value// get和set需要使用的同一个值 constructor(getter, private readonly _setter) { } get value() { return this._value } set value(newValue) { this._setter(newValue) } }

      computed 获取值的实现

      值的展示

      在我们完成初始化之后,页面上其实是不会存在我们期望的内容的。它其实是这样的一个页面:

      必然。我们还没有将用户传入的逻辑进行处理,所以根本看不到呀!

      那我们现在要做的就是先将数据展示到页面上。那我们就先将其effect出来,在读取值的时候执行effect。

      class ComputedRefImpl { public readonly effect private _value// get和set需要使用的同一个值 constructor(getter, private readonly _setter) { this.effect = new ReactiveEffect(getter, () => { }) } get value() { this._value = this.effect.run() // 执行 return this._value } set value(newValue) { this._setter(newValue) } }

      这样页面的数据就可以正常展示了。

      but,我们却忽略了一个问题,在多次读取时,其实并没有computed的缓存功能,只是effect正常获取某个值,获取一次执行一次。

      缓存功能

      那么,我们要做的就是继续完善computed的缓存功能。

      既然computed也是响应式的,那么它是一个effect,同时肯定也需要有一个缓存标识,控制它的缓存特性。如果依赖的属性发生变化,那么这个缓存标识会更新,重新执行get,没有就不重新执行。

      定义一个_dirty,默认应该取值的时候进行计算。一开始为true,默认为新值,更新。当执行完更新的时候应该将其置称false,为false时就默认不执行更新。

      那么除了需要在get读取值的时候进行判断,我们还需要依赖属性发生变化的时候,再去进行判断重新渲染,即在构造函数传入我们的一个调度回调scheduler(我们上篇文章实现的调度函数,在依赖属性发生变化的时候,会执行我们传入的回调函数)。

      class ComputedRefImpl { public readonly effect private _value// get和set需要使用的同一个值 public _dirty = true // 默认应该取值的时候进行计算 constructor(getter, private readonly _setter) { this.effect = new ReactiveEffect(getter, () => { //稍后依赖的属性变化 就会执行此调度函数 if (!this._dirty) { this._dirty = true } }) } get value() { if (this._dirty) { // 脏数据 this._dirty = false this._value = this.effect.run() // 执行 } return this._value } set value(newValue) { this._setter(newValue) } }

      我们可以看到,此时introduction值未发生变化,就只会执行一次effet。

      computed 设置值实现

      修改值,我们可以通过上述用法一来传入一个带有set和get函数的对象,在设置值的时候通过解构的方式进行赋值。

      在 computed 中的set时,我们可以拿到computed的新值,但并没有重新渲染更新页面。此时我们需要做的就是将computed相关的属性进行依赖的收集,并在发生变化的时候进行相应的依赖触发。与effect类似。

      既然类似effect,那我们就需要一个存放依赖的dep(在类中定义public dep = undefined),get时收集,属性变化时触发。

      // ComputedRefImpl 类 get value() { trackEffects(this.dep || (this.dep = new Set())) // 收集 if (this._dirty) { // 脏数据 this._dirty = false this._value = this.effect.run() // 执行 } return this._value }

      // ComputedRefImpl 类 constructor(getter, private readonly _setter) { this.effect = new ReactiveEffect(getter, () => { //稍后依赖的属性变化 就会执行此调度函数 if (!this._dirty) { this._dirty = true // 触发更新 triggerEffects(this.dep) } }) }

      其中收集trackEffects和触发triggerEffects就是将effect中的tracktrigger中部分共用功能提取出来了。

      比如收集的这个功能trackEffects。与effect类似,我们都需要将记录相应的依赖属性和依赖属性的effect,那我们就可以将其独立成一个新的功能函数,降低耦合度。

      export function track(target, type, key) { // 收集effect中 属性对应的effect if (!activeEffect) return let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { depsMap.set(key, (dep = new Set())) } // 判断去重 set中是否存在activeEffect trackEffects(dep) } // 将set存起来 export function trackEffects(dep) { if (!activeEffect) return let shouldTrack = !dep.has(activeEffect) // 不存在 if (shouldTrack) { dep.add(activeEffect) // 属性记录effect // 反向记录 effect记录哪些属性收集过 activeEffect.deps.push(dep) // 让activeEffect 记录住对应的dep 稍后清理会用到 } }

      triggerEffects也是如此:

      export function trigger(target, type, key, value, oldValue) { // 判断targetMap是否存在target // 不存在 直接返回 不需要收集 // 存在 取depsMap中对应key的effect 执行run const depsMap = targetMap.get(target) if (!depsMap) return let effects = depsMap.get(key) if (effects) { triggerEffects(effects) } } export function triggerEffects(effects) { effects = [...effects] // effects 中 set结构删除再添加会导致死循环 effects.forEach(effect => { // 在执行effect时,又要执行自己,需要屏蔽自己的effect if (effect !== activeEffect) { if (effect.scheduler) effect.scheduler() // 如果存在自己的调度函数就执行自己的scheduler else effect.run() // 否则就执行run } }); }

      上述只是vue3中computed的简版实现方式,源码中比我们实现的考虑的要多很多,有兴趣的可以自己去看看(源码路径:packages/reactivity/src/computed.ts):

      最后,我们也可以看到,页面上过了1秒之后,相应的属性变化,页面也同样发生了变化