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

news2025/3/1 1:13:43

前言

本文是 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 Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  const proxy = new Proxy(target, baseHandlers)
  // WeakMap
  proxyMap.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?: boolean
  scheduler?: (...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 = effect
        return fn() // 执行用户定义的方法并返回值
      } finally {
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  effect.id = uid++ // 区别effect
  effect.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
  // 获取effect
  let 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 tracked
    return
  }
  let deps = []
  // schedule runs for SET | ADD | DELETE
  if (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.coloradmin.cn/o/1127791.html

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

相关文章

安科瑞带防逆流功能的数据通讯网关-安科瑞黄安南

AWT200 数据通讯网关应用于各种终端设备的数据采集与数据分析。用于实现设备的监测、控制、计算&#xff0c;为系统与设备之间建立通讯纽带&#xff0c;实现双向的数据通讯。实时监测并及时发现异常&#xff0c;同时自身根据用户规则进行逻辑判断&#xff0c;可以节省人力和通讯…

2023年面试测试工程师一般问什么问题?

面试和项目一起&#xff0c;是自学路上的两大拦路虎。面试测试工程师一般会被问什么问题&#xff0c;总结下来一般是下面这4类&#xff1a; 1.做好自我介绍 2.项目相关问题 3.技术相关问题 4.人事相关问题 接下来&#xff0c;主要从以上四个方向分别展开介绍。为了让大家更有获…

[ThinkPHP]The namespace “work“ is ambiguous (worker, workflow)

问题截图&#xff1a; 解决办法&#xff1a; console.php增加相关配置

PAM从入门到精通(二十三)

接前一篇文章&#xff1a;PAM从入门到精通&#xff08;二十二&#xff09; 本文参考&#xff1a; 《The Linux-PAM Application Developers Guide》 先再来重温一下PAM系统架构&#xff1a; 更加形象的形式&#xff1a; 七、PAM-API各函数源码详解 前边的文章讲解了各PAM-API…

方案聚焦:高可用的F5分布式云DNS负载均衡

DNS是实现互联网的主要技术之一。它也是网络基础设施的重要组成部分&#xff0c;DNS管理一个分布式和冗余的架构&#xff0c;确保高可用性和高质量的用户响应时间&#xff0c;因此拥有一个可用的、智能的、安全和可扩展的DNS基础设施是至关重要的。然而DNS没有真正的能力来分配…

如何有效取代FTP来帮助企业快速传输大文件

在互联网的发展历史上&#xff0c;FTP是一种具有里程碑意义的协议&#xff0c;它最早出现在1971年&#xff0c;是实现网络上文件传输的基础。FTP的优点是简单、稳定、兼容性强&#xff0c;可以在不同的操作系统和平台之间进行文件交换。然而&#xff0c;时代在进步&#xff0c;…

系统升级数量超微软预期,Win10/11盗版激活被封杀

声明&#xff1a;本文提供的命令、工具来自第三方网站&#xff0c;仅供学习交流使用&#xff0c;下载后24小时内删除&#xff0c;一切非法使用责任由使用者自行承担。 上月底 Win11 迎来了 Moment 4 功能更新&#xff0c;任务栏取消合并居然真的回归了。 巨硬终于妥协&#x…

软件开发行业的乱象——低价引流中途收费?299?399?

在当今的软件开发行业&#xff0c;存在着许多乱象。这些乱象不仅影响了软件开发的效率和效果&#xff0c;也给整个行业带来了许多负面影响。其中&#xff0c;价格方面是软件开发行业乱象的一个重要方面。 首先&#xff0c;从价格方面来看&#xff0c;软件开发行业的价格标准非常…

【均值漂移】mean-shift算法详解

Mean-shift算法是一种非参数密度估计算法&#xff0c;主要用于图像分割、目标跟踪和聚类等领域。其基本原理是以某个点为中心&#xff0c;计算该点周围所有点的密度&#xff0c;并将中心点移动到密度最大的位置&#xff0c;不断迭代&#xff0c;直到中心点不再移动或满足停止条…

常用智能优化算法改进策略---飞行游走篇(五种策略)可用于改进所有智能算法,让小白也会改进智能算法。...

本期文章将讲述常用智能优化算法改进策略---飞行游走篇&#xff0c;一共包含五种常见的改进策略&#xff1a; ①莱维飞行&#xff0c;②随机游走&#xff0c;③螺旋飞行&#xff0c;④高斯随机游走&#xff0c;⑤三角形游走 五种策略可以方便移植到其他任何智能算法的改进中&am…

开发常用的 Linux 命令知识积累

查看硬盘的使用情况df -h单元为根据大小适当显示&#xff0c;-m单位为M Linux中查找文件夹的命令是find命令。 全盘搜索&#xff0c;也可以指定目录搜索。find 搜索目录 -name 目标名字&#xff0c;find / -name file liunx自己总结常见命令 执行命令 含义 cd ~ 切换到登录…

了解docker

了解docker docker版本演进docker架构docker生态docker安装 docker版本演进 lxc&#xff1a; lxc 是最早的 linux 容器技术&#xff0c;早期版本的 docker 直接使用 lxc 来实现容器的底层功能。虽然使用者相对较少&#xff0c;但 lxc 项目仍在持续开发演进中libcontainer&#…

Flutter饱受争议的7个缺点,大家怎么看?

Flutter是一款由Google推出的跨平台移动应用开发框架&#xff0c;近年来备受关注。尽管Flutter在某些方面表现出色&#xff0c;但仍然有一些人对它的发展前景表示怀疑。近期一些文章针对Flutter的发展提出了不少质疑和批评&#xff0c;称其难以成为移动应用开发的“顶流明星”&…

Hacker 资讯 | 10 月下旬区块链黑客松活动汇总

「TinTin Hacker 快讯」是 TinTinLand 建立的一个资讯专栏&#xff0c;汇集近期线上线下的黑客松及 Grant&#xff0c;旨在帮助开发者和区块链爱好者获取最新的黑客松资讯&#xff0c;鼓励他们了解并根据自身情况参与不同的黑客松&#xff0c;更好地建设 Web3 生态。 ETHMiami …

1.初识MySQL

初识 MySQL 1.服务器处理客户端请求2.常用存储引擎3.关于存储引擎的一些操作3.1 查看当前服务器程序支持的存储引擎3.2 设置表的存储引擎3.2.1 创建表时指定存储引擎3.2.2 修改表的存储引擎 4.总结 MySQL 默认采用 TCP/IP 的方式来处理客户端与服务器连接过程。 1.服务器处理客…

2023高频前端面试题-vue

1. 什么是 M V VM Model-View-ViewModel 模式 Model 层: 数据模型层 通过 Ajax、fetch 等 API 完成客户端和服务端业务模型的同步。 View 层: 视图层 作为视图模板存在&#xff0c;其实 View 就是⼀个动态模板。 ViewModel 层: 视图模型层 负责暴露数据给 View 层&…

解决oracle12c安装失败【INS-30131】执行安装程序验证所需要的初始设置失败问题

最近看到很多公司都要求会使用Oracle数据库,所以我就在网上找了Oracle教程,打算学习一下,可人生就是喜欢捉弄我,Oracle安装竟然都出了问题,别提学习了,可真让我很难受,那么安装出现了什么问题呢? 看下图: 原因 - 无法访问临时位置。 操作 - 请确保当前用户具有访问临时位置所…

muduo源码学习base——Atomic(原子操作与原子整数)

Atomic(原子操作与原子整数&#xff09; 前置知识AtomicIntegerTget()getAndAdd()getAndSet() 关于原子操作实现无锁队列(lock-free-queue) 前置知识 happens-before&#xff1a; 用来描述两个操作的内存可见性 如果操作 X happens-before 操作 Y&#xff0c;那么 X 的结果对于…

极智AI | 有趣的羊驼系列大模型

欢迎关注我的公众号 [极智视界],获取我的更多经验分享 大家好,我是极智视界,本文来介绍一下 有趣的羊驼系列大模型。 邀您加入我的知识星球「极智视界」,星球内有超多好玩的项目实战源码下载,链接:https://t.zsxq.com/0aiNxERDq "羊驼模型" 在大模型的介绍中应…

CodeWhisperer proxy代理连不上(解决)

报错&#xff1a; 2023-10-24 14:15:50 [INFO]: selected AWS ID sign in 2023-10-24 14:15:50 [ERROR]: API response (oidc.us-east-1.amazonaws.com /device_authorization): {} 2023-10-24 14:15:50 [ERROR]: webviewId"authWebview": Error: Webview error->…