最浅显易懂的 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 ,就可以实现一个响应性了。

相关推荐
reembarkation14 分钟前
使用pdfjs-dist 预览pdf,并添加文本层的实现
前端·javascript·pdf
KenXu30 分钟前
F2C-PTD工具将需求快速转换为代码实践
前端
月阳羊31 分钟前
【硬件-笔试面试题-95】硬件/电子工程师,笔试面试题(知识点:RC电路中的时间常数)
java·经验分享·单片机·嵌入式硬件·面试
给月亮点灯|38 分钟前
Vue3基础知识-setup()、ref()和reactive()
前端·javascript·vue.js
芜青39 分钟前
【Vue2手录12】单文件组件SFC
前端·javascript·vue.js
冷冷的菜哥40 分钟前
react实现无缝轮播组件
前端·react.js·typescript·前端框架·无缝轮播
hrrrrb1 小时前
【Python】字符串
java·前端·python
阿笑带你学前端1 小时前
Supabase云同步架构:Flutter应用的数据同步策略
前端
yinke小琪1 小时前
说说hashCode() 和 equals() 之间的关系
java·后端·面试
梦想CAD控件1 小时前
(在线CAD平台)网页集成CAD SDK的方法
前端·javascript·github