Vue3.js“非原始值”响应式实现基本原理笔记(二)

如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~

作者:前端小王hs

阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主

此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来

书籍:《Vue.js设计与实现》 作者:霍春阳

本篇博文将在书第5.1节5.4节的基础上进一步总结所提到的基础概念,附加了测试的代码运行示例,方便正在学习Vue3想分析Vue3源码的朋友快速阅读

如有帮助,不胜荣幸

前文:
Vue3.js“非原始值”响应式实现基本原理笔记(一)

如何代理Object

“读取”是一个很宽泛的概念(原文)

在之前的笔记中,只简单的讨论了如obj.foo这般获取对象属性值的读取,但读取有很多种,例如下面几种:

  1. 访问属性:obj.foo
  2. 判断:key in obj
  3. 遍历:for (const key in obj)

这一章的内容,就是针对这几种不同的读取,以及其他的常见行为如删除等进行拦截

实现的逻辑主要是看操作符对应的拦截函数

在书中是通过查阅ECMA规范,明确操作符运行逻辑,进而找到操作符的运算结果是调用什么抽象方法,然后通过这个抽象方法找到对应的内部方法,进而对比Vue3.js“非原始值”响应式实现基本原理笔记(一)提到的Proxy内部方法表,选取对应的方法拦截

拦截 in

下面in操作符为例,我们来看一下书中是如何逐步找到拦截方法

ECMA-262规范的13.10.1(原文),找到in操作符的运行时逻辑:

  1. 让 lref 的值为 RelationalExpression 的执行结果。
  2. 让 lval 的值为 ? GetValue(lref)。
  3. 让 rref 的值为 ShiftExpression 的执行结果。
  4. 让 rval 的值为 ? GetValue(rref)。
  5. 如果 Type(rval) 不是对象,则抛出 TypeError 异常。
  6. 返回 ? HasProperty(rval, ?
    ToPropertyKey(lval))。

关键是第6步,出现了HasProperty(),然后在在ECMA-262规范的7.3.11(原文)找到关于这个方法的逻辑:

  1. 断言:Type(O) 是 Object。
  2. 断言:IsPropertyKey§ 是 true。
  3. 返回 ? O.[[HasProperty]] §。

可以发现这个内部方法[[HasProperty]],然后在表中找到对应的拦截函数——has,如下图所示:

image.png

然后就可以在Proxyhandler中使用has进行拦截了,代码如下:

const obj = { foo: 1 }
const p = new Proxy(obj, {
  has(target, key) {
    track(target, key)
    return Reflect.has(target, key)
  }
})

effect(() => {
  'foo' in p // 将会建立依赖关系
})

第5章有非常多的关于ECMA的运行时逻辑,在书中没有解释,所以笔者在这里还是简单介绍一下一些关键词(以上述in的运行时逻辑为例):

  1. RelationalExpression:关系表达式,例如<>=ininstanceof<=>=等操作符,在这里指的操作符in左侧的表达式
  2. **ShiftExpression**:位移操作的表达式,操作符in右侧的表达式
  3. lreflvalRelationalExpression会生成一个引用lref,然后通过GetValue(lref)得到lval
  4. rreflref:逻辑同上

拦截 for…in(遍历所有可枚举属性)

逻辑和in是相同的,找规范找到最后发现可以使用ownKey()去拦截

但是在ownKey中会做一些处理,代码如下:

const obj = { foo: 1 }
const ITERATE_KEY = Symbol()

const p = new Proxy(obj, {
  ownKeys(target) {
    // 将副作用函数与 ITERATE_KEY 关联
    track(target, ITERATE_KEY)
    return Reflect.ownKeys(target)
  }
})

注:这本书的特点就是类似于电视连续剧,整一章节的内容是不断累积的,所以的时候不要间断,同时要多复习

在之前的笔记中,track是传入targetkey,代码如下:

function track(target, key) {
  // 没有 activeEffect,直接 return
  if (!activeEffect) return;
  let depsMap = bucket.get(target);
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  const array = Array.from(deps);
  deps.add(activeEffect);
  activeEffect.deps.push(deps);
}

现在变成了传入ITERATE_KEYiterate的意思是重复

为什么传入ITERATE_KEY?

因为ownKeys是用来获取一个对象的所有键,不是与任何具体的进行绑定,所以就定义一个ITERATE_KEY作为标识,去与副作用函数进行绑定

那么当触发的时候,也同样需要传入ITERATE_KEY,代码如下:

trigger(target, ITERATE_KEY)

这里需要注意的是,遍历是遍历,触发拦截是触发拦截,整个过程只会触发一次ownKeys,可以理解为执行到for..in时就触发拦截,然后for...in循环遍历自身可枚举的属性

添加属性对for…in的影响

在上述代码中,当执行p.bar=2时,for...in就会由循环一次变为两次(因为obj变为了两个),但此时不会触发与ITERATE_KEY关联的副作用函数

原因非常简单,执行p.bar=2,关联的是与bar相关联的副作用函数

解决的方法也非常简单,在trigger中把两者都添加到effectsToRun

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  // 取得与 key 相关联的副作用函数
  const effects = depsMap.get(key)
  // 取得与 ITERATE_KEY 相关联的副作用函数
  const iterateEffects = depsMap.get(ITERATE_KEY)

  const effectsToRun = new Set()
  // 将与 key 相关联的副作用函数添加到 effectsToRun
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  // 将与 ITERATE_KEY 相关联的副作用函数也添加到effectsToRun
  iterateEffects && iterateEffects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })

  effectsToRun.forEach(effectFn => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
}

这里的ITERATE_KEY是外部定义的Symbol

target即遍历的obj

修改属性对foo…in的影响

修改属性不会对foo...in产生影响,但需要注意的是修改属性新增属性使用的都是[[Set]],所以需要做个区分,代码如下:

const p = new Proxy(obj, {  
  // 拦截设置操作
  set(target, key, newVal, receiver) {
    // 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
    const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD';
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver);
    // 将 type 作为第三个参数传递给 trigger 函数
    trigger(target, key, type);
    return res;
  },
  // 省略其他拦截函数
});

然后再在trigger中进行判断,如果是ADD时才执行depsMap.get(ITERATE_KEY),代码如下:

function trigger(target, key, type) {  
  // 省略其他逻辑
  // 只有当操作类型为 'ADD' 时,才触发与 ITERATE_KEY 相关联的副作用函数  
  if (type === 'ADD') {  
    const iterateEffects = depsMap.get(ITERATE_KEY);  
    iterateEffects && iterateEffects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn);
      }
    });
  }
}

在书中还提到了定义枚举类型的重要性:

const TriggerType = {
  SET: 'SET',
  ADD: 'ADD'
};

便于后期维护

删除属性对for…in的影响

在书中通过查阅规范,得知是通过deleteProperty去拦截的,所以代码如下:

const p = new Proxy(obj, {  
  deleteProperty(target, key) {  
    // 检查被操作的属性是否是对象自己的属性  
    const hadKey = Object.prototype.hasOwnProperty.call(target, key);  
    // 使用 Reflect.deleteProperty 完成属性的删除  
    const res = Reflect.deleteProperty(target, key);  
    if (res && hadKey) {  
      // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新  
      trigger(target, key, 'DELETE');  
    }  
    return res;  
  }  
});

这里手写是进行了一个判断,删除的属性是否属于自身,这是因为执行的逻辑可能是如delete p.a,执行了但是这个属性不存在,所以需要进行一个判断

那么最后就是在trigger中继续加多一个type判断,代码如下:

function trigger(target, key, type) {  
  // 省略其他逻辑
  // 只有当操作类型为 'ADD' 或 'DELETE' 时才触发 
  if (type === 'ADD' || type === 'DELETE') {  
    const iterateEffects = depsMap.get(ITERATE_KEY);  
    iterateEffects && iterateEffects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn);
      }
    });
  }
}

其实看到这里,能够发现在设计时是从CURD去考虑的不同情况

总结

这篇笔记主要复习了不同读取情况下的响应式实现:

  1. 如何拦截in操作符
  2. 如何拦截for…in操作符
  3. 当新增、修改和删除时如何实现响应式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/780009.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32F103C8T6核心板原理图和PCB分享

PCB图 原理图 资料下载地址&#xff1a; 原理图PCB库: https://545c.com/d/45573183-61875742-29897c?p7526 (访问密码: 7526)

第3章.中央服务器的物联网模式--企业系统集成

为了从物联网实施中获得最大价值&#xff0c;物联网系统需要与企业中的现有软件系统集成。事实上&#xff0c;与外部系统的集成允许网络世界和物理世界之间的交互——代表物理世界的物联网系统和驻留在网络/虚拟世界中的外部系统。用于此模式的符号如下图所示&#xff1a; 图3.…

mac怎么压缩pdf文件大小,mac压缩pdf文件大小不改变清晰度

在数字化时代&#xff0c;pdf格式因其良好的兼容性和稳定性&#xff0c;成为了文档分享和传输的首选。然而&#xff0c;随着文件内容的丰富&#xff0c;pdf文件的体积也越来越大&#xff0c;给存储和传输带来了不小的困扰。本文将揭秘几种简单有效的pdf文件压缩方法&#xff0c…

图神经网络实战(16)——经典图生成算法

图神经网络实战&#xff08;16&#xff09;——经典图生成算法 0. 前言1. 图生成技术2. Erdős–Rnyi模型3. 小世界模型小结系列链接 0. 前言 图生成算法是指用于创建模拟图或网络结构的算法&#xff0c;这些算法可以根据特定的规则和概率分布生成具有特定属性的图&#xff0c…

SCI一区级 | Matlab实现BO-Transformer-BiLSTM时间序列预测

SCI一区级 | Matlab实现BO-Transformer-BiLSTM时间序列预测 目录 SCI一区级 | Matlab实现BO-Transformer-BiLSTM时间序列预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.【SCI一区级】Matlab实现BO-Transformer-BiLSTM时间序列预测&#xff0c;贝叶斯优化Transfor…

C++_STL---list

list的相关介绍 list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。 list的底层是带头双向循环链表结构&#xff0c;链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向其前一个元素和后一个元素。…

WAIC | 上海人形机器人创新中心 | 最新演讲 | 详细整理

前言 笔者看了7月4号的人形机器人与具身智能发展论坛的直播&#xff0c;并在7月5日到了上海WAIC展会现场参观。这次大会的举办很有意义&#xff0c;听并看了各家的最新成果&#xff0c;拍了很多照片视频&#xff0c;部分演讲也录屏了在重复观看学习 稍后会相继整理创立穹彻智…

[c++] 可变参数模版

前言 可变参数模板是C11及之后才开始使用,学校的老古董编译器不一定能用 相信大家在刚入门c/c时都接触过printf函数 int printf ( const char * format, ... ); printf用于将数据格式化输出到屏幕上,它的参数非常有意思,可以支持任意数量,任意类型的多参数.而如果我们想实现类…

【Java探索之旅】继承概念_语法_父类的成员访问

文章目录 &#x1f4d1;前言一、继承1.1 继承的概念1.2 继承语法1.3 继承发生后 二、父类的访问2.1 父类成员变量访问2.2 父类成员方法访问 &#x1f324;️全篇总结 &#x1f4d1;前言 在面向对象编程中&#xff0c;继承是一种重要的概念&#xff0c;它允许我们创建一个类&…

[go-zero] 简单微服务调用

文章目录 1.注意事项2.服务划分及创建2.1 用户微服务2.2 订单微服务 3.启动服务3.1 etcd 服务启动3.2 微服务启动3.3 测试访问 1.注意事项 go-zero微服务的注册中心默认使用的是Etcd。 本小节将以一个订单服务调用用户服务来简单演示一下&#xff0c;其实订单服务是api服务&a…

VSCode设置好看清晰的字体!中文用鸿蒙,英文用Jetbrains Mono

一、中文字体——HarmonyOS Sans SC 1、下载字体 官网地址&#xff1a;https://developer.huawei.com/consumer/cn/design/resource/ 直接下载&#xff1a;https://communityfile-drcn.op.dbankcloud.cn/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20230517…

昇思25天学习打卡营第18天 | K近邻算法实现红酒聚类

1、实验目的 了解KNN的基本概念&#xff1b;了解如何使用MindSpore进行KNN实验。 2、K近邻算法原理介绍 K近邻算法&#xff08;K-Nearest-Neighbor, KNN&#xff09;是一种用于分类和回归的非参数统计方法&#xff0c;最初由 Cover和Hart于1968年提出(Cover等人,1967)&#…

Golang | Leetcode Golang题解之第220题存在重复元素III

题目&#xff1a; 题解&#xff1a; func getID(x, w int) int {if x > 0 {return x / w}return (x1)/w - 1 }func containsNearbyAlmostDuplicate(nums []int, k, t int) bool {mp : map[int]int{}for i, x : range nums {id : getID(x, t1)if _, has : mp[id]; has {retu…

ctfshow web sql注入 web242--web249

web242 into outfile 的使用 SELECT ... INTO OUTFILE file_name[CHARACTER SET charset_name][export_options]export_options:[{FIELDS | COLUMNS}[TERMINATED BY string]//分隔符[[OPTIONALLY] ENCLOSED BY char][ESCAPED BY char]][LINES[STARTING BY string][TERMINATED…

【三级等保】等保整体建设方案(Word原件)

建设要点目录&#xff1a; 1、系统定级与安全域 2、实施方案设计 3、安全防护体系建设规划 软件全文档&#xff0c;全方案获取方式&#xff1a;本文末个人名片直接获取。

数据结构——二叉树相关题目

1.寻找二叉树中数值为x的节点 //寻找二叉树中数值为x的节点 BTNode* TreeFind(BTNode* root, BTDataType x)//传过来二叉树的地址和根的地址&#xff0c;以及需要查找的数据 {if (root Null){return Null;}//首先需要先判断这个树是否为空&#xff0c;如果为空直接返回空if (…

基于python的数据分解-趋势-季节性-波动变化

系列文章目录 前言 时间序列数据的分解&#xff0c;一般分为趋势项&#xff0c;季节变化项和随机波动项。可以基于加法或者乘法模型。季节变化呈现出周期变化&#xff0c;因此也叫季节效应(周期&#xff09;。 一、数据分解步骤 &#xff08;1&#xff09;估计时间序列的长期…

拓扑排序,PageRank(markov),实对称矩阵等

拓扑排序 多件事情有先后顺序&#xff0c;如何判断哪个先哪个后 拓扑排序算法&#xff1a; 1.读入图时&#xff0c;需要记录每个顶点的入度&#xff0c;以及相邻的所有顶点 2.将入度为0的顶点入队&#xff08;先进先出&#xff09; 3.取出队首元素a&#xff0c;&#xf…

rocketmq-console可视化界面功能说明

rocketmq-console可视化界面功能说明 登录界面OPS(运维)Dashboard(驾驶舱)Cluster(集群)Topic(主题)Consumer(消费者)Producer(生产者)Message(消息)MessageTrace(消息轨迹) rocketmq-console是rocketmq的一款可视化工具&#xff0c;提供了mq的使用详情等功能。 本章针对于rock…

基于springboot+vue+uniapp的高校宿舍信息管理系统小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…