北京网站建设飞沐,网页设计与网站建设完全学习手册,手机域名,有没有通信专业业余做兼职的网站文章目录1. VUE的响应式原理1.1 ViewModel1.2 双向绑定的基本原理1.3 什么是响应性1.4 Vue 中的响应性是如何工作的2. Vue 渲染机制2.1 虚拟 DOM2.2 渲染管线2.3 带编译时信息的虚拟 DOM2.3.1 静态提升2.3.2 修补标记 Flags2.3.3 树结构打平2.3.4 对 SSR 激活的影响1. VUE的响应…
文章目录1. VUE的响应式原理1.1 ViewModel1.2 双向绑定的基本原理1.3 什么是响应性1.4 Vue 中的响应性是如何工作的2. Vue 渲染机制2.1 虚拟 DOM2.2 渲染管线2.3 带编译时信息的虚拟 DOM2.3.1 静态提升2.3.2 修补标记 Flags2.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.defineProperty 和 Proxy 。
Vue 2 使用 Object.defineProperty 完全由于需支持更旧版本浏览器的限制。在 Vue 3 中使用了 Proxy 来创建响应式对象将 getter/setter 用于 ref。 首先要对数据data进行劫持监听。所以需要设置一个监听器Observer用来监听所有的属性。 每一个组件都有一个Watcher实例。如果属性发生变化需要通知订阅者Watcher看是否需要更新。因为订阅者有多个所以需要一个消息订阅器发布者Dep订阅者集合的管理数组来专门收集这些订阅者在Observer和Watcher之间进行统一管理。 还需要一个指令解析器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() 函数会产生一个副作用或者就简称为作用因为它会更改程序里的状态。A0 和 A1 被视为这个作用的依赖因为它们的值被用来执行这个作用。因此这次作用也可以说是一个它依赖的订阅者。
我们需要一个魔法函数能够在 A0 或 A1 (这两个依赖) 变化时调用 update() (产生作用)。
whenDepsChange(update)这个 whenDepsChange() 函数有如下的任务
当一个变量被读取时进行追踪。例如我们执行了表达式 A0 A1 的计算则 A0 和 A1 都被读取到了。如果一个变量在当前运行的副作用中被读取了就将该副作用设为此变量的一个订阅者。例如由于 A0 和 A1 在 update() 执行时被访问到了则 update() 需要在第一次调用之后成为 A0 和 A1 的订阅者。探测一个变量的变化。例如当我们给 A0 赋了一个新的值后应该通知其所有订阅了的副作用重新执行。
1.4 Vue 中的响应性是如何工作的
我们是可以追踪一个对象的属性进行读和写的。
在 JavaScript 中有两种劫持属性访问的方式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] 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)}
}副作用订阅将被存储在一个全局的 WeakMaptarget, Mapkey, Seteffect 数据结构中。如果在第一次追踪时没有找到对相应属性订阅的副作用集合它将会在这里新建。这就是 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 vueconst 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 vueconst A0 ref(0)
const A1 ref(1)
const A2 computed(() A0.value A1.value)A0.value 2在内部computed 会使用响应式副作用来管理失效与重新计算的过程。
那么常见的响应式副作用的用例是什么呢自然是更新 DOM我们可以像下面这样实现一个简单的“响应式渲染”
import { ref, watchEffect } from vueconst 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 的响应式追踪而像 watch 和 computed 这样的选项也是在内部调用相应等价的组合式 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 渲染管线
编译Vue 模板被编译为了渲染函数即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成也可以通过使用运行时编译器即时完成。挂载运行时渲染器调用渲染函数遍历返回的虚拟 DOM 树并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行因此它会追踪其中所用到的所有响应式依赖。修补当一个依赖发生变化后副作用会重新运行这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树将它与旧树进行比较然后将必要的更新应用到真实 DOM 上去。 2.3 带编译时信息的虚拟 DOM
虚拟 DOM 在 React 和大多数其他实现中都是纯运行时的协调算法无法预知新的虚拟 DOM 树会是怎样因此它总是需要遍历整棵树、比较每个 vnode 上 props 的区别来确保正确性。另外即使一棵树的某个部分从未改变还是会在每次重渲染时创建新的 vnode带来了完全不必要的内存压力。这也是虚拟 DOM 最受诟病的地方之一这种有点暴力的协调过程通过牺牲效率来换取可声明性和正确性。
但实际上我们并不需要这样。在 Vue 中框架同时控制着编译器和运行时。这使得我们可以为紧密耦合的模板渲染器应用许多编译时优化。编译器可以静态分析模板并在生成的代码中留下标记使得运行时尽可能地走捷径。与此同时我们仍旧保留了边界情况时用户想要使用底层渲染函数的能力。我们称这种混合解决方案为带编译时信息的虚拟 DOM。
下面我们将讨论一些 Vue 编译器用来提高虚拟 DOM 运行时性能的主要优化
2.3.1 静态提升
在模板中常常有部分内容是不带任何动态绑定的
divdivfoo/div !-- 需提升 --divbar/div !-- 需提升 --div{{ dynamic }}/div
/divfoo 和 bar 这两个 div 是完全静态的没有必要在重新渲染时再次创建和比对它们。Vue 编译器自动地会提升这部分 vnode 创建函数到这个模板的渲染函数之外并在每次渲染时都使用这份相同的 vnode渲染器知道新旧 vnode 在这部分是完全相同的所以会完全跳过对它们的差异比对。
此外当有足够多连续的静态元素时它们还会再被压缩为一个“静态 vnode”其中包含的是这些节点相应的纯 HTML 字符串。这些静态节点会直接通过 innerHTML 来挂载。同时还会在初次挂载后缓存相应的 DOM 节点。如果这部分内容在应用中其他地方被重用那么将会使用原生的 cloneNode() 方法来克隆新的 DOM 节点这会非常高效。
2.3.2 修补标记 Flags
对于单个有动态绑定的元素来说我们可以在编译时推断出大量信息
!-- 仅含 class 绑定 --
div :class{ active }/div!-- 仅含 id 和 value 绑定 --
input :idid :valuevalue!-- 仅含文本子节点 --
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 :idid/div !-- 要追踪 --div !-- 不会追踪 --div{{ bar }}/div !-- 要追踪 --/div
/div编译的结果会被打平为一个数组仅包含所有动态的后代节点
div (block root)
- div 带有 :id 绑定
- div 带有 {{ bar }} 绑定当这个组件需要重渲染时只需要遍历这个打平的树而非整棵树。这也就是我们所说的树结构打平这大大减少了我们在虚拟 DOM 协调时需要遍历的节点数量。模板中任何的静态部分都会被高效地略过。
v-if 和 v-for 指令会创建新的区块节点
div !-- 根区块 --divdiv v-if !-- if 区块 --...div/div
/div一个子区块会在父区块的动态子节点数组中被追踪这为他们的父区块保留了一个稳定的结构。
2.3.4 对 SSR 激活的影响
修补标记和树结构打平都大大提升了 Vue SSR 激活的性能表现
单个元素的激活可以基于相应 vnode 的修补标记走更快的捷径。在激活时只有区块节点和其动态子节点需要被遍历这在模板层面上实现更高效的部分激活。