文章目录
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就会自动更新,在单向绑定的基础上,如果用户更新了View,Model的数据也会自动更新。
双向绑定由三个重要部分构成:
数据层(Model):应用数据及业务逻辑
视图层(View):应用的展示效果,各类UI组件
业务逻辑层(ViewModel):框架封装的核心,负责将数据与视图关联起来
它还有两个主要部分组成:
在 JavaScript 中有两种劫持属性访问的方式:Object.defineProperty 和 Proxy 。
首先要对数据(data)进行劫持监听。所以需要设置一个监听器Observer,用来监听所有的属性。
每一个组件都有一个Watcher实例。如果属性发生变化,需要通知订阅者Watcher,看是否需要更新。因为订阅者有多个,所以需要一个消息订阅器(发布者)Dep(订阅者集合的管理数组)来专门收集这些订阅者,在Observer和Watcher之间进行统一管理。
还需要一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化为一个订阅者Watcher,并替换模板数据或绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
1、实现一个监听器Observer,用来劫持并监听所有属性,如果发生变化,就通知订阅者。
2、实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
3、实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并据此初始化视图和订阅器Watcher。
双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数 据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几 个步骤:
如果我们在 JavaScript 写类似的逻辑:
当我们更改 A0 后,A2 不会自动更新。
那么我们如何在 JavaScript 中做到这一点呢?首先,为了能重新运行计算的代码来更新 A2,我们需要将其包装为一个函数:
let A2
function update() {
A2 = A0 + A1
}
然后,我们需要定义几个术语:
我们需要一个魔法函数,能够在 A0 或 A1 (这两个依赖) 变化时调用 update() (产生作用)。
whenDepsChange(update)
这个 whenDepsChange() 函数有如下的任务:
我们是可以追踪一个对象的属性进行读和写的。
在 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] = 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
}
在 track() 内部,我们会检查当前是否有正在运行的副作用。如果有,我们会查找到一个所有追踪了该属性的订阅者,它们存储在一个 Set 中,然后将当前这个副作用添加到该 Set 中。
// 这会在一个副作用就要运行之前被设置
// 我们会在后面处理它
let activeEffect
function track(target, key) {
if (activeEffect) {
const effects = getSubscribersForProperty(target, key)
effects.add(activeEffect)
}
}
副作用订阅将被存储在一个全局的 WeakMap
在 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 的响应式追踪,而像 watch 和 computed 这样的选项也是在内部调用相应等价的组合式 API。
const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* 更多 vnode */
]
}
这里所说的 vnode 即一个纯 JavaScript 的对象 (一个“虚拟节点”),它代表着一个
一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载 (mount)。
如果我们有两份虚拟 DOM 树,渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为修补 (patch),又被称为“比较差异 (diffing)”或“协调 (reconciliation)”。
虚拟 DOM 带来的主要收益是它赋予了开发者编程式地、声明式地创建、审查和组合所需 UI 结构的能力,而把直接与 DOM 相关的操作交给了渲染器。
虚拟 DOM 在 React 和大多数其他实现中都是纯运行时的:协调算法无法预知新的虚拟 DOM 树会是怎样,因此它总是需要遍历整棵树、比较每个 vnode 上 props 的区别来确保正确性。另外,即使一棵树的某个部分从未改变,还是会在每次重渲染时创建新的 vnode,带来了完全不必要的内存压力。这也是虚拟 DOM 最受诟病的地方之一:这种有点暴力的协调过程通过牺牲效率来换取可声明性和正确性。
但实际上我们并不需要这样。在 Vue 中,框架同时控制着编译器和运行时。这使得我们可以为紧密耦合的模板渲染器应用许多编译时优化。编译器可以静态分析模板并在生成的代码中留下标记,使得运行时尽可能地走捷径。与此同时,我们仍旧保留了边界情况时用户想要使用底层渲染函数的能力。我们称这种混合解决方案为带编译时信息的虚拟 DOM。
下面,我们将讨论一些 Vue 编译器用来提高虚拟 DOM 运行时性能的主要优化:
在模板中常常有部分内容是不带任何动态绑定的:
foo
bar
{{ dynamic }}
foo 和 bar 这两个 div 是完全静态的,没有必要在重新渲染时再次创建和比对它们。Vue 编译器自动地会提升这部分 vnode 创建函数到这个模板的渲染函数之外,并在每次渲染时都使用这份相同的 vnode,渲染器知道新旧 vnode 在这部分是完全相同的,所以会完全跳过对它们的差异比对。
此外,当有足够多连续的静态元素时,它们还会再被压缩为一个“静态 vnode”,其中包含的是这些节点相应的纯 HTML 字符串。这些静态节点会直接通过 innerHTML 来挂载。同时还会在初次挂载后缓存相应的 DOM 节点。如果这部分内容在应用中其他地方被重用,那么将会使用原生的 cloneNode() 方法来克隆新的 DOM 节点,这会非常高效。
2.3.2 修补标记 Flags
对于单个有动态绑定的元素来说,我们可以在编译时推断出大量信息:
{{ dynamic }}
在为这些元素生成渲染函数时,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 */))
}
再来看看上面这个例子中生成的代码,你会发现所返回的虚拟 DOM 树是经一个特殊的 createElementBlock() 调用创建的:
export function render() {
return (_openBlock(), _createElementBlock(_Fragment, null, [
/* children */
], 64 /* STABLE_FRAGMENT */))
}
这里我们引入一个概念“区块”,内部结构是稳定的一个部分可被称之为一个区块。在这个用例中,整个模板只有一个区块,因为这里没有用到任何结构性指令 (比如 v-if 或者 v-for)。
每一个块都会追踪其所有带修补标记的后代节点 (不只是直接子节点),举个例子:
...
{{ bar }}
编译的结果会被打平为一个数组,仅包含所有动态的后代节点:
div (block root)
- div 带有 :id 绑定
- div 带有 {{ bar }} 绑定
当这个组件需要重渲染时,只需要遍历这个打平的树而非整棵树。这也就是我们所说的树结构打平,这大大减少了我们在虚拟 DOM 协调时需要遍历的节点数量。模板中任何的静态部分都会被高效地略过。
v-if 和 v-for 指令会创建新的区块节点:
...
一个子区块会在父区块的动态子节点数组中被追踪,这为他们的父区块保留了一个稳定的结构。
2.3.4 对 SSR 激活的影响
修补标记和树结构打平都大大提升了 Vue SSR 激活的性能表现:
单个元素的激活可以基于相应 vnode 的修补标记走更快的捷径。
在激活时只有区块节点和其动态子节点需要被遍历,这在模板层面上实现更高效的部分激活。
原文链接:https://blog.csdn.net/fmk1023/article/details/125543084