使用Proxy实现Vue3—watchEffect

1.watchEffect

    watchEffect 是Vue3新增的一个api,其功能与watch类似,均可以在侦听到数据发生变化时执行回调。不同之处在于

  • 1 无需直接指定要监听的数据, 回调函数中使用到哪些数据就监视哪些数据(必须是响应式数据)。
  • 2 默认初始时就会执行一次, 收集所需要监听的数据。
  • 3 无需配置,默认深度侦听。
    下面是其基本使用
<script setup>
  import { reactive, ref, watchEffect } from "vue";
  let firstName = ref("李");
  let lastName = ref("云龙");
  let fullName = "";
  let obj = reactive({
    wife: {
      name: "秀芹",
    },
  });
  watchEffect(() => {
    fullName = firstName.value + lastName.value;
    console.log(fullName);
  });
  watchEffect(() => {
    console.log(obj.wife.name);
  });
  firstName.value = "赵";
  lastName.value = "刚";
  obj.wife.name = "冯楠";
</script>
watchEffect.jpg

从控制台打印结果能够看出,watchEffect默认会执行一次,能够自动收集所需要侦听的数据。数据变化时触发回调,且能实现深度侦听。

2.实现思路

下面让我们梳理一下实现以上功能的大致思路。

  • 首先,watchEffect接收的必须是响应式数据,即我们要确保数据在发生变化时能够被侦听到。这一点我们可以参考Vue数据响应式原理,使用观察者模式,对数据的get和set进行拦截,触发get时进行依赖收集,即将当前属性与其在watchEffect中指定的回调函数进行关联,触发set时执行相应的回调函数。以上操作可以使用ES6的Proxy来实现。

  • 接下来就是watchEffect具体要做的事。由于watchEffect默认会执行一次,因此要做的第一点就是,执行该回调函数。前面讲到watchEffect接收的是响应式数据,而执行该回调函数则一定会访问到所需要的变量,因此就一定会触发依赖收集。之前提到,依赖收集是将当前依赖与其对应的回调函数进行关联。那如何能访问到这个回调函数呢。显然,我们需要一个全局变量,用来存储当前正在被收集的依赖所对应的回调函数。在依赖收集之前将其挂载到全局,以确保触发依赖收集时能够访问到该回调。收集结束则踢出全局,让出位置来给下一个被收集的依赖存储其回调。
    总结一下,watchEffect要做的就是三点。

  • 1 全局挂载回调

  • 2 执行回调

  • 3 踢出回调
    下面依照上述思路来实现。首先使用Proxy实现数据响应化。

3.数据响应化

3.1Proxy

    首先简单介绍一下Proxy。借阮一峰老师的话说,Proxy 可以理解成在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制可以对外界的访问进行过滤和改写。其基本用法如下:

// target是目标对象,handler参数也是一个对象,用来定制拦截行为。
// 返回的是代理对象,后面所有的操作都是操作代理对象而不是目标对象
var proxy = new Proxy(target, handler);

下面演示如何定制基础的拦截行为

const obj = {
  name: "xiaom",
};
const objProxy = new Proxy(obj, {
  get(target, key) {
    console.log("get", key);
    /* Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,
    就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为  */
    return Reflect.get(target, key);
  },
  set(target, key, val) {
    console.log("set", key, val);
    const res = Reflect.set(target, key, val);
    return res;
  },
});

console.log(objProxy.name);
objProxy.name = "xiaom";
console.log(objProxy.name);

// 下面是控制台打印结果
get name
xiaom
set name xiaom
get name
xiaom

可以看到,Proxy使用Reflect对象上对应的方法来完成默认行为,并返回代理对象。关于Proxy的更多用法这里不做详细介绍,后续会继续分享。下面使用Proxy来完成数据的响应化

function observe(obj) {
  if (typeof obj !== "object") {
    return obj;
  }
  return new Proxy(obj, {
    get(target, key, receiver) {
      //console.log(key + '被收集');
      const res = Reflect.get(target, key, receiver); 
      // 依赖收集
      collect(target, key);
      // 深层响应化
      return typeof obj === "object" ? observe(res) : res;
    },
    set(target, key, val, receiver) {
      //console.log("set", key);
      const res = Reflect.set(target, key, val, receiver);
      // 侦听到数据变化,执行相应回调
      update(target, key);
      return res;
    }
  });
}

接下来实现最关键的两步:依赖收集 ,监听执行

3.2 实现依赖收集

    我们首先要明确一点,依赖与其对应的回调函数是如何关联起来的?
    首先想到的是采用键值对的方式存入字典(Map)中。键名是被收集的依赖。键值是所对应的回调函数。显然这样是有缺陷的,那就是当我们对多个对象进行响应式处理时,这多个对象中可能存在同名属性,将会造成覆盖。因此我们应对每一个响应式对象单独创建一个字典。并将这个对象作为键名,所对应的字典作为键值。存入一个全局字典中。但是,我们知道字典是无法将对象作为键名的。因此我们要用到ES6的WeakMap结构。WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。同时还要注意,一个属性(依赖)对应的回调函数可能不止一个。可能再多个watchEfect中都收集了该属性。因此每个属性对应的键值应当是一个Set结构。
    明确了以上所述,开始实现依赖收集。首先创建创建一个全局的WeakMap,以及一个全局变量用于临时存储回调。

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

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

/**
 * @description 实现依赖收集,为属性和其对应的回调函数建立关联
 * @param {Object} target 目标对象
 * @param {any}  key  当前被收集的属性
 */
function collect(target, key) {
  const effect = cb; // 从全局获取当前依赖对应的回调
  if (effect) {
    // 建立target,key和effect之间映射关系
    let depMap = targetMap.get(target);
    // 初始化时,该对象所对应的键值对还不存在,则创建一个map
    if (!depMap) {
      depMap = new Map();
      targetMap.set(target, depMap);
    }
    // 获取该属性对应的的Set
    let deps = depMap.get(key);
    if (!deps) { 
      deps = new Set();
      depMap.set(key, deps);
    }
    deps.add(effect); // 给当前属性添加一个回调
  }
}
3.3 实现侦听执行

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

function update(target, key) {
  // 根据target和key获取对应的set
  // 循环执行
  const depMap = targetMap.get(target);
  if (!depMap) {
    return;
  }
  const deps = depMap.get(key);
  deps.forEach((dep) => dep());
}

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

4.实现watchEffect

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

function watchEffect(fn) {
  cb = fn;
  fn();// 触发依赖收集
  cb = null;
}

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

let firstName = observe({ value: "李" });
let lastName = observe({ value: "云龙" });
let fullName = "";
let obj = observe({
  wife: {
    name: "秀芹",
  },
});
watchEffect(() => {
  fullName = firstName.value + lastName.value;
  console.log(fullName);
});
watchEffect(() => {
  console.log(obj.wife.name);
});
firstName.value = "赵";
lastName.value = "刚";
obj.wife.name = "冯楠";
watchEffect.png

可以看到其能够实现首次执行,依赖收集,侦听执行,深度侦听。行为与Vue的watchEffect一致。
最后附上完整代码

let cb = null;
const targetMap = new WeakMap();
// 数据响应化
function observe(obj) {
  if (typeof obj !== "object") {
    return obj;
  }
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      collect(target, key);
      return typeof obj === "object" ? observe(res) : res;
    },
    set(target, key, val, receiver) {
      const res = Reflect.set(target, key, val, receiver);
      update(target, key);
      return res;
    },
  });
}
// 依赖收集
function collect(target, key) {
  const effect = cb;
  if (effect) {
    let depMap = targetMap.get(target);
    if (!depMap) {
      depMap = new Map();
      targetMap.set(target, depMap);
    }
    let deps = depMap.get(key);
    if (!deps) {
      deps = new Set();
      depMap.set(key, deps);
    }
    deps.add(effect);
  }
}
// 侦听执行
function update(target, key) {
  const depMap = targetMap.get(target);
  if (!depMap) {
    return;
  }
  const deps = depMap.get(key);
  deps.forEach((dep) => dep());
}

function watchEffect(fn) {
  cb = fn;
  fn();
  cb = null;
}

参考:https://es6.ruanyifeng.com/#docs/proxy

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容