【Vue3】源码解析-响应式原理

【Vue3】源码解析

      • 系列文章
      • Proxy API
      • Proxy和响应式对象reactive
      • ref()方法运行原理

系列文章

【Vue3】源码解析-前置
【Vue3】源码解析-响应式原理
【Vue3】源码解析-虚拟DOM

Proxy API

见上

Proxy和响应式对象reactive

在Vue 3中,使用响应式对象方法如下代码所示:

import {ref,reactive} from 'vue'
...
setup(){const name = ref('test')const state = reactive({list: []})return {name,state}
}
...

在Vue 3中,Composition API中会经常使用创建响应式对象的方法ref/reactive,其内部就是利用了Proxy API来实现的,特别是借助handler的set方法,可以实现双向数据绑定相关的逻辑,这对于Vue 2中的Object.defineProperty()是很大的改变,主要提升如下:

  • Object.defineProperty()只能单一的监听已有属性的修改或者变化,无法检测到对象属性的新增或删除(Vue
    2中是采用$set()方法来解决),而Proxy则可以轻松实现。
  • Object.defineProperty()无法监听响应式数据类型是数组的变化(主要是数组长度变化,Vue
    2中采用重写数组相关方法并添加钩子来解决),而Proxy则可以轻松实现。

正是由于Proxy的特性,在原本使用Object.defineProperty()需要很复杂的方式才能实现的上面两种能力,在Proxy无需任何配置,利用其原生的特性就可以轻松实现。

ref()方法运行原理

在Vue 3的源码中,所有关于响应式的代码都在vue-next/package/reactivity下面,其中reactivity/src/index.ts里暴露了所有可以使用的方法。我们以常用的ref()方法举例,来看看Vue 3是如何利用Proxy的。 ref()方法的主要逻辑在reactivity/src/ref.ts中,其代码如下:

...
// 入口方法
export function ref(value?: unknown) {return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {// rawValue表示原始对象,shallow表示是否递归// 如果本身已经是ref对象,则直接返回if (isRef(rawValue)) {return rawValue}// 创建一个新的RefImpl对象return new RefImpl(rawValue, shallow)
}
...

createRef这个方法接收的第二个参数是shallow,表示是否是递归监听响应式,这个和另外一个响应式方法shallowRef()是对应的。在RefImpl构造函数中,有一个value属性,这个属性是由toReactive()方法所返回,toReactive()方法则在reactivity/src/reactive.ts文件中,如下代码所示:

class RefImpl<T> {...constructor(value: T, public readonly _shallow: boolean) {this._rawValue = _shallow ? value : toRaw(value)// 如果是非递归,调用toReactivethis._value = _shallow ? value : toReactive(value)}...
}

在reactive.ts中,则开始真正创建一个响应式对象,如下代码所示:

export function reactive(target: object) {// 如果是readonly,则直接返回,就不添加响应式了if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {return target}return createReactiveObject(target,// 原始对象false,// 是否readonlymutableHandlers,// proxy的handler对象baseHandlersmutableCollectionHandlers,// proxy的handler对象collectionHandlersreactiveMap// proxy对象映射)
}

其中,createReactiveObject()方法传递了两种handler,分别是baseHandlers和collectionHandlers,如果target的类型是Map,Set,WeakMap,WeakSet则会使用collectionHandlers,类型是Object,Array则会是baseHandlers,如果是一个基础对象,也不会创建Proxy对象,reactiveMap则存储所有响应式对象的映射关系,用来避免同一个对象的重复创建响应式。我们在来看看createReactiveObject()方法的实现,如下代码所示:

function createReactiveObject(...) {// 如果target不满足typeof val === 'object',则直接返回targetif (!isObject(target)) {if (__DEV__) {console.warn(`value cannot be made reactive: ${String(target)}`)}return target}// 如果target已经是proxy对象或者只读,则直接返回// exception: calling readonly() on a reactive objectif (target[ReactiveFlags.RAW] &&!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {return target}// 如果target已经被创建过Proxy对象,则直接返回这个对象const existingProxy = proxyMap.get(target)if (existingProxy) {return existingProxy}// 只有符合类型的target才能被创建响应式const targetType = getTargetType(target)if (targetType === TargetType.INVALID) {return target}// 调用Proxy API创建响应式const proxy = new Proxy(target,targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)// 标记该对象已经创建过响应式proxyMap.set(target, proxy)return proxy
}

可以看到在createReactiveObject()方法中,主要做了以下事情:

  • 防止只读和重复创建响应式。
  • 根据不同的target类型选择不同的handler。
  • 创建Proxy对象。

最终会调用new Proxy来创建响应式对象,我们以baseHandlers为例,看看这个handler是怎么实现的,在reactivity/src/baseHandlers.ts可以看到这部分代码,主要实现了这几个handler,如下代码所示:

const get = /*#__PURE__*/ createGetter()
...
export const mutableHandlers: ProxyHandler<object> = {get,set,deleteProperty,has,ownKeys
}

以handler.get为例看看在其内部做了什么操作,当我们尝试读取对象的属性时,便会进入get方法,其核心代码如下所示:

function createGetter(isReadonly = false, shallow = false) {return function get(target: Target, key: string | symbol, receiver: object) {if (key === ReactiveFlags.IS_REACTIVE) { // 如果访问对象的key是__v_isReactive,则直接返回常量return !isReadonly} else if (key === ReactiveFlags.IS_READONLY) {// 如果访问对象的key是__v_isReadonly,则直接返回常量return isReadonly} else if (// 如果访问对象的key是__v_raw,或者原始对象只读对象等等直接返回targetkey === ReactiveFlags.RAW &&receiver ===(isReadonly? shallow? shallowReadonlyMap: readonlyMap: shallow? shallowReactiveMap: reactiveMap).get(target)) {return target}// 如果target是数组类型const targetIsArray = isArray(target)// 并且访问的key值是数组的原生方法,那么直接返回调用结果if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)}// 求值const res = Reflect.get(target, key, receiver)// 判断访问的key是否是Symbol或者不需要响应式的key例如__proto__,__v_isRef,__isVueif (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res}// 收集响应式,为了后面的effect方法可以检测到if (!isReadonly) {track(target, TrackOpTypes.GET, key)}// 如果是非递归绑定,直接返回结果if (shallow) {return res}// 如果结果已经是响应式的,先判断类型,再返回if (isRef(res)) {const shouldUnwrap = !targetIsArray || !isIntegerKey(key)return shouldUnwrap ? res.value : res}// 如果当前key的结果也是一个对象,那么就要递归调用reactive方法对改对象再次执行响应式绑定逻辑if (isObject(res)) {return isReadonly ? readonly(res) : reactive(res)}// 返回结果return res}
}

上面这段代码是Vue 3响应式的核心代码之一,其逻辑相对比较复杂,读者可以根据注释来理解,总结下来,这段代码主要做了以下事情:

  • 对于handler.get方法来说,最终都会返回当前对象对应key的结果即obj[key],所以该段代码最终会return结果。
  • 对非响应式key,只读key等直接返回对应的结果。
  • 对于数组类型的target,key值如果是原型上的方法,例如includes,push,pop等,采用Reflect.get直接返回。
  • 在effect添加收集监听track,为响应式监听服务。
  • 当当前key对应的结果是一个对象时,为了保证set方法能够触发,需要循环递归的对这个对象进行响应式绑定即递归调用reactive()方法。

handler.get方法主要功能是对结果value的返回,那么我们看看handler.set主要做了什么,其代码如下所示:

function createSetter(shallow = false) {return function set(target: object,key: string | symbol,value: unknown,// 即将被设置的新值receiver: object): boolean {// 缓存旧值let oldValue = (target as any)[key]if (!shallow) {// 新旧值转换原始对象value = toRaw(value)oldValue = toRaw(oldValue)// 如果旧值已经是一个RefImpl对象且新值不是RefImpl对象// 例如var v = Vue.reactive({a:1,b:Vue.ref({c:3})})场景的set
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {oldValue.value = value // 直接将新值赋给旧址的响应式对象里return true}}// 用来判断是否是新增key还是更新key的值const hadKey =isArray(target) && isIntegerKey(key)? Number(key) < target.length: hasOwn(target, key)// 设置set结果,并添加监听effect逻辑const result = Reflect.set(target, key, value, receiver)// 判断target没有动过,包括在原型上添加或者删除某些项if (target === toRaw(receiver)) {if (!hadKey) {trigger(target, TriggerOpTypes.ADD, key, value)// 新增key的触发监听} else if (hasChanged(value, oldValue)) {trigger(target, TriggerOpTypes.SET, key, value, oldValue)// 更新key的触发监听}}// 返回set结果 true/falsereturn result}
}

handler.set方法核心功能是设置key对应的值即obj[key] = value,同时对新旧值进行逻辑判断和处理,最后添加上trigger触发监听track逻辑,便于触发effect。 如果读者感觉上述源码理解比较困难,笔者剔除一些边界和兼容判断,将整个流程进行梳理和简化,可以参考下面这段便于理解的代码:

let foo = {a:{c:3,d:{e:4}},b:2}
const isObject = (val)=>{return val !== null && typeof val === 'object'
}
const createProxy = (target)=>{let p = new Proxy(target,{get:(obj,key)=>{let res = obj[key] ? obj[key] : undefined// 添加监听track(target)// 判断类型,避免死循环if (isObject(res)) {return createProxy(res)// 循环递归调用} else {return res}},set: (obj, key, value)=> {console.log('set')obj[key] = value;// 触发监听trigger(target)return true}})return p
}let result = createProxy(foo)result.a.d.e = 6 // 打印出set

当尝试去修改一个多层嵌套的对象的属性时,会触发该属性的上一级对象的get方法,利用这个就可以对每个层级的对象添加Proxy代理,这样就实现了多层嵌套对象的属性修改问题,在此基础上同时添加track和trigger逻辑,就完成了基本的响应式流程。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【每日一题】1670. 设计前中后队列-2023.11.28

题目&#xff1a; 1670. 设计前中后队列 请你设计一个队列&#xff0c;支持在前&#xff0c;中&#xff0c;后三个位置的 push 和 pop 操作。 请你完成 FrontMiddleBack 类&#xff1a; FrontMiddleBack() 初始化队列。void pushFront(int val) 将 val 添加到队列的 最前面…

如何通过“闻香”给葡萄酒分类?

有句话叫做“闻香识女人”&#xff0c;葡萄酒也如同美女&#xff0c;千娇百媚风情万种&#xff0c;所以通过“闻香”也可以给葡萄酒进行分类。 那么&#xff0c;云仓酒庄的品牌雷盛红酒分享葡萄酒都有哪些不同的香呢&#xff1f; 云仓酒庄是云仓酒庄的结合&#xff0c;也就是在…

计算机网络(超详解!) 第二节 物理层(上)

1.物理层的基本概念 物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流&#xff0c;而不是指具体的传输媒体。 物理层的作用是要尽可能地屏蔽掉不同传输媒体和通信手段的差异。 用于物理层的协议也常称为物理层规程(procedure)。 2.物理层的主要任务 主要…

vivado产生报告阅读分析21

其他命令选项 • -of_objects <suggestion objects> &#xff1a; 启用特定建议的报告。在此模式下运行时 &#xff0c; report_qor_suggestions 不会生成新建议。此命令可快速执行 &#xff0c; 读取 RQS 文件后 &#xff0c; 此命令可用于查看其中包 含的建议。其…

Linux下各种字符编码进行转码

支持各种编码相互转换 具体 iconv --list 可以查看 支持的转码格式 1.代码实现 #include <iostream> #include <iconv.h> #include <cstring>int iconv_convert(const char *inCharset, const char *outCharset, char *inbuf, unsigned int inlen, char *o…

流程控制翻转学习

&#x1f4d1;前言 本文主要是【Python】——Python流程控制翻转学习的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 &#x1f304;每…

Leetcode49.字母异位词分组

忘了之前做哪家笔试题遇到过这个题&#xff0c;对哈希表一点印象都没有了&#xff0c;完全懵逼&#xff0c;虽然写出来自己感觉正确的代码了&#xff0c;但是代码太厚重了而且运行还出错了。今天正好在leetcode看见了写一下题解记录一下吧~ 题目描述 给你一个字符串数组&…

OSHI-操作系统和硬件信息库

文章目录 引言一、快速入门1.1 OSHI的简介1.2 引入依赖1.3 涉及的包&#xff08;package&#xff09;1.4 涉及的核心类 二、操作系统信息&#xff1a;OperatingSystem2.1 总揽2.2 文件系统信息&#xff1a;FileSystem2.3 网络参数信息&#xff1a;NetworkParams2.4 进程信息&am…

Docker,从入门到精通

1、DockerFile 介绍 dockerfile 是啥?dockerfile 用来构建 docker 镜像的文件。 具体步骤&#xff1a; 1、编写一个 dockerfile 文件 2、docker build 构造一个镜像 3、docker run 运行镜像 4、docker push 发布镜像 DockerFile 构建过程 1、每个保留关键字都必须是大…

WSDM 2024 | LLMs辅助基于内容的推荐系统增强BPR训练数据

本文提出了一种简单而有效的基于LLMs的图数据增强策略&#xff0c;称为LLMRec&#xff0c;以增强基于内容的推荐系统。LLMRec包含三种数据增强策略和两种去噪策略。数据增强策略包括从文本自然语言的角度挖掘潜在的协同信号, 构建用户画像(LLM-based), 并强化item side informa…

Adobe Illustrator绘图解决卡顿问题

最近在用AI做矢量图&#xff0c;但是遇到了一个很难搞的问题&#xff0c;当我们需要分辨率较高的图片的时候&#xff0c;Python用Matplotlib生成的pdf时dpi参数会设置为600及以上&#xff0c;但是样子的话就造成了pdf文件过大以及AI卡顿&#xff0c;比如&#xff0c;下午生成的…

opencv-python将一个图像中的倾斜矩形调整到方正的位置

要将一个倾斜的矩形物体调整为方正&#xff0c;可以使用OpenCV的cv2.warpPerspective函数进行透视变换。透视变换可以将一个平面上的点映射到另一个平面上。。 透视变换的步骤如下&#xff1a; 1.检测矩形的四个角点坐标。 2.根据四个角点坐标计算出变换矩阵。 3.使用cv2.warp…

DHCP协议与域名系统

计算机网络与协议&#xff1a; 1、DHCP协议 DHCP&#xff08;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议&#xff09;是一个用于局域网的网络协议&#xff0c;位于OSI模型的应用层&#xff0c;使用UDP协议工作&#xff0c;主要用于自动分配IP地址给用…

MongoDB 基础命令介绍

MongoDB 是一个开源、高性能、可扩展的文档数据库。与传统的关系型数据库不同&#xff0c;MongoDB 采用了非常灵活的文档模型&#xff08;BSON 格式&#xff09;&#xff0c;可以轻松地存储和查询各种类型的数据。在 MongoDB 中&#xff0c;我们可以使用一些基础命令来管理数据…

解码 SQL:深入探索 Antlr4 语法解析器背后的奥秘

探寻SQL的背后机制 前言 在数据领域&#xff0c;SQL&#xff08;Structured Query Language&#xff09;是一门广泛使用的语言&#xff0c;用于查询和处理数据。你可能已经使用过诸如MySQL、Hive、ClickHouse、Doris、Spark和Flink等工具来编写SQL查询。 每一种框架都提供了…

CZ‘s ID Card

描述 你有身份证吗?你必须在家庭户口簿上登记身份证号码。从身份证上你可以得到每个人的具体个人信息。这个数字有18位&#xff0c;前17位包含特殊含义:前6位代表你来自哪个地区&#xff0c;后8位代表你的生日。 这里是代表你所在地区的代码。 {{11,"Beijing"},{12,…

F22服装管理软件系统 前台任意文件上传漏洞复现

0x01 产品简介 F22服装管理软件系统是广州锦铭泰软件科技有限公司一款专为服装行业开发的综合性管理软件。该产品旨在帮助服装企业实现全面、高效的管理&#xff0c;提升生产效率和经营效益。 0x02 漏洞概述 F22服装管理软件系统UploadHandler.ashx接口处存在任意文件上传漏洞…

实时天气(预报)API接口

实时天气预报API接口 一、实时天气(预报)API接口二、使用步骤1、接口2、请求参数3、请求参数示例4、接口 返回示例 三、 如何获取appKey和uid1、申请appKey:2、获取appKey和uid 四、重要说明 一、实时天气(预报)API接口 一款帮助你获取实时天气和天气预报的API接口 二、使用步…

对于 ` HttpServletResponse ` , ` HttpServletRequest `我们真的学透彻了吗

对于 **HttpServletResponse , HttpServletRequest**我们真的学透彻了吗 问题引入 PostMapping("/importTemplate") public void importTemplate(HttpServletResponse response) {ExcelUtil<SysUser> util new ExcelUtil<SysUser>(SysUser.class);uti…

RHCSA---基本命令使用

文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 Linux中终端中的很多操作都是通过命令行实现的&#xff0c;最常用的输入命令的方法有以下两种。 (1).打开自带的终端&#xff0c;类似于Windows中的CMD (2).ssh远程连接&#xff0c;关于…