Vue原理解析

news2024/12/25 13:46:34

文章目录

  • 1. VUE的响应式原理
    • 1.1 ViewModel
    • 1.2 双向绑定的基本原理
    • 1.3 什么是响应性
    • 1.4 Vue 中的响应性是如何工作的
  • 2. Vue 渲染机制
    • 2.1 虚拟 DOM
    • 2.2 渲染管线
    • 2.3 带编译时信息的虚拟 DOM
      • 2.3.1 静态提升
      • 2.3.2 修补标记 Flags
      • 2.3.3 树结构打平
      • 2.3.4 对 SSR 激活的影响

1. VUE的响应式原理

响应式的基本原理:双向数据绑定,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新,在单向绑定的基础上,如果用户更新了ViewModel的数据也会自动更新。

双向绑定由三个重要部分构成:

数据层(Model):应用数据及业务逻辑
视图层(View):应用的展示效果,各类UI组件
业务逻辑层(ViewModel):框架封装的核心,负责将数据与视图关联起来

1.1 ViewModel

作用:

  • 数据变化更新视图
  • 视图变化更新数据

它还有两个主要部分组成:

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

1.2 双向绑定的基本原理

JavaScript 中有两种劫持属性访问的方式:Object.definePropertyProxy

  • Vue 2 使用 Object.defineProperty 完全由于需支持更旧版本浏览器的限制。
  • Vue 3 中使用了 Proxy 来创建响应式对象,将 getter/setter 用于 ref

在这里插入图片描述
首先要对数据(data)进行劫持监听。所以需要设置一个监听器Observer,用来监听所有的属性。
每一个组件都有一个Watcher实例。如果属性发生变化,需要通知订阅者Watcher,看是否需要更新。因为订阅者有多个,所以需要一个消息订阅器(发布者)Dep(订阅者集合的管理数组)来专门收集这些订阅者,在ObserverWatcher之间进行统一管理。
还需要一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化为一个订阅者Watcher,并替换模板数据或绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。

1、实现一个监听器Observer,用来劫持并监听所有属性,如果发生变化,就通知订阅者。
2、实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
3、实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并据此初始化视图和订阅器Watcher。

1.3 什么是响应性

如果我们在 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() 函数会产生一个副作用,或者就简称为作用,因为它会更改程序里的状态。
  • A0A1 被视为这个作用的依赖,因为它们的值被用来执行这个作用。因此这次作用也可以说是一个它依赖的订阅者

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

whenDepsChange(update)

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

  • 当一个变量被读取时进行追踪。例如我们执行了表达式 A0 + A1 的计算,则 A0A1 都被读取到了。
  • 如果一个变量在当前运行的副作用中被读取了,就将该副作用设为此变量的一个订阅者。例如由于 A0A1update() 执行时被访问到了,则 update() 需要在第一次调用之后成为 A0A1 的订阅者。
  • 探测一个变量的变化。例如当我们给 A0 赋了一个新的值后,应该通知其所有订阅了的副作用重新执行。

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

我们是可以追踪一个对象的属性进行读和写的。

JavaScript 中有两种劫持属性访问的方式:getter/settersProxies。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
}
  • 当你将一个响应性对象的属性解构为一个局部变量时,响应性就会“断开连接”,因为对局部变量的访问不再触发 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

在内部,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。这会在渲染机制一章中详细介绍。

ref()computed()watchEffect() 这些 API 都是组合式 API 的一部分,如果你至今只使用过选项式 API,那么你需要知道的是组合式 API 更贴近 Vue 底层的响应式系统。事实上,Vue 3 中的选项式 API 正是基于组合式 API 建立的。对该组件实例 (this) 所有的属性访问都会触发 getter/setter 的响应式追踪,而像 watchcomputed 这样的选项也是在内部调用相应等价的组合式 API。

2. Vue 渲染机制

2.1 虚拟 DOM

const vnode = {
  type: 'div',
  props: {
    id: 'hello'
  },
  children: [
    /* 更多 vnode */
  ]
}

这里所说的 vnode 即一个纯 JavaScript 的对象 (一个“虚拟节点”),它代表着一个 <div> 元素。它包含我们创建实际元素所需的所有信息。它还包含更多的子节点,这使它成为虚拟 DOM 树的根节点。

一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载 (mount)

如果我们有两份虚拟 DOM 树,渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为修补 (patch),又被称为“比较差异 (diffing)”或“协调 (reconciliation)”。

虚拟 DOM 带来的主要收益是它赋予了开发者编程式地、声明式地创建、审查和组合所需 UI 结构的能力,而把直接与 DOM 相关的操作交给了渲染器。

2.2 渲染管线

  1. 编译:Vue 模板被编译为了渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
  2. 挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
  3. 修补:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。

在这里插入图片描述

2.3 带编译时信息的虚拟 DOM

虚拟 DOM 在 React 和大多数其他实现中都是纯运行时的:协调算法无法预知新的虚拟 DOM 树会是怎样,因此它总是需要遍历整棵树、比较每个 vnode 上 props 的区别来确保正确性。另外,即使一棵树的某个部分从未改变,还是会在每次重渲染时创建新的 vnode,带来了完全不必要的内存压力。这也是虚拟 DOM 最受诟病的地方之一:这种有点暴力的协调过程通过牺牲效率来换取可声明性和正确性。

但实际上我们并不需要这样。在 Vue 中,框架同时控制着编译器和运行时。这使得我们可以为紧密耦合的模板渲染器应用许多编译时优化。编译器可以静态分析模板并在生成的代码中留下标记,使得运行时尽可能地走捷径。与此同时,我们仍旧保留了边界情况时用户想要使用底层渲染函数的能力。我们称这种混合解决方案为带编译时信息的虚拟 DOM

下面,我们将讨论一些 Vue 编译器用来提高虚拟 DOM 运行时性能的主要优化:

2.3.1 静态提升

在模板中常常有部分内容是不带任何动态绑定的:

<div>
  <div>foo</div> <!-- 需提升 -->
  <div>bar</div> <!-- 需提升 -->
  <div>{{ dynamic }}</div>
</div>

foobar 这两个 div 是完全静态的,没有必要在重新渲染时再次创建和比对它们。Vue 编译器自动地会提升这部分 vnode 创建函数到这个模板的渲染函数之外,并在每次渲染时都使用这份相同的 vnode,渲染器知道新旧 vnode 在这部分是完全相同的,所以会完全跳过对它们的差异比对。

此外,当有足够多连续的静态元素时,它们还会再被压缩为一个“静态 vnode”,其中包含的是这些节点相应的纯 HTML 字符串。这些静态节点会直接通过 innerHTML 来挂载。同时还会在初次挂载后缓存相应的 DOM 节点。如果这部分内容在应用中其他地方被重用,那么将会使用原生的 cloneNode() 方法来克隆新的 DOM 节点,这会非常高效。

2.3.2 修补标记 Flags

对于单个有动态绑定的元素来说,我们可以在编译时推断出大量信息:

<!-- 仅含 class 绑定 -->
<div :class="{ active }"></div>

<!-- 仅含 id 和 value 绑定 -->
<input :id="id" :value="value">

<!-- 仅含文本子节点 -->
<div>{{ dynamic }}</div>

在为这些元素生成渲染函数时,Vue 在 vnode 创建调用中直接编码了每个元素所需的更新类型:

createElementVNode("div", {
  class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */)

最后这个参数 2 就是一个修补标记 (patch flag)。一个元素可以有多个修补标记,会被合并成一个数字。运行时渲染器也将会使用位运算来检查这些标记,确定相应的更新操作:

if (vnode.patchFlag & PatchFlags.CLASS /* 2 */) {
  // 更新节点的 CSS class
}

位运算检查是非常快的。通过这样的修补标记,Vue 能够在更新带有动态绑定的元素时做最少的操作。

Vue 也为 vnode 的子节点标记了类型。举个例子,包含多个根节点的模板被表示为一个片段 (fragment),大多数情况下,我们可以确定其顺序是永远不变的,所以这部分信息就可以提供给运行时作为一个修补标记。

export function render() {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    /* children */
  ], 64 /* STABLE_FRAGMENT */))
}

2.3.3 树结构打平

再来看看上面这个例子中生成的代码,你会发现所返回的虚拟 DOM 树是经一个特殊的 createElementBlock() 调用创建的:

export function render() {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    /* children */
  ], 64 /* STABLE_FRAGMENT */))
}

这里我们引入一个概念“区块”,内部结构是稳定的一个部分可被称之为一个区块。在这个用例中,整个模板只有一个区块,因为这里没有用到任何结构性指令 (比如 v-if 或者 v-for)。

每一个块都会追踪其所有带修补标记的后代节点 (不只是直接子节点),举个例子:

<div> <!-- root block -->
  <div>...</div>         <!-- 不会追踪 -->
  <div :id="id"></div>   <!-- 要追踪 -->
  <div>                  <!-- 不会追踪 -->
    <div>{{ bar }}</div> <!-- 要追踪 -->
  </div>
</div>

编译的结果会被打平为一个数组,仅包含所有动态的后代节点:

div (block root)
- div 带有 :id 绑定
- div 带有 {{ bar }} 绑定

当这个组件需要重渲染时,只需要遍历这个打平的树而非整棵树。这也就是我们所说的树结构打平,这大大减少了我们在虚拟 DOM 协调时需要遍历的节点数量。模板中任何的静态部分都会被高效地略过。

v-ifv-for 指令会创建新的区块节点:

<div> <!-- 根区块 -->
  <div>
    <div v-if> <!-- if 区块 -->
      ...
    <div>
  </div>
</div>

一个子区块会在父区块的动态子节点数组中被追踪,这为他们的父区块保留了一个稳定的结构。

2.3.4 对 SSR 激活的影响

修补标记和树结构打平都大大提升了 Vue SSR 激活的性能表现:

  • 单个元素的激活可以基于相应 vnode 的修补标记走更快的捷径。
  • 在激活时只有区块节点和其动态子节点需要被遍历,这在模板层面上实现更高效的部分激活。

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

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

相关文章

Zookeeper安装和基本使用

目录标题一、下载二、安装三、启动客户端测试四、使用zk一、下载 注意&#xff1a;自zk3.5.5版本以后&#xff0c;已编译的jar包&#xff0c;尾部有bin&#xff0c;应该使用的是apache-zookeeper-3.8.0-bin.tar.gz。&#xff0c;因此在下载高版本时&#xff0c;因该下载后缀带b…

华为手表开发:WATCH 3 Pro(5)点击按钮弹窗

华为手表开发&#xff1a;WATCH 3 Pro&#xff08;5&#xff09;点击按钮弹窗初环境与设备创建项目认识目录结构修改首页 -> 新建按钮 “ 按钮 ”文件名&#xff1a;**index.hml**引用包&#xff1a;system.prompt点击结果初 鸿蒙可穿戴开发 希望能写一些简单的教程和案例…

三大指标继续狂飙!重庆啤酒:不惧强弱分化加剧,深耕高端市场

十多年前&#xff0c;重庆啤酒因为9个跌停而被一片唱衰&#xff0c;资本市场经典的“关灯吃面”典故自此出现&#xff0c;被股民沿用至今。不过自2020年&#xff0c;重庆啤酒开始逆转走势&#xff0c;股价连续上涨。2021年重庆啤酒营收突破百亿大关&#xff0c;净赚11.66亿元&a…

关于小程序,你想知道的这些

近年来&#xff0c;各大平台纷纷上架小程序&#xff0c;迎来了小程序的爆发式增长。今天就来跟大家简单分享一下小程序基本的运行机制和安全机制。 小程序的由来 在小程序没有出来之前&#xff0c;最初微信WebView逐渐成为移动web重要入口&#xff0c;微信发布了一整套网页开…

自监督表征学习方法——BYOL(Bootstrap Your Own Latent)

自监督表征学习方法——BYOL(Bootstrap Your Own Latent) 参考文献&#xff1a;《Bootstrap Your Own Latent A New Approach to Self-Supervised Learning》 1.前言背景 学习良好的图像表示是计算机视觉中的一个关键挑战&#xff0c;因为它允许对下游任务进行有效的训练。许…

POE接口电路设计

首先看到PSE端&#xff0c;主要包含PSE芯片&#xff0c;MOS管&#xff08;有些PSE芯片MOS管是集成在芯片里面的&#xff09;&#xff0c;自恢复保险丝&#xff0c;还有TVS管&#xff0c;一般这个TVS管保护电压是58V的。这个电阻是用来检测POE传输的电流的&#xff0c;一般是0.几…

vue2 diff算法及虚拟DOM

概括&#xff1a;diff算法&#xff0c;虚拟DOM中采用的算法&#xff0c;把树形结构按照层级分解&#xff0c;只比较同级元素&#xff0c;不同层级的节点只有创建和删除操作。 一、虚拟DOM (1) 什么是虚拟DOM&#xff1f; 虚拟 DOM (Virtual DOM&#xff0c;简称 VDOM) 是一种…

KDDAC-10kV 电缆交流耐压及振荡波局放测试系统

一、概述 目前对电力电缆检修的管理&#xff0c;主要是依据《电力设备交接和预防性试验规程》所规定的项目和试验周期&#xff0c;定期在停电状态下进行绝缘性能试验。其中变频串联谐振试验由于试验状况接近电缆的运行工况&#xff0c;因此成为国内目前应用广泛的试验方法。 …

社科院与杜兰大学中外合作办学金融管理硕士——30+的年龄在职读研有必要吗?

说起读研&#xff0c;年龄在什么区间最合适呢&#xff1f;上次有位咨询的同学反馈年龄已经快35岁了&#xff0c;有一份不错的工作&#xff0c;但又不甘心止步于此&#xff0c;想要通过提升学历升职加薪&#xff0c;但又纠结自己是否能静下心来学习、是否能顺利毕业、拿到的证书…

[HSCSEC 2023] rev,pwn,crypto,Ancient-MISC部分

比赛后有讲解&#xff0c;没赶上&#xff0c;前20比赛完1小时提交WP&#xff0c;谁会大半夜的起来写WP。总的感觉pwn,crypto过于简单&#xff0c;rev有2个难的不会&#xff0c;其它不是我的方向都感觉过于难&#xff0c;一个都没作。revDECOMPILEONEOONE入门题&#xff0c;一个…

12 循环神经网络(基础篇) Basic RNN

文章目录问题引入关于权重权重共享RNN CellRNN原理RNN计算过程代码实现RNN Cell维度说明代码RNN维度说明NumLayers说明计算过程代码参考实例问题分析多分类问题代码RNN CellRNN改进Embedding网络结构Embedding说明Linear说明代码课程来源&#xff1a; 链接课程文本参考&#xf…

前端react面试题指南

概述下 React 中的事件处理逻辑 抹平浏览器差异&#xff0c;实现更好的跨平台。避免垃圾回收&#xff0c;React 引入事件池&#xff0c;在事件池中获取或释放事件对象&#xff0c;避免频繁地去创建和销毁。方便事件统一管理和事务机制。 为了解决跨浏览器兼容性问题&#xff0…

CSS基础:盒子模型和浮动

盒子模型 所有HTML元素可以看作盒子&#xff0c;在CSS中&#xff0c;"box model"这一术语是用来设计和布局时使用 CSS盒模型本质上是一个盒子&#xff0c;封装HTML元素。 它包括&#xff1a;外边距&#xff08;margin&#xff09;&#xff0c;边框&#xff08;bord…

操作系统考试突击复习笔记

0 基础概念补充特权命令&#xff1a;有特殊权限的指令&#xff0c;比如清内存、置时钟、分配系统资源、修改虚拟内存的段表和页表&#xff0c;修改用户的访问权限。系统调用&#xff1a;操作系统为应用程序提供的使用接口&#xff0c;可以理解为一种可供应用程序调用的特殊函数…

Elasticsearch7.8.0版本进阶——数据写流程

目录一、数据写流程概述二、数据写流程步骤2.1、数据写流程图2.2、数据写流程步骤&#xff08;新建索引和删除文档所需要的步骤顺序&#xff09;2.3、数据写流程的请求参数一、数据写流程概述 新建、删除索引和新建、删除文档的请求都是写操作&#xff0c; 必须在主分片上面完…

http409报错原因

今天一个同事的接口突然报409,大概百度了一下,不是很清楚,谷歌也没找到特别好的解释 因为是直接调用的gitlab,就直接看了下gitlab的api The following table shows the possible return codes for API requests. Return valuesDescription200 OKThe GET, PUT or DELETE request…

【halcon】灰度直方图直观理解与应用

灰度直方图 横坐标&#xff1a;是 0~255 表示灰度值的范围 纵坐标&#xff1a;是在不同灰度值下像素的个数&#xff01; 那么灰度直方图的本质就是统计不同灰度下像素的个数&#xff01; 它的直观目的&#xff0c;就是查看灰度的分布情况&#xff01; 与之相关的函数&#xff…

宝塔搭建实战php开源likeadmin通用管理pc端nuxt3源码(三)

大家好啊&#xff0c;我是测评君&#xff0c;欢迎来到web测评。 昨天给大家分享了admin前端的搭建部署方式&#xff0c;今天来给大家分享pc端在本地搭建&#xff0c;与打包发布到宝塔的方法&#xff0c;希望能够帮助到大家&#xff0c;感兴趣的朋友可以自行下载学习。 技术架构…

C语言在游戏中播放音乐

使用 mciSendString 播放音乐 mciSendString 支持 mp3、wma、wav、mid 等多种媒体格式&#xff0c;使用非常简单。这里做一个简单的范例&#xff0c;用 mciSendString 函数播放 MP3 格式的音乐&#xff0c;代码如下&#xff1a; // 编译该范例前&#xff0c;请把 music.mp3 放…

Kotlin实现简单音乐播放器

关于音乐播放器&#xff0c;我真的是接触比较多&#xff0c;听歌作为我第一大爱好&#xff0c;之前也用Java设计过音乐播放器&#xff0c;感兴趣的同学可以阅读&#xff1a;Android Studio如何实现音乐播放器&#xff08;简单易上手&#xff09;和 Android Studio实现音乐播放器…