Vue原理解析

news/2024/11/1 14:20:52/

文章目录

  • 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 + A1console.log(A2) // 3A0 = 2
console.log(A2) // 仍然是 3

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

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

let A2function 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] = valuetrigger(target, key)}})
}function ref(value) {const refObject = {get value() {track(refObject, 'value')return value},set value(newValue) {value = newValuetrigger(refObject, 'value')}}return refObject
}
  • 当你将一个响应性对象的属性解构为一个局部变量时,响应性就会“断开连接”,因为对局部变量的访问不再触发 get / set 代理捕获。
  • reactive() 返回的代理尽管行为上表现得像原始对象,但我们通过使用 === 运算符还是能够比较出它们的不同。

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

// 这会在一个副作用就要运行之前被设置
// 我们会在后面处理它
let activeEffectfunction 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 = effectupdate()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 和 A1A2.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.ppmy.cn/news/24630.html

相关文章

生产Nginx现大量TIME-WAIT,连接耗尽,该如何处理?

背景说明&#xff1a; 在尼恩读者50交流群中&#xff0c;是不是有小伙伴问&#xff1a; 尼恩&#xff0c;生产环境 Nginx 后端服务大量 TIME-WAIT &#xff0c; 该怎么办&#xff1f; 除了Nginx进程之外&#xff0c;还有其他的后端服务如&#xff1a; 尼恩&#xff0c;生产环境…

【图像分类】基于PyTorch搭建LSTM实现MNIST手写数字体识别(双向LSTM,附完整代码和数据集)

写在前面: 首先感谢兄弟们的关注和订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。 在https://blog.csdn.net/AugustMe/article/details/128969138文章中,我们使用了基于PyTorch搭建LSTM实现MNIST手…

做 SQL 性能优化真是让人干瞪眼

很多大数据计算都是用SQL实现的&#xff0c;跑得慢时就要去优化SQL&#xff0c;但常常碰到让人干瞪眼的情况。 比如&#xff0c;存储过程中有三条大概形如这样的语句执行得很慢&#xff1a; select a,b,sum(x) from T group by a,b where …; select c,d,max(y) from T grou…

AcWing《蓝桥杯集训·每日一题》—— 3956.截断数组

AcWing《蓝桥杯集训每日一题》—— 3956. 截断数组 文章目录AcWing《蓝桥杯集训每日一题》—— 3956. 截断数组一、题目二、解题思路三、代码实现本次博客我是通过Notion软件写的&#xff0c;转md文件可能不太美观&#xff0c;大家可以去我的博客中查看&#xff1a;北天的 BLOG…

Mysql 存储过程

什么是存储过程&#xff1f; 存储过程是事先经过编译并存储在数据库的一段sql语句的集合 如何创建一个存储过程&#xff1f; create procedure 存储过程名称([参数列表]) beginsql语句; end#例 create procedure p1() beginselect * from t_goods;select * from t_user; end如…

【Android实现16位灰度图数据转RGB数据并以bitmap格式显示】

Android实现16位灰度图数据转RGB数据并以bitmap显示(单通道Gray数据转三通道RGB数据并显示) 需求发现问题解决方案需求 问题需求:项目上需要实现将深度相机传感器给出的数据实时显示出来的功能。经过了解得知,传感器给出的数据为16位灰度图数据,即16位数据表示一个像素的…

SpringMVC基础入门(一)之理论基础概念

文章目录SpringMVC1.概念2.常用注解请求与响应1.请求参数2.JSON传输3.常用注解响应1.响应页面2.响应JSON数据Rest风格1.介绍2.常用注解SpringMVC 1.概念 &#xff08;1&#xff09;定义 SpringMVC是一种基于Java实现MVC模型的轻量级Web框架。 &#xff08;2&#xff09;为什…

vue项目第三天

论坛项目动态路由菜单以及渲染用户登录全局前置拦截器获取用户的菜单以及接口执行过程解析菜单数据&#xff0c;渲染伟动态路由。菜单数据将数据源解析为类似路由配置对象。使用递归方法解析菜单数据/*** 解析对象数据* params {menu集合数据} list* return {返回对象}* **/ fu…