阅读视图

发现新文章,点击刷新页面。

VUE3响应式原理——从零解析

基本概念

在开始讲解响应式原理之前,我们需要知道两个基本概念:

什么是副作用函数?

即该函数的执行影响到其他函数的执行结果,则称该函数为副作用函数。例如:

const obj = { text: 'test' };

function effect() {
    obj.text = ‘hello’;
}

effect()执行后,其他使用到obj.text的函数中,读取到的值将是hello,而不是text,产生了副作用,故称effect()为副作用函数。

什么是响应式数据?

即当某个数据发生变化时,所有使用该数据的地方都发生了变化,则称该数据为响应式数据。例如:

const obj = { text: 'test' };

function effect() {
    ducoment.body.innerText = obj.text;
}

effect();
obj.text = 'hello';

obj.text的值设置为hello后,若body显示的内容由test变为hello,则称obj是一个响应式数据。


如何实现响应式?

通过上述基本概念的举例说明可以看出,响应式数据涉及到了数据的读取(get)和设置(set)操作——副作用函数执行时,进行了读取操作;数据值改变时,进行了设置操作,同时副作用函数被执行。

那怎么样才能保证对数据进行设置操作时,副作用函数被执行呢?可以在读取操作时使用一个容器将副作用函数保存起来,在设置操作时取出副作用函数执行,就实现了最简单的响应式。

Snipaste_2026-02-27_14-43-27.png

Snipaste_2026-02-27_14-43-34.png 在ES2015+以后,Proxy可以实现拦截数据的getset操作,并进行一些特殊处理。

// 副作用函数
function effect() {
    document.getElementById("result").innerHTML = obj.text;
}

const data = { text: "test" };

// 收集副作用函数的容器
const bucket = new Set();

// 响应式数据
const obj = new Proxy(data, {
    get(target, key) {
        // 读取时将副作用函数存入容器
        bucket.add(effect);
        return target[key];
    },
    set(target, key, newVal) {
        target[key] = newVal;
        // 设置后将容器中的副作用函数取出逐一执行
        bucket.forEach((fn) => fn());
        return true;
    },
});

然而,在实际应用过程中,副作用函数名称并不都是effect,可能是其他名称,也可能是一个匿名函数。因此,需要改造一下原有的effect函数,允许其接收一个真正的副作用函数,并存到一个变量中,解决副作用函数名称被硬编码的问题。

// 当前激活的副作用函数
let activeEffect;

// 改造原有的effect函数
function effect(fn){
    activeEffect = fn;
    fn();
}

const data = { text: "test" };

// 收集副作用函数的容器
const bucket = new Set();

// 响应式数据
const obj = new Proxy(data, {
    get(target, key) {
        if (activeEffect) {
            bucket.add(activeEffect);
        }

        return target[key];
    },
    set(target, key, newVal) {
        target[key] = newVal;
        bucket.forEach((fn) => fn());
        return true;
    },
});


如何仅触发特定的副作用函数?

上一节中,已经实现了基本的响应式数据。但如果给obj中原本不存在的属性设置数据后,会发现副作用函数被执行了两次,例如下面这段代码:

effect(() => {
    console.log('执行了副作用函数');
})

function exec() {
    obj.text = 'hello';
    obj.name = '张三';
}

exec();

这和预期不一致——原始数据没有name属性,且副作用函数中未读取该属性,exec()执行到最后一行时,不应触发副作用函数的执行。

通过观察可以发现,objtexteffect呈现一种树状结构: Snipaste_2026-02-26_17-28-35.png

拓展可以得到以下情况: Snipaste_2026-02-27_14-27-09.png

targetkeyeffect是一对多的关系,因此单单使用Set是不满足的,需要调整收集副作用函数的容器的数据结构。

// 当前激活的副作用函数
let activeEffect;

// 改造原有的effect函数
export function effect(fn) {
    activeEffect = fn;
    fn();
}

const data = { text: "test" };

// 收集副作用函数的容器
const bucket = new WeakMap();

// 响应式数据
export const obj = new Proxy(data, {

    get(target, key) {
        if (!activeEffect) {
            return target\[key];
        }

        let depsMap = bucket.get(target);
        if (!depsMap) {
            // 如果不存在,则创建一个新的Map
            bucket.set(target, (depsMap = new Map()));
        }

        let effectsSet = depsMap.get(key);
        if (!effectsSet) {
            // 如果不存在,则创建一个新的Set
            depsMap.set(key, (effectsSet = new Set()));
        }

        effectsSet.add(activeEffect);
        return target[key];
    },

    set(target, key, newVal) {
        target[key] = newVal;

        const depsMap = bucket.get(target);
        // 没有收集到有副作用函数的属性,直接返回
        if (!depsMap) {
            return;
        }

        // 取出与属性绑定的所有副作用函数逐一执行
        const effectsSet = depsMap.get(key);
        effectsSet && effectsSet.forEach((fn) => fn());
        return true;
    },
});
❌