深入Vue3响应式核心:computed 的实现原理与应用

在 Vue3 中,computed是一个看似简单却比较精妙的 API。最主要是能自动缓存计算结果,并在依赖变化时重新触发更新。因此,computed 是如何知道自己的依赖变了,并执行对应的响应式更新函数进行执行的呢?

本文将带你深入 computed 的内部实现和相关核心机制,通过简化的源码分析 "依赖收集 → 脏标记 → 触发更新" 的完整链路。

一、computed 的基本用法

javascript 复制代码
<script setup>
import { reactive, computed } from 'vue'

const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

// 一个计算属性 ref,第一种写法:函数式传参computed(fn)
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})
// 第二种写法:
const publishedBooksMessage = computed({
	get() {
	return author.books.length > 0 ? 'Yes' : 'No'
	},
	set(newVal) {
		author.books = newVal // 更新author.books数据
	}
})
</script>

<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</template>

自动缓存:只要 author.books 不变,多次访问 publishedBooksMessage 不会重复计算

响应式:当 author.books 变化,publishedBooksMessage 的值自动更新

这些特性背后,是一套精细的响应式协作机制。

二、简化版 computed 源码

javascript 复制代码
import { track, trigger } from "./reactive";
import { effect } from "./effect";
import { ref } from "vue";
class computedRefImpl {
    
    constructor(getter, setter) {
        this._value = undefined;
        this._dirty = true; // 进行惰性求值的关键标志
        this.setter = setter;
        this.effect = effect(getter, { // this.effect 执行时在 getter 中依赖的响应式数据会track对应内部设置的Effect实例
            lazy: true, // 默认懒执行
            scheduler: () => { // 依赖的数据更新时会触发执行
                if(!this._dirty) {
                    this._dirty = true;
                    trigger(this, 'value', 'set'); // 触发computed依赖的activeEffect执行
                }
            }
        })
    }
    get value() {
        if(this._dirty) { // '脏值'重新执行getter获取新值
            this._value = this.effect();
            this._dirty = false; // 设置缓存,后续依赖的数据未更新时直接返回原有获取的值
        }
        track(this, 'value'); // track 收集computed依赖的activeEffect
        return this._value;
    }
    set value(newValue) {
        this.setter && this.setter(newValue); // 该函数设置更新响应式依赖的数据后执行scheduler对应computed依赖的activeEffect
    }
}

function computed(getterOrOptions) {
    let getter;

    let setter;

    if(typeof getterOrOptions === 'function'){ // getterOrOptions 为函数时
        getter = getterOrOptions;
    }else{ // 配置对象方式
        getter = getterOrOptions?.get;
        setter = getterOrOptions?.set || (() => {});
    }
    return new computedRefImpl(getter, setter);
}

// 示例1
const a = ref(1);
const b = computed(() => a.value + 1);

effect(() => {

    console.log(b.value); // 输出2

})

setTimeout(() => {
    a.value = 2; // 输出3
}, 1000);



// 示例2
const d = ref(1);
const c = computed({
    get: () => d.value + 1,
    set: (newValue) => {
        d.value = newValue;
    }
})
effect(() => {
    console.log(c.value); // 2  2s后为4
})
setTimeout(() => {
    c.value = 3;
}, 2000);

三、依赖是如何被收集的?

1.示例1中的computed 怎么知道它依赖了 a 数据

在首次读取 .value 时,通过activeEffect 机制自动收集,并在constructor中对依赖的数据收集内部自己的activeEffect ,将来在依赖的数据更新时则会触发scheduler执行从而触发原来 .value 时收集到的activeEffect。

步骤分解:

1.创建 computed 时(lazy: true)

getter 不会立即执行

此时 a 完全不知道 computed 的存在

2.首次访问 b.value

进入 get value() ,因 _dirty = true,执行 this.effect()

this.effect() 即运行对应的getter:() => a.value + 1

收集track computed自己的effect

3.执行 getter 时的关键过程:

activeEffect = this.effect; // 设置当前激活的 effect

const result = getter(); // 访问 a.value

track 内部将 activeEffect(即 computed.effect)加入 a 的依赖列表

结果:a 现在知道变化时通知对应的computed,这就是 "懒依赖收集",只在真正使用时建立依赖,避免无用开销。

四、依赖变化后,如何触发更新?

问题:a.value = 2 后,computed 是怎么知道要重新计算的

答案:通过 scheduler 提前标记 _dirty = true,并执行trigger触发computed已track的effect。

五、为什么需要 _dirty 和惰性求值

假设没有 _dirty 缓存:

javascript 复制代码
// 每次访问都重新计算
get value() {
  return this.effect(); // 每次都执行 getter
}

后果:性能浪费:即使依赖未变,也重复计算

副作用风险:如果 getter 有副作用比如复杂计算会多次执行

而缓存的设计:

只在依赖变化后标记 _dirty = true,只在下次读取时才重新计算

,无人读取则永不计算

这就是 "惰性求值"

六、计算属性缓存 vs 方法

在文章开头所使用的示例,你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:

javascript 复制代码
template
<p>{{ calculateBooksMessage() }}</p>
js
// script
function calculateBooksMessage() {
  return author.books.length > 0 ? 'Yes' : 'No'
}

若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性 list,需要循环一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于 list。没有缓存的话,我们会重复执行非常多次 list 的 getter,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。

七、结语

computed 的实现体现了 Vue 3 响应式系统的三大核心思想:

1.精准依赖追踪:通过 activeEffect + track/trigger 自动追踪依赖

2.惰性与缓存:只在必要时计算,避免无效工作

3.分层解耦:依赖数据作为中间层,向下监听、向上通知,职责清晰

因此,computed不仅是一个简单的api函数,更是响应式编程思想的完美体现。如果有问题也欢迎大家一起在评论区交流沟通!

相关推荐
独自破碎E2 小时前
【滑动窗口】BISHI47 交换到最大
java·开发语言·javascript
剑亦未配妥2 小时前
CSS 折叠引发的 scrollHeight 异常 —— 一次 Blink 引擎的诡异 Bug
前端·css·bug
CappuccinoRose2 小时前
HTML语法学习文档(三)
前端·学习·html·html5·标签·实体字符
0思必得02 小时前
[Web自动化] Selenium获取网页元素在桌面上的位置
前端·python·selenium·自动化
匀泪2 小时前
云原生(企业高性能 Web 服务器(Nginx 核心))
服务器·前端·云原生
国产化创客2 小时前
ESP32平台嵌入式Web前端框架选型分析
前端·物联网·前端框架·智能家居
是欢欢啊3 小时前
全新的table组件,vue3+element Plus
前端·javascript·vue.js
硬汉嵌入式4 小时前
QEMU & FFmpeg作者Fabrice Bellard推出MicroQuickJS,一款面向嵌入式系统JavaScript引擎,仅需10K RAM
javascript·ffmpeg·microquickjs