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

相关推荐
测试199825 分钟前
2024软件测试面试热点问题
自动化测试·软件测试·python·测试工具·面试·职场和发展·压力测试
栈老师不回家36 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙42 分钟前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
马剑威(威哥爱编程)1 小时前
MongoDB面试专题33道解析
数据库·mongodb·面试
小远yyds1 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
帅比九日3 小时前
【HarmonyOS Next】封装一个网络请求模块
前端·harmonyos