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

基本概念

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

什么是副作用函数?

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

js 复制代码
const obj = { text: 'test' };

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

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

什么是响应式数据?

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

js 复制代码
const obj = { text: 'test' };

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

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

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


如何实现响应式?

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

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

在ES2015+以后,Proxy可以实现拦截数据的get、set操作,并进行一些特殊处理。

js 复制代码
// 副作用函数
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函数,允许其接收一个真正的副作用函数,并存到一个变量中,解决副作用函数名称被硬编码的问题。

js 复制代码
// 当前激活的副作用函数
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中原本不存在的属性设置数据后,会发现副作用函数被执行了两次,例如下面这段代码:

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

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

exec();

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

通过观察可以发现,obj、text、effect呈现一种树状结构:

拓展可以得到以下情况:

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

js 复制代码
// 当前激活的副作用函数
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;
    },
});
相关推荐
hai_android7 小时前
一个公式看懂动态跨表查找:P8 + VLOOKUP
前端·javascript·vue.js
wdssmq9 小时前
「折腾」Vue 简单的状态管理实践
vue.js
Q264336502312 小时前
【有源码】基于Spring Boot和Vue的在线考试与交流系统-教育信息化背景下在线考试与交流一体化平台
java·vue.js·spring boot·mysql·spring·毕业设计·课程设计
苹果二13 小时前
使用Vue的指南
前端·javascript·vue.js
害人终害己14 小时前
‘vue-cli-service‘ 不是内部或外部命令,也不是可运行的程序或批处理文件。
前端·javascript·vue.js
excel1 天前
研究 Vue 3 源码的收获
前端·vue.js
vx-程序开发1 天前
【计算机毕设】51300童装超市管理系统的设计与实现
java·vue.js·spring boot·源码·课程设计·毕设·大作业
ym hyd 1111 天前
题库建立与考试系统源码 Java+SpringBoot+Vue3 前后分离
java·开发语言·vue.js·spring boot·毕设
YZ1225521 天前
【Docker专题】使用Docker部署Vue-Flask项目前后端分离版【前端Docker部署】
前端·vue.js·docker
专业程序开发源2 天前
springboot古城景区管理系统88564-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计