构建一个自定义的Vue3响应式监听器:watchEffect(附源码)(GPT4加成)

构建一个自定义的Vue3响应式监听器:watchEffect(附源码)

1.watchEffect

watchEffect 是Vue3新增的一个api,其功能在侦听到数据发生变化时执行回调。作为一名 Vue.js 开发者,你肯定会熟悉 watchEffect 这个常用API,与watch不同点在与

  • 1 不需要指定监听的数据, 回调函数中使用到哪些数据就监视哪些数据(类似computed计算属性)。
  • 2 初始时就会执行一次, 收集依赖。
  • 3 默认深度侦听。

2.watchEffect使用方式

ini 复制代码
<script setup>
  import { reactive, ref, watchEffect } from "vue";
  let firstName = ref("张");
  let lastName = ref("三");
  let fullName = "";
  watchEffect(() => {
    console.log(firstName.value + lastName.value);
  });
  firstName.value = "李";
  lastName.value = "四";
</script>

3.思路

  • 在构建一个Vue3风格的响应式监听器时,我们首先要处理的是如何让数据成为"响应式"的。这意味着数据的变化应该能够被系统检测到,并触发相应的操作。为了实现这一点,我们采用观察者模式的原则,通过Proxy这一ES6特性来拦截对数据的读(get)和写(set)操作。在数据被读取时,我们进行所谓的依赖收集,即将访问的属性与watchEffect中定义的回调函数相关联;而当数据被修改时,相应的回调函数将被触发执行。

    具体到watchEffect的实现上,我们需要关注以下几个关键步骤:

    1. 全局回调挂载:这是实现响应式系统的关键一环。我们需要一个全局的变量来暂存当前被处理的依赖项所对应的回调函数。在依赖收集阶段,我们将此回调函数暂存至全局变量中,以便在触发依赖收集时能够顺利访问到这个回调。
    2. 执行回调函数watchEffect的设计使其在首次运行时就执行一次回调函数。这个执行过程不仅仅是简单的函数调用,更重要的是它触发了依赖收集的过程。由于回调函数必然会访问到响应式数据,这样就自然而然地激发了对应数据的依赖收集。
    3. 从全局移除回调:一旦依赖收集完成,当前的回调函数就完成了其任务。此时,我们需要将其从全局变量中移除,为下一个依赖项的回调函数的收集腾出空间。

    通过以上步骤,我们就能构建一个有效的watchEffect函数,它能够在数据变化时自动执行相关的回调,为Vue应用带来动态的响应式特性。

4.数据响应化

4.1 Proxy

下面使用Proxy来完成数据的响应化

javascript 复制代码
// 数据响应化
function createReactive(obj) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }
  return new Proxy(obj, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      track(target, key);
      return typeof result === "object" ? createReactive(result) : result;
    },
    set(target, key, value, receiver) {
      const outcome = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return outcome;
    },
  });
}

4.2 实现依赖收集

当我们构建一个类似于Vue的响应式系统时,一个关键的问题是如何将依赖(即数据属性)与相应的回调函数有效地联系起来。理想情况下,我们想要用一个映射结构(如Map)来存储这种关系:依赖项作为键,对应的回调函数作为值。然而,这种方法面临一个问题:当处理多个响应式对象时,若这些对象有同名属性,则会发生冲突。

为了解决这个问题,我们需要为每个响应式对象独立地存储依赖关系。解决方案是将每个对象与一个专门的映射(Map)关联,这个映射维护着对象属性(依赖项)与回调函数之间的关系。而所有这些对象及其关联的映射被存储在一个全局映射中。但是,标准的Map结构不能有效地处理对象作为键的情况,因为它们不支持垃圾回收机制。

这就是我们选择使用WeakMap的原因。WeakMap是ES6中引入的一种特殊类型的Map,它允许对象作为键名,同时支持垃圾回收。这意味着,如果没有其他引用指向一个对象,该对象就可以被垃圾回收,从而避免内存泄漏。此外,考虑到一个属性可能在多个地方被侦听(即可能有多个回调函数关联到同一属性),我们将每个属性映射到一个Set结构,这个Set中存储了所有相关的回调函数。

实现这一逻辑的步骤如下:

  1. 创建一个全局的WeakMap,用于存储每个响应式对象及其对应的依赖关系映射(即属性和回调函数的映射)。
  2. 设置一个全局变量,用于临时存储当前正在处理的回调函数。

通过这种方式,我们可以高效地管理依赖和回调函数,确保即使在复杂的应用中也能保持良好的性能和内存管理。

javascript 复制代码
// 存储回调全局变量
let currentEffect;
/* 
存储依赖关系的数据结构。它的整体结构是,以需要响应化的对象作为键名,键值是一个map结构。
该Map以对象的每个属性名为键名,键值为set结构。
该Set存储了该属性变化时需要触发的所有回调。
 */
const dependencyMap = new WeakMap();

接着实现依赖收集。过程是为响应式对象的某个属性所对应的集合添加一个回调函数。

ini 复制代码
// 依赖跟踪
function track(target, key) {
  if (currentEffect) {
    let depsMap = dependencyMap.get(target);
    if (!depsMap) {
      depsMap = new Map();
      dependencyMap.set(target, depsMap);
    }
    let dep = depsMap.get(key);
    if (!dep) {
      dep = new Set();
      depsMap.set(key, dep);
    }
    dep.add(currentEffect);
  }
}

4.3 实现侦听执行

接下来实现监听执行。从全局WeakMap中取出当前变更属性所对应的所有回调函数,依次执行 ,这一步比较简单。

ini 复制代码
// 触发更新
function trigger(target, key) {
  const depsMap = dependencyMap.get(target);
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect());
  }
}

至此,数据相应化的工作已经完成,接下来实现watchEffect。

5.实现watchEffect

前面已经总结过,watchEffect只有三点。全局挂载回调,执行回调,踢出回调。

ini 复制代码
// 自定义watchEffect实现
function customWatchEffect(effect) {
  currentEffect = effect;
  effect(); // 依赖收集
  currentEffect = null;
}

至此所有工作都已完成,我们来测试一下。

ini 复制代码
let firstName = createReactive({ value: "张" });
let lastName = createReactive({ value: "三三" });
customWatchEffect(() => {
  console.log(firstName.value + lastName.value);
});
firstName.value = "李";
lastName.value = "四四";

可以看到其能够实现首次执行,依赖收集,侦听->执行,深度侦听。动作与Vue3中的watchEffect一致。

最后附上完整代码

ini 复制代码
let currentEffect;
const dependencyMap = new WeakMap();
​
// 数据响应化
function createReactive(obj) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }
  return new Proxy(obj, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      track(target, key);
      return typeof result === "object" ? createReactive(result) : result;
    },
    set(target, key, value, receiver) {
      const outcome = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return outcome;
    },
  });
}
​
// 依赖跟踪
function track(target, key) {
  if (currentEffect) {
    let depsMap = dependencyMap.get(target);
    if (!depsMap) {
      depsMap = new Map();
      dependencyMap.set(target, depsMap);
    }
    let dep = depsMap.get(key);
    if (!dep) {
      dep = new Set();
      depsMap.set(key, dep);
    }
    dep.add(currentEffect);
  }
}
​
// 触发更新
function trigger(target, key) {
  const depsMap = dependencyMap.get(target);
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect());
  }
}
​
// 自定义watchEffect实现
function customWatchEffect(effect) {
  currentEffect = effect;
  effect();
  currentEffect = null;
}
相关推荐
zwjapple15 分钟前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20202 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem3 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊3 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术3 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing3 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止4 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall4 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴4 小时前
简单入门Python装饰器
前端·python
袁煦丞5 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作