vue3 源码解析(1)— reactive 响应式实现

前言

本文是 vue3 源码解析系列的第一篇文章,项目代码的整体实现是参考了 v3.2.10 版本,项目整体架构可以参考之前我写过的文章 rollup 实现多模块打包。话不多说,让我们通过一个简单例子开始这个系列的文章。

举个例子

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>reactive</title>
</head>
<body>
<div id="app"></div>
<!--响应式模块的代码-->
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>let { reactive, effect } = VueReactivity;const user = {name: 'Alice',age: 25,address: {city: 'New York',state: 'NY'}};let state = reactive(user)effect(() => {app.innerHTML = state.address.city});setTimeout(() => {state.address.city = 'California'}, 1000);
</script>
</body>
</html>

在这里插入图片描述

通过例子可以看到1s之后改变数据视图也跟随变,在 vue3 中是那如何实现这一效果的呢?我们先从例子中的 reactive 函数出发。

实现响应式

有了解过 vue3 源码的知道实现响应式的核心是 proxy ,这里关于 proxy 的方法暂时不做过多的介绍也不是本文的重点所在,这里先简单看下核心函数 reactive 代码实现。

reactive

reactive 函数返回一个对象的响应式代理。并且响应式的转换是“深度”的,它影响所有嵌套属性。一个响应式对象还会深度展开任何是 ref 的属性,同时保持响应式。

import { reactiveHandlers } from './baseHandlers'const reactiveMap = new WeakMap()function reactive (target) {return createReactiveObject(target, reactiveHandlers, reactiveMap)
}function createReactiveObject (target, baseHandlers, proxyMap) {if (!isObject(target)) {return target}// target already has corresponding Proxyconst existingProxy = proxyMap.get(target)if (existingProxy) {return existingProxy}const proxy = new Proxy(target, baseHandlers)// WeakMapproxyMap.set(target, proxy)return proxy
}

需要注意的是:

  1. 如果一个对象已经被创建成响应式对象了,则直接返回该响应式对象,避免重复代理同一个目标对象。

  2. 使用 WeakMap 可以避免内存泄漏问题,因为当目标对象 target 不再被引用时,其对应的代理对象 existingProxy 也会被自动垃圾回收。

reactiveHandlers

reactiveHandlers 是实现响应式数据的关键对象,它通过拦截对象的操作来实现对数据的监听和更新。

const reactiveHandlers = {get,set
}

reactiveHandlers 中包含了一些处理数据的方法,比如 getset

get

当我们访问某个响应式对象的属性时,get 方法会被触发,它会对该属性进行依赖收集,将响应式对象和该属性建立联系。

import { isObject } from '@vue/shared'const get = createGetter()function createGetter (isReadonly = false, shallow = false) {return function get (target, key, receiver) {const res = Reflect.get(target, key, receiver)track(target, 'get', key)  // 收集依赖// 懒代理if (isObject(res)) {return reactive(res) // 递归}return res}
}

这里会有一个懒代理具体的好处包括:

  1. 性能优化:代理只会在需要的时候触发,避免了不必要的代理和响应式数据更新,提高了组件渲染的性能。

  2. 减少不必要的内存开销:只有在需要时才会创建响应式数据对象,避免了不必要的内存开销。

  3. 更加灵活:懒代理可以更细粒度地控制哪些数据需要进行代理和哪些数据不需要进行代理,可以更加灵活地处理组件状态。

set

当我们修改某个响应式对象的属性时,set 方法会被触发,它会更新该属性的值,并通知所有依赖该属性的组件进行更新。

import { hasChanged } from '@vue/shared'const set = createSetter()function createSetter (shallow = false) {return function set (target, key, value, receiver) {// 注意先读取后设置const oldValue = Reflect.get(target, key)const result = Reflect.set(target, key, value, receiver)// 修改if (hasChanged(value, oldValue)) {// 触发更新trigger(target, 'set', key, value, oldValue)}return result}
}

其中函数的参数 shallow 表示是否进行浅层响应式处理。这里后续会提到。

副作用函数

除了上述响应式相关的代码,还有一个更为重要的方法就是副作用函数 effect

effect

effect 函数主要用于创建响应式数据的副作用函数,该函数接受一个函数作为参数。该函数会立即运行一次。其中任何响应式属性被更新时,函数将再次运行。

type ReactiveEffectOptions = {lazy?: booleanscheduler?: (...args: any[]) => any
}function effect (fn, options?: ReactiveEffectOptions) {const _effect = createReactiveEffect(fn, options)if (!options || !options.lazy) {_effect() // 默认执行}return _effect
}

使用 effect 函数的好处是,能够简化响应式数据与副作用之间的代码关系,使得代码更加易于理解和维护。同时,effect 函数还支持配置项:

  • lazy:是否延迟执行副作用函数。
  • scheduler:指定异步任务调度器,可用于节流、防抖等。

createReactiveEffect

createReactiveEffect 函数的作用是创建一个具有响应式能力的函数。

let uid = 0
let activeEffect // 保存当前的effect
let effectStack = [] // 定义一个栈结构,解决effect嵌套的问题function createReactiveEffect (fn, options) {const effect = function reactiveEffect () {// 保证effect唯一性if (!effectStack.includes(effect)) {try {effectStack.push(effect)activeEffect = effectreturn fn() // 执行用户定义的方法并返回值} finally {effectStack.pop()activeEffect = effectStack[effectStack.length - 1]}}}effect.id = uid++ // 区别effecteffect.fn = fn // 保存用户方法effect.options = options // 保存用户属性return effect
}

在这段代码主要作用是:

  1. 定义 effect 函数,该函数代表了一个具有响应式能力的函数,并且会在响应式状态发生变化时被调用。

  2. effect 函数中,通过 effectStack 数组来保证 effect 函数的唯一性。每次执行effect 函数之前,都会将 effect 函数压入 effectStack 数组中,以此来判断当前是否已经存在同样的 effect 函数。如果不存在,则将当前 effect 函数设置为 activeEffect,执行用户定义的方法 fn() 并返回结果。这个过程中,使用 try...finally 语句块来确保 effectStack 数组可以被正确地维护。

  3. 最后,为 effect 函数添加一些属性,包括 idfnoptions,并将 effect 函数返回。

总之,这段代码的主要目的是为 vue3 的响应式系统中的副作用函数 effect 创建一个具有响应式能力的版本,并保证了每个 effect 函数的唯一性。

依赖收集与派发更新

在创建完响应式对象之后如何在数据更新的时候重新执行 effect 函数呢?这里就需要用到依赖收集(track)与派发更新(trigger)。

track

track 函数作用是追踪响应式对象中属性的访问,并将当前的依赖关系与属性关联。

let targetMap = new WeakMap()function track (target, type, key) {if (activeEffect === undefined) return// 获取effectlet depsMap = targetMap.get(target)if (!depsMap) {targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key)if (!dep) {depsMap.set(key, (dep = new Set()))}if (!dep.has(activeEffect)) {dep.add(activeEffect)}
}

在这段代码主要作用是实现了跟踪(track)依赖项(target)和副作用(activeEffect)之间的关系。

trigger

trigger 函数用于触发一个响应式对象上的更新,它的作用是让 vue3 知道该响应式对象已经进行了变化,从而重新渲染相关视图。

function trigger (target, type, key, newValue?, oldValue?) {const depsMap = targetMap.get(target)if (!depsMap) {// never been trackedreturn}let deps = []// schedule runs for SET | ADD | DELETEif (key !== undefined) {deps.push(depsMap.get(key)) // [Set[activeEffect]]}// 副作用函数const effects = []for (const dep of deps) {if (dep) {effects.push(...dep)}}for (const effect of effects) {effect()}
}

在这段代码主要作用是:

  1. 函数会从 targetMap 中获取与目标对象关联的依赖关系图 depsMap。如果 depsMap 不存在,即目标对象从未被追踪过,那么就直接返回。

  2. 再根据操作类型和属性名,从 depsMap 中获取与之相关的依赖集合(dep)。如果 dep 存在,就将其加入 deps 数组中。最后,通过 effects 数组收集所有与 deps 集合相关的副作用函数,然后依次调用它们,完成重新渲染视图的工作。

需要注意的是,deps 集合中存储的是 effect 而不是具体的响应式对象。这是因为一个副作用函数可以同时依赖多个响应式对象,因此在触发更新时,需要先从 depsMap 中获取与目标对象相关的 deps 集合,然后再从 dep 集合中获取所有副作用函数,最后统一执行它们。

执行过程

本文所涉及的代码已基本编写完成,为了更好的理解每个函数是如何执行的,我们可以通过 debugger 来调试一下。

初始化

在第一次执行之后以下全局变量对应的值。

reactiveMap

reactiveMap 对应的数据如下图所示。
在这里插入图片描述

targetMap

effect 函数执行过程中 activeEffect 函数会一直存在,此时依赖收集完之后对应的 targetMap 数据如下图所示。
在这里插入图片描述
需要注意的是: effect 执行完之后会从 effectStack 数组中删除栈顶元素,并将数组的最后一个元素作为当前激活的 effect(即 activeEffect)。这通常在处理依赖项收集时使用,当一个 effect 被触发时,它会将自己推入 effectStack 中,当它完成执行后,会从 effectStack 中弹出自己,然后 activeEffect 变为新的栈顶元素。这样做可以保证在 effect 执行期间,新的 effect 触发时不会干扰当前正在执行的 effect

数据更新时

触发 get

当访问 state.address 时又会触发 get ,同时也会触发 track ,由于此时 effect 已经执行完当前没有激活状态下的 effect ,所以此处不会再次进行依赖收集。在触发 get 时对应的数据如下图所示。

在这里插入图片描述
需要注意的是:通过 Reflect.get 返回的 res 是一个对象此时又会触发 reactive 但此时的返回值已经存在于 reactiveMap 中,所以不会重复进行响应式的处理。

触发 set

执行 address.city = 'California' 会触发 set ,此时新值和旧值不一样会触发 trigger。在触发 set 时对应的数据如下图所示。

在这里插入图片描述

触发更新

触发 trigger 时会从 targetMap 的子项 depsMap 中获取对应的 effect 函数并执行。

在这里插入图片描述

再次执行 effect

由于数据的改变 effect 函数会被再一次触发,再次访问 state.address 同样会触发 get 与第一次不同的是此时的数据已经被更新为最新的值。与此同时 activeEffect 也会被再一次赋值,此时对应的数据如下图所示。

在这里插入图片描述
由于第一次访问的时候对应的值已经被缓存,再次访问的时候就不会重复进行依赖收集和响应式的处理,而是直接返回最新的值。

总结

综上所述,vue3 的响应式原理通过使用 Proxy 对象实现数据代理,结合副作用函数和依赖追踪,实现了高效的数据变化追踪和自动更新机制。这种设计使得 vue3 在处理数据和视图之间的关系时更加灵活和高效。

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

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

相关文章

电力通信与泛在电力物联网技术的应用与发展-安科瑞黄安南

摘要&#xff1a;随着我国社会经济的快速发展&#xff0c;我国科技实力得到了非常大的提升&#xff0c;当前互联网通信技术在社会中得到了广泛的应用。随着电力通信技术的快速发展与更新&#xff0c;泛在电力物联网建设成为电力通讯发展的重要方向。本文已泛在电力物联网系统为…

无中微子双贝塔衰变

无中微子双贝塔衰变 无中微子双贝塔衰变的原子核理论 双贝塔衰变的研究缘起 玛丽亚格佩特-梅耶&#xff08;Maria Goeppert-Mayer&#xff09;在1935年提出了双贝塔衰变的可能性埃托雷马约拉纳&#xff08;Ettore Majorana&#xff09;在1937年证明了如果中微子是否是Majorana…

PCL 透视投影变换(OpenGL)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 在现实生活中,我们总会注意到离我们越远的东西看起来更小。这个神奇的效果被称之为透视(Perspective)。透视的效果在我们看一条无限长的高速公路或铁路时尤其明显,正如下面图片显示的这样: 由于透视的原因,平行线…

CVE-2023-46227 Apache inlong JDBC URL反序列化漏洞

项目介绍 Apache InLong&#xff08;应龙&#xff09;是一站式、全场景的海量数据集成框架&#xff0c;同时支持数据接入、数据同步和数据订阅&#xff0c;提供自动、安全、可靠和高性能的数据传输能力&#xff0c;方便业务构建基于流式的数据分析、建模和应用。 项目地址 h…

以太网链路聚合与交换机堆叠,集群

目录 以太网链路聚合 一.链路聚合的基本概念 二.链路聚合的配置 1.手工模式 2.LACP模式 系统优先级 接口优先级 最大活动接口数 活动链路选举 负载分担 负载分担模式 三.典型使用场景 交换机之间 交换机和服务器之间 交换机和堆叠系统 防火墙双机热备心跳线 四…

I/O 模型学习笔记【全面理解BIO/NIO/AIO】

文章目录 I/O 模型什么是 I/O 模型Java支持3种I/O模型BIO&#xff08;Blocking I/O&#xff09;NIO&#xff08;Non-blocking I/O&#xff09;AIO&#xff08;Asynchronous I/O&#xff09; BIO、NIO、AIO适用场景分析 java BIOJava BIO 基本介绍Java BIO 编程流程一个栗子实现…

10个基于.Net开发的Windows开源软件项目

1、基于.NET的强大软件开发工具 一个基于.Net Core构建的简单、跨平台快速开发框架。JNPF开发平台前后端封装了上千个常用类&#xff0c;方便扩展&#xff1b;集成了代码生成器&#xff0c;支持前后端业务代码生成&#xff0c;满足快速开发&#xff0c;提升工作效率&#xff1b…

动态开辟内存空间函数

文章目录 malloc函数calloc函数malloc函数和calloc函数的不同free函数realloc函数 malloc函数 参数是要开辟内存空间的大小 开辟成功则返回值为开辟空间的首地址&#xff0c;若开辟失败则返回一个空指针NULL calloc函数 第一个参数为开辟空间的元素个数&#xff0c;第二个参数…

【软件教程】如何用C++交叉编译出能在Android运行的ELF程序或so动态库

一、配置NDK交叉编译平台 1. 打开Android的官方ndk下载链接https://developer.android.com/ndk/downloads?hlzh-cn&#xff0c;下载windows 64位ndk环境包。 2. 解压后将具有以下文件的路径加入到系统环境变量。 3. 配置好环境变量&#xff0c;如下图所示&#xff0c;Path中存…

多线程进阶

多线程常见面试题 文章目录 多线程常见面试题1. 常见的锁策略1.1乐观锁&悲观锁1.2 轻量级锁&重量级锁1.3 自旋锁&挂起等待锁1.4 读写锁&普通互斥锁1.5 公平锁&非公平锁1.6可重入锁&不可重入锁 2. CAS3. Sychronized原理3.1 锁升级3.2 锁消除3.3 锁粗化…

支持多校 微信课表小程序源码 排课小程序源码 支持导入课表 情侣课表 背景设置

练手Lab课程表小程序源码是一个基于thinkphp系统进行开发的前后端分离系统。 源码功能介绍 1、情侣功能 2、情侣间留言 3、情侣间互相设置课程表背景 4、自己日、周课程表背景设置 5、教务系统课程表导入 6、导入别人分享的课表 7、导入别人分享的单课 8、多校支持 9…

AIR101 LuatOS LVGL 显示多个标签例程

屏幕资料 AIR101与屏幕连接 PC端仿真环境合宙官方PC端版本环境搭建教程 PC电脑仿真 -- sys库是标配 _G.sys require("sys") sys.taskInit(function()local cnt0lvgl.init(480,320)--lvgl初始化local cont lvgl.cont_create(nil, nil);-- lvgl.cont_set_fit(cont, …

Centos8 降低gcc版本至gcc-7.3

1 首先卸载系统中的gcc sudo yum remove gcc 2 重新安装gcc-7.3 sudo dnf group install “Development Tools” 然后再次卸载gcc sudo yum remove gcc 然后发现centos-release-scl-rh已经安装了 sudo yum install centos-release-scl-rh yum -y install devtoolset-7-gcc dev…

不希望你的数据在云中?关闭iPhone或Mac上的iCloud

​如果你不想使用iCloud&#xff0c;可以很容易地从设备设置中选择退出并关闭它。当你禁用iCloud时&#xff0c;它会删除该设备对iCloud的访问&#xff0c;但不会删除苹果服务器上的任何数据。我们将在本文末尾向你展示如何做到这一点。 注销iCloud并完全禁用它 如果你根本不…

HDR图像处理软件 Photomatix Pro mac中文版新增功能

Photomatix Pro mac是一款专业的HDR合成软件&#xff0c;可以将不同曝光的多张照片合成为一张照片&#xff0c;而保留更多的细节。并且合成时可以帮助去除照片中的鬼影。Photomatix Pro提供两种类型的过程来增加动态范围&#xff0c;一个过程称为HDR色调映射&#xff0c;另一个…

YOLOv5算法改进(20)— 如何去写YOLOv5相关的论文(包括论文阅读+规律总结+写作方法)

前言:Hello大家好,我是小哥谈。最近一直在阅读关于YOLOv5的相关论文,读着读着我发现一条可以发论文的规律,特此简单总结一下,希望能够对同学们有所启迪!🌈 前期回顾: YOLOv5算法改进(1)— 如何去改进YOLOv5算法

kafka入门03——简单实战

目录 安装Java 安装Zookeeper 安装Kafka 生产与消费 主要是记录下Kafka的安装配置过程&#xff0c;前置条件需要安装jdk和zookeeper。 安装Java 1.Oracle官网下载对应jdk安装包 官网地址&#xff1a;Java Downloads | Oracle 好人分享了下载需要的oracle账号&#xff0c…

WPS中图的自动编号及引用

WPS中图的自动编号及引用 图的自动编号图编号的引用图编号及引用的更新 图的自动编号 将光标放置在需要插入编号的位置点击“引用”→“题注”&#xff1a; 点击“引用”→“题注”&#xff1a; 点击“编号”&#xff0c;设置图的编号格式&#xff0c;可勾选“包含章节编号”&…

Plex Media Server for Mac: 打造您的专属媒体库

在数字媒体时代&#xff0c;我们越来越依赖各种媒体应用来丰富我们的生活。其中&#xff0c;Plex Media Server for Mac以其高效、稳定和多功能性&#xff0c;逐渐成为了Mac用户们的首选。今天&#xff0c;我们就来深入探讨这款个人媒体软件的优势和应用场景。 Plex Media Serv…

防止消息丢失与消息重复——Kafka可靠性分析及优化实践

系列文章目录 上手第一关&#xff0c;手把手教你安装kafka与可视化工具kafka-eagle Kafka是什么&#xff0c;以及如何使用SpringBoot对接Kafka 架构必备能力——kafka的选型对比及应用场景 Kafka存取原理与实现分析&#xff0c;打破面试难关 防止消息丢失与消息重复——Kafka可…