最浅显易懂的 Vue3 响应性原理

大家好,我是哈默。今天我们来学习一下 vue3 中的 响应性原理

响应性数据和副作用函数

现在,假如我有一个响应性的数据:

js 复制代码
const data = {
  name: "哈默",
  age: 20,
  msg: "hello",
};

页面上有一处 div#app 的容器:

js 复制代码
<div id="app"></div>

那我们可以通过一个副作用函数 effect 将响应性数据里面的 msg 渲染到 div#app 中。

js 复制代码
function effect() {
  document.getElementById("app").innerText = obj.msg;
}

这个时候,我们执行 effect 函数:

js 复制代码
effect();

页面上就可以成功的渲染出来我们的 obj.msg 的值 hello。

那么,现在我们的目标是:

在数据发生改变时,重新渲染视图。

而我们知道,vue3 中是使用 Proxy 来监听数据的读取和设置操作的,所以我们可以将 data 进行改造:

js 复制代码
const data = {
  name: "哈默",
  age: 20,
  msg: "hello",
};

const obj = new Proxy(data, {
  get(target, key) {
    console.log("读取了属性");
    return data[key];
  },
  set(target, key, value) {
    console.log("设置了属性");
    target[key] = value;

    return true;
  },
});

这个时候,我们就可以使用代理对象 obj 来访问被代理对象 data 里的属性或者设置属性:

js 复制代码
obj.msg;
obj.msg = "bye";

控制台中,就会打印出我们读取和设置的行为:

现在,我们想要在设置响应性数据的时候,重新渲染视图。

比如将 obj.msg 从 hello 变成了 bye,我们希望页面上也能从 hello 变成 bye。

那么,我们只需要重新执行 effect 就可以了:

js 复制代码
function effect() {
  document.getElementById("app").innerText = obj.msg;
}

effect 就会把我们目前 obj.msg 的值渲染到对应的 div#app 中。

问题是:我们如何能够在 obj.msg 的值发生改变的时候,重新执行 effect 呢?

我们在设置 obj.msg 的时候,会触发 set 行为:

js 复制代码
set(target, key, value) {
  console.log("设置了属性");
  target[key] = value;

  // TODO: 重新执行 effect

  return true;
}

所以我们只要在 set 行为里重新执行 effect 就好了。

因为我们在第一次将 obj.msg 渲染到页面上的时候,需要获取 obj.msg 的值,才能知道要渲染什么内容到页面上。

那么,在获取 obj.msg 的时候,也就是 get 行为的时候,我们就可以将 effect 存在一个容器里面。

然后我们就可以在设置 obj.msg 的时候,也就是 set 行为时,将容器里的 effect 依次执行。

diff 复制代码
+ const fns = new Set()

const obj = new Proxy(data, {
  get(target, key) {
    console.log("读取了属性");
+   fns.add(effect)
    return data[key];
  },
  set(target, key, value) {
    console.log("设置了属性");
    target[key] = value;

+   fns.forEach(fn => fn())

    return true;
  },
});

来看下效果:

现在,我们就完成了一个最基本的响应性的实现。

但是,现在我们有 2 个问题:

  1. effect 执行的逻辑是硬编码的。
  2. 无论我们修改 obj 的什么属性,都会重新执行 effect。

问题 1

我们现在的 effect 只能做一件事情:

js 复制代码
function effect() {
  document.getElementById("app").innerText = obj.msg;
}

所以,我们需要将其改造一下:

diff 复制代码
+ let activeEffect;

- function effect() {
-   document.getElementById("app").innerText = obj.msg;
- }
+ function effect(fn) {
+  activeEffect = fn;
+  fn();
+ }

const obj = new Proxy(data, {
  get(target, key) {
-   fns.add(effect)
+   fns.add(activeEffect);
    return data[key];
  },
  set(target, key, value) {
    target[key] = value;

    fns.forEach((fn) => fn());

    return true;
  },
});

我们让 effect 接受一个参数 fn,这样我们就可以执行多个副作用函数。

js 复制代码
effect(fn1); // fn1 会更新页面上 p1 元素
effect(fn2); // fn2 会更新页面上 p2 元素

问题 2

我们先来看一下目前修改一个属性的时候,所有 effect 都会执行的现象。

代码:

diff 复制代码
const data = {
  name: "哈默",
  age: 20,
  msg: "hello",
};

let activeEffect;

function effect(fn) {
  activeEffect = fn;

  fn();
}

const fns = new Set();

const obj = new Proxy(data, {
  get(target, key) {
    console.log("读取了属性");
    fns.add(activeEffect);
    return data[key];
  },
  set(target, key, value) {
    console.log("设置了属性");
    target[key] = value;
    fns.forEach((fn) => fn());

    return true;
  },
});

+ effect(() => {
+   console.log("根据 msg 属性的值,渲染 p1 的内容");
+   document.getElementById("p1").innerText = obj.msg;
+ });
+ effect(() => {
+   console.log("根据 age 属性的值,渲染 p2 的内容");
+   document.getElementById("p2").innerText = obj.age;
+ });
+ effect(() => {
+   console.log("根据 name 属性的值,渲染 p3 的内容");
+   document.getElementById("p3").innerText = obj.name;
+ });

setTimeout(() => {
  obj.msg = "bye";
}, 5000);

我们改变 obj.msg 的值,从 hello 改成 bye,在页面上:

我们发现,除了出发了 msg 的重新渲染,age、name 相应的区域也发生了重新渲染。

所以我们需要建立起 effect 和某个对象某个属性之间的联系。

在 vue 中,最后是这样做的:

这样一来,我们的某一个 effect 就可以被放到某一个对象的某一个属性的一个 Set 数组中了。

当我们改变某一个响应性对象的属性,比如 msg 的时候,我们就只需要找到 msg 对应的那个 Set 数组,然后循环执行就可以了。

到这里,我们的响应性系统又进一步完善了一下。

总结

整个响应性的原理就是围绕着 Proxyeffect 来进行的,我们只需要能够在响应性数据发生变化的时候,正确地执行对应的 effect ,就可以实现一个响应性了。

相关推荐
程序员鱼皮17 分钟前
又被 Cursor 烧了 1 万块,我麻了。。。
前端·后端·ai·程序员·大模型·编程
孟祥_成都30 分钟前
nextjs 16 基础完全指南!(一) - 初步安装
前端·next.js
程序员爱钓鱼30 分钟前
使用简单 JSON + 自定义 t 函数实现轻量多语言国际化(无需 next-intl)
前端·javascript·trae
一 乐43 分钟前
助农平台|基于SprinBoot+vue的助农服务系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·ecmascript·springboot
vivo互联网技术1 小时前
浅谈 AI 搜索前端打字机效果的实现方案演进
前端·vue·dom
●VON1 小时前
Electron 小游戏实战:太空打砖块(Space Breakout)
前端·javascript·electron
重铸码农荣光1 小时前
深入理解 JavaScript 原型机制:从“如何拿到小米 SU7”说起
前端·javascript
乐观的用户1 小时前
搞懂虚拟列表实现原理与步骤
前端·vue.js
Heo1 小时前
Webpack高级之常用配置项
前端·javascript·面试
用户84913717547161 小时前
从源码看设计:Java 集合框架的安全性与性能权衡 (基于 JDK 1.8)
java·面试