vue3-深入响应式系统

Vue 最标志性的功能就是其低侵入性的响应式系统。组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时,视图会随即自动更新。这让状态管理更加简单直观,但理解它是如何工作的也是很重要的,这可以帮助我们避免一些常见的陷阱。在本节中,我们将深入研究 Vue 响应性系统的一些底层细节。

什么是响应性

这个术语在今天的各种编程讨论中经常出现,但人们说它的时候究竟是想表达什么意思呢?本质上,响应性是一种可以使我们声明式地处理变化的编程范式。一个经常被拿来当作典型例子的用例即是 Excel 表格:

这里单元格 A2 中的值是通过公式 = A0 + A1 来定义的 (你可以在 A2 上点击来查看或编辑该公式),因此最终得到的值为 3,正如所料。但如果你试着更改 A0 或 A1,你会注意到 A2 也随即自动更新了。

而 JavaScript 默认并不是这样的。如果我们用 JavaScript 写类似的逻辑:

let A0 = 1
let A1 = 2
let A2 = A0 + A1

console.log(A2) // 3

A0 = 2
console.log(A2) // 仍然是 3

当我们更改 A0 后,A2 不会自动更新。

那么我们如何在 JavaScript 中做到这一点呢?首先,为了能重新运行计算的代码来更新 A2,我们需要将其包装为一个函数:

let A2

function update() {
  A2 = A0 + A1
}

然后,我们需要定义几个术语:

这个 update() 函数会产生一个副作用,或者就简称为作用 (effect),因为它会更改程序里的状态。

  • A0 和 A1 被视为这个作用的依赖 (dependency),因为它们的值被用来执行这个作用。因此这次作用也可以说是一个它依赖的订阅者 (subscriber)。

  • 我们需要一个魔法函数,能够在 A0 或 A1 (这两个依赖) 变化时调用 update() (产生作用)。

whenDepsChange(update)

这个 whenDepsChange() 函数有如下的任务:

  • 当一个变量被读取时进行追踪。例如我们执行了表达式 A0 + A1 的计算,则 A0 和 A1 都被读取到了。

  • 如果一个变量在当前运行的副作用中被读取了,就将该副作用设为此变量的一个订阅者。例如由于 A0 和 A1 在 update() 执行时被访问到了,则 update() 需要在第一次调用之后成为 A0 和 A1 的订阅者。

  • 探测一个变量的变化。例如当我们给 A0 赋了一个新的值后,应该通知其所有订阅了的副作用重新执行。

Vue 中的响应性是如何工作的

我们无法直接追踪对上述示例中局部变量的读写,原生 JavaScript 没有提供任何机制能做到这一点。但是,我们是可以追踪对象属性的读写的。

在 JavaScript 中有两种劫持 property 访问的方式:getter / setters 和 Proxies。

Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制。

而在 Vue 3 中则使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref。下面的伪代码将会说明它们是如何工作的:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
    }
  })
}

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    }
  }
  return refObject
}

这里和下面的代码片段皆旨在以最简单的形式解释核心概念,因此省略了许多细节和边界情况。

以上代码解释了我们在基础章节部分讨论过的一些 reactive() 的局限性:

当你将一个响应式对象的属性赋值或解构到一个本地变量时,访问或赋值该变量是非响应式的,因为它将不再触发源对象上的 get / set 代理。注意这种“断开”只影响变量绑定——如果变量指向一个对象之类的非原始值,那么对该对象的修改仍然是响应式的。

从 reactive() 返回的代理尽管行为上表现得像原始对象,但我们通过使用 === 运算符还是能够比较出它们的不同。

在 track() 内部,我们会检查当前是否有正在运行的副作用。如果有,我们会查找到一个存储了所有追踪了该属性的订阅者的 Set,然后将当前这个副作用作为新订阅者添加到该 Set 中。

// 这会在一个副作用就要运行之前被设置
// 我们会在后面处理它
let activeEffect

function track(target, key) {
  if (activeEffect) {
    const effects = getSubscribersForProperty(target, key)
    effects.add(activeEffect)
  }
}

副作用订阅将被存储在一个全局的 WeakMap<target, Map<key, Set<effect>>> 数据结构中。如果在第一次追踪时没有找到对相应属性订阅的副作用集合,它将会在这里新建。这就是 getSubscribersForProperty() 函数所做的事。为了简化描述,我们跳过了它其中的细节。

在 trigger() 之中,我们会再查找到该属性的所有订阅副作用。但这一次我们需要执行它们:

function trigger(target, key) {
  const effects = getSubscribersForProperty(target, key)
  effects.forEach((effect) => effect())
}

现在让我们回到 whenDepsChange() 函数中:

function whenDepsChange(update) {
  const effect = () => {
    activeEffect = effect
    update()
    activeEffect = null
  }
  effect()
}

它将原本的 update 函数包装在了一个副作用函数中。在运行实际的更新之前,这个外部函数会将自己设为当前活跃的副作用。这使得在更新期间的 track() 调用都能定位到这个当前活跃的副作用。

此时,我们已经创建了一个能自动跟踪其依赖的副作用,它会在任意依赖被改动时重新运行。我们称其为响应式副作用。

Vue 提供了一个 API 来让你创建响应式副作用 watchEffect()。事实上,你会发现它的使用方式和我们上面示例中说的魔法函数 whenDepsChange() 非常相似。我们可以用真正的 Vue API 改写上面的例子:

import { ref, watchEffect } from 'vue'

const A0 = ref(0)
const A1 = ref(1)
const A2 = ref()

watchEffect(() => {
  // 追踪 A0 和 A1
  A2.value = A0.value + A1.value
})

// 将触发副作用
A0.value = 2

使用一个响应式副作用来更改一个 ref 并不是最优解,事实上使用计算属性会更直观简洁:

import { ref, computed } from 'vue'

const A0 = ref(0)
const A1 = ref(1)
const A2 = computed(() => A0.value + A1.value)

A0.value = 2

使用一个响应式副作用来更改一个 ref 并不是最优解,事实上使用计算属性会更直观简洁:

import { ref, computed } from 'vue'

const A0 = ref(0)
const A1 = ref(1)
const A2 = computed(() => A0.value + A1.value)

A0.value = 2

在内部,computed 会使用响应式副作用来管理失效与重新计算的过程。

那么,常见的响应式副作用的用例是什么呢?自然是更新 DOM!我们可以像下面这样实现一个简单的“响应式渲染”:

import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {
  document.body.innerHTML = `计数:${count.value}`
})

// 更新 DOM
count.value++

实际上,这与 Vue 组件保持状态和 DOM 同步的方式非常接近——每个组件实例创建一个响应式副作用来渲染和更新 DOM。当然,Vue 组件使用了比 innerHTML 更高效的方式来更新 DOM。这会在渲染机制一章中详细介绍。

运行时 vs. 编译时响应性

Vue 的响应式系统基本是基于运行时的。追踪和触发都是在浏览器中运行时进行的。运行时响应性的优点是,它可以在没有构建步骤的情况下工作,而且边界情况较少。另一方面,这使得它受到了 JavaScript 语法的制约,导致需要使用一些例如 Vue ref 这样的值的容器。

一些框架,如 Svelte,选择通过编译时实现响应性来克服这种限制。它对代码进行分析和转换,以模拟响应性。该编译步骤允许框架改变 JavaScript 本身的语义——例如,隐式地注入执行依赖性分析的代码,以及围绕对本地定义的变量的访问进行作用触发。这样做的缺点是,该转换需要一个构建步骤,而改变 JavaScript 的语义实质上是在创造一种新语言,看起来像 JavaScript 但编译出来的东西是另外一回事。

Vue 团队确实曾通过一个名为响应性语法糖的实验性功能来探索这个方向,但最后由于这个原因,我们认为它不适合这个项目。

响应性调试

Vue 的响应性系统可以自动跟踪依赖关系,但在某些情况下,我们可能希望确切地知道正在跟踪什么,或者是什么导致了组件重新渲染。

组件调试钩子

我们可以在一个组件渲染时使用 onRenderTracked 生命周期钩子来调试查看哪些依赖正在被使用,或是用 onRenderTriggered 来确定哪个依赖正在触发更新。这些钩子都会收到一个调试事件,其中包含了触发相关事件的依赖的信息。推荐在回调中放置一个 debugger 语句,使你可以在开发者工具中交互式地查看依赖:

<script setup>
import { onRenderTracked, onRenderTriggered } from 'vue'

onRenderTracked((event) => {
  debugger
})

onRenderTriggered((event) => {
  debugger
})
</script>

组件调试钩子仅会在开发模式下工作

调试事件对象有如下的类型定义:

type DebuggerEvent = {
  effect: ReactiveEffect
  target: object
  type:
    | TrackOpTypes /* 'get' | 'has' | 'iterate' */
    | TriggerOpTypes /* 'set' | 'add' | 'delete' | 'clear' */
  key: any
  newValue?: any
  oldValue?: any
  oldTarget?: Map<any, any> | Set<any>
}

计算属性调试

我们可以向 computed() 传入第二个参数,是一个包含了 onTrack 和 onTrigger 两个回调函数的对象:

  • onTrack 将在响应属性或引用作为依赖项被跟踪时被调用。

  • onTrigger 将在侦听器回调被依赖项的变更触发时被调用。 这两个回调都会作为组件调试的钩子,接受相同格式的调试事件:

const plusOne = computed(() => count.value + 1, {
  onTrack(e) {
    // 当 count.value 被追踪为依赖时触发
    debugger
  },
  onTrigger(e) {
    // 当 count.value 被更改时触发
    debugger
  }
})

// 访问 plusOne,会触发 onTrack
console.log(plusOne.value)

// 更改 count.value,应该会触发 onTrigger
count.value++

计算属性的 onTrack 和 onTrigger 选项仅会在开发模式下工作。

侦听器调试

和 computed() 类似,侦听器也支持 onTrack 和 onTrigger 选项:

watch(source, callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

watchEffect(callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

侦听器的 onTrack 和 onTrigger 选项仅会在开发模式下工作。

与外部状态系统集成

Vue 的响应性系统是通过深度转换普通 JavaScript 对象为响应式代理来实现的。这种深度转换在一些情况下是不必要的,在和一些外部状态管理系统集成时,甚至是需要避免的 (例如,当一个外部的解决方案也用了 Proxy 时)。

将 Vue 的响应性系统与外部状态管理方案集成的大致思路是:将外部状态放在一个 shallowRef 中。一个浅层的 ref 中只有它的 .value 属性本身被访问时才是有响应性的,而不关心它内部的值。当外部状态改变时,替换此 ref 的 .value 才会触发更新。

不可变数据

如果你正在实现一个撤销/重做的功能,你可能想要对用户编辑时应用的状态进行快照记录。然而,如果状态树很大的话,Vue 的可变响应性系统没法很好地处理这种情况,因为在每次更新时都序列化整个状态对象对 CPU 和内存开销来说都是非常昂贵的。

不可变数据结构通过永不更改状态对象来解决这个问题。与 Vue 不同的是,它会创建一个新对象,保留旧的对象未发生改变的一部分。在 JavaScript 中有多种不同的方式来使用不可变数据,但我们推荐使用 Immer 搭配 Vue,因为它使你可以在保持原有直观、可变的语法的同时,使用不可变数据。

我们可以通过一个简单的组合式函数来集成 Immer(https://immerjs.github.io/immer/):

import produce from 'immer'
import { shallowRef } from 'vue'

export function useImmer(baseState) {
  const state = shallowRef(baseState)
  const update = (updater) => {
    state.value = produce(state.value, updater)
  }

  return [state, update]
}
状态机

状态机是一种数据模型,用于描述应用可能处于的所有可能状态,以及从一种状态转换到另一种状态的所有可能方式。虽然对于简单的组件来说,这可能有些小题大做了,但它的确可以使得复杂的状态流更加健壮和易于管理。

XState(https://xstate.js.org/) 是 JavaScript 中一个比较常用的状态机实现方案。这里是集成它的一个例子:

import { createMachine, interpret } from 'xstate'
import { shallowRef } from 'vue'

export function useMachine(options) {
  const machine = createMachine(options)
  const state = shallowRef(machine.initialState)
  const service = interpret(machine)
    .onTransition((newState) => (state.value = newState))
    .start()
  const send = (event) => service.send(event)

  return [state, send]
}

RxJS

RxJS(https://rxjs.dev/) 是一个用于处理异步事件流的库。VueUse(https://vueuse.org/) 库提供了 @vueuse/rxjs 扩展来支持连接 RxJS 流与 Vue 的响应性系统。

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

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

相关文章

DDD爱好者通病-《软件方法》自测题解析37

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 《软件方法》第5章自测题2 5 [ 单选题 ] 我们经常会听到有人说“系统分为几个功能模块”。针对“功能模块”&#xff0c;以下说法正确的是&#xff1a;  A) 它把外部和内部混在一…

蓝桥杯:C++二分算法

在基本算法中&#xff0c;二分法的应用非常广泛&#xff0c;它是一种思路简单、编程容易、效率极高的算法。蓝桥杯软件类大赛中需要应用二分法的题目很常见。 二分法有整数二分和实数二分两种应用场景 二分法的概念 二分法的概念很简单&#xff0c;每次把搜索范围缩小为上一…

Linux POSIX信号量 线程池

Linux POSIX信号量 线程池 一. 什么是POSIX信号量&#xff1f;二. POSIX信号量实现原理三. POSIX信号量接口函数四. 基于环形队列的生产消费模型五. 线程池 一. 什么是POSIX信号量&#xff1f; POSIX信号量是一种用于同步和互斥操作的机制&#xff0c;属于POSIX&#xff08;Po…

WINCC如何新增下单菜单,切换显示页面

杭州工控赖工 首先我们先看一下&#xff0c;显示的效果&#xff0c;通过下拉菜单&#xff0c;切换主显示页面。如图一&#xff1a; 图1 显示效果 第一步&#xff1a; 通过元件新增一个组合框&#xff0c;见图2&#xff1b; 组合框的设置&#xff0c;设置下拉框的长宽及组合数…

[java基础揉碎]数组 值拷贝和引用拷贝的赋值方式

目录 数组的介绍 为什么有数组 数组的三种使用方式 动态初始化: 静态初始化: 数组使用注意事项和细节 值拷贝和引用拷贝的赋值方式 数组反转: 数组拷贝: 数组的介绍 数组可以存放多个同一类型的数据。数组也是一种数据类型&#xff0c;是引用类型。 即&#xff1a;数组…

解决ubuntu登录密码问题

解决ubuntu登录密码问题 不要随便删除密码&#xff0c;不要随便改密码&#xff0c;很容导致密码过期&#xff0c;或者密码无效。参考了很多人的做法&#xff0c;都没有得到解决。下面的过程&#xff0c;够详细了&#xff0c;我就是这么搞好的。 1、重启虚拟机&#xff0c;不停…

Ubuntu忘记登录密码重置步骤

Ubuntu忘记登录密码重置步骤 1.开机界面长按shitf键&#xff0c;进入grub&#xff0c;并选择Advanced options for ubuntu&#xff0c;按下回车 2.选择一个较新版本的recovery mode&#xff0c;按下回车 3.会跑一些数据&#xff0c;等待跑完后会出现下面的界面&#xff0c;选择…

SG7050VEN(晶体振荡器SPXO)输出:LVDS低相位抖动

SG7050VEN 提供了从25 MHz到500 MHz的宽广频率范围&#xff0c;2.5V和3.3V供电电压,可以轻松集成到各种电源中&#xff0c;7.0 5.0 1.5 mm 的封装&#xff0c;LVDS输出已成为高速数据传输的首选&#xff0c;它提供了低功耗和高速率的优势&#xff0c;同时还能最小化电磁干扰。…

基于BP算法的SAR成像matlab仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1 BP算法的基本原理 4.2 BP算法的优点与局限性 5.完整工程文件 1.课题概述 基于BP算法的SAR成像。合成孔径雷达&#xff08;SAR&#xff09;是一种高分辨率的雷达系统&#xff0c;能够在各种天气和光…

幻兽帕鲁服务器创建私服教程(新版教程更简单)

幻兽帕鲁官方服务器不稳定&#xff1f;自己搭建幻兽帕鲁服务器&#xff0c;低延迟、稳定不卡&#xff0c;目前阿里云和腾讯云均推出幻兽帕鲁专用服务器&#xff0c;腾讯云直接提供幻兽帕鲁镜像系统&#xff0c;阿里云通过计算巢服务&#xff0c;均可以一键部署&#xff0c;鼠标…

Linux第58步_备份busybox生成rootfs根文件系统

备份busybox生成rootfs根文件系统 打开终端 输入“ls回车” 输入“cd linux/回车” 输入“ls回车”&#xff0c;产看“linux”目录下的文件和文件夹 输入“cd nfs/回车”&#xff0c;切换到“nfs”目录 输入“ls回车”&#xff0c;产看“nfs”目录下的文件和文件夹 输入…

大模型专题:大模型赋能智慧办公评测报告

今天分享的是大模型系列深度研究报告&#xff1a;《大模型专题&#xff1a;大模型赋能智慧办公评测报告》。 &#xff08;报告出品方&#xff1a;国家工业信息安全发展研究中心人工智能所&#xff09; 报告共计&#xff1a;34页 来源&#xff1a;人工智能学派 评测背景 当…

Deep learning学习笔记

lec 1&#xff1a;Regression 1.5 Linear neural networks for regression线性神经网络的回归 I parameterizing output layer, I handling data, I specifying loss function, I training model. 浅层网络包括线性模型&#xff0c;其中包含了许多经典的统计预测方法&…

javaweb学习day03(JS+DOM)

一、javascript入门 1 官方文档 地址: https://www.w3school.com.cn/js/index.asp离线文档: W3School 离线手册(2017.03.11 版).chm 2 基本说明 JavaScript 能改变 HTML 内容&#xff0c;能改变 HTML 属性&#xff0c;能改变 HTML 样式 (CSS)&#xff0c;能完成 页面的数据…

阿里云服务器租用价格2024年新版活动报价和租用收费标准

2024年最新阿里云服务器租用费用优惠价格表&#xff0c;轻量2核2G3M带宽轻量服务器一年61元&#xff0c;折合5元1个月&#xff0c;新老用户同享99元一年服务器&#xff0c;2核4G5M服务器ECS优惠价199元一年&#xff0c;2核4G4M轻量服务器165元一年&#xff0c;2核4G服务器30元3…

macOS 安装 conda

macOS 安装 conda 安装 conda参考 Conda是一个开源的软件包管理系统和环境管理系统&#xff0c;用于安装和管理软件包和其依赖项。 安装 conda mkdir miniconda3 cd miniconda3 bash Miniconda3-latest-MacOSX-x86_64.sh$ conda list参考 macOS 安装 conda开始使用conda

XSS数据接收平台

一.使用xss数据接收平台的好处&#xff1a; 正常执行反射型xss和存储型xss&#xff0c;反射型xss在执行poc时&#xff0c;会直接在页面弹出执行注入的poc代码&#xff1b;存储型则是&#xff0c;在将poc代码注入用户的系统中后&#xff0c;用户访问有存储型xss的地方&#xff…

前端工程化面试题 | 12.精选前端工程化高频面试题

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

51单片机项目(31)——基于51单片机篮球计分器的proteus仿真

1.功能设计 可以通过两组按键&#xff0c;控制两个队伍的加减分&#xff0c;加分设置有&#xff0b;1分按键&#xff0c;&#xff0b;2分按键&#xff0c;&#xff0b;3分按键。减分设置有&#xff0d;1分按键。 设置有开始/暂停按键&#xff0c;按下开始&#xff0c;数码管便开…

LeetCode Python - 20.有效的括号

目录 题目答案运行结果 题目 给定一个只包括 ‘(’&#xff0c;‘)’&#xff0c;‘{’&#xff0c;‘}’&#xff0c;‘[’&#xff0c;‘]’ 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正…