Vue Reactivity
概述
截至目前,Vue 凭借强大的功能、较低的学习成本、以及丰富的生态等显著优势,在国内外 Web 开源框架领域中占据了相当大的市场份额。可以说,如果想要快速高效地构建Web应用,Vue的易用性使其已经成为了不少开发者的“不二之选”。Vue的易用性主要依托其两大核心特性实现,具体如下:
- 声明式渲染:Vue基于标准HTML拓展了一套简单易懂的模版语法,可以使开发者声明式地描述视图与状态之间的关系。
举个例子,现有一个View
页面,它包含了foo
、bar
和baz
三个JavaScript状态。
那么我们可以轻松地利用Vue模版语法描述出该View
页面视图,如下。
<template>
<h1 class="text-4xl">
<span class="bg-gradient-to-r from-[#ec695c] to-[#61c454] bg-[length:0_2px]
bg-no-repeat bg-right-bottom hover:bg-[length:100%_2px] transition-[background-size]
duration-500 ease-linear hover:bg-left-bottom">
{{ foo }}
</span>
</h1>
<div class="inline-block px-3 py-1.5 m-1.5 rounded-md bg-gray-100 text-gray-800
hover:bg-gray-200 transition-colors duration-300 text-base">
<span>Bar: {{ bar }}</span>
<span>baz: {{ baz }}</span>
</div>
</template>
- 响应式:Vue会自动追踪视图所依赖的状态,并在这些状态发生变化时更新视图。
在上述示例中,View
页面视图模版中使用了foo
、bar
和baz
这状态,Vue会通过内部机制自动完成对这些状态依赖的收集工作。在此基础上,一旦foo
、bar
、baz
中任意一个状态的值发生了变更,View
页面视图将随之进行对应的更新操作。通常,我们将这种更新称为 响应式更新。
需要说明的是,在Vue中,并非所有的JavaScript状态都是响应式的。响应式状态的构建,必须通过Vue提供的特定API定义,如ref
、reactive
、computed
等。通常,我们也称这些特殊的JavaScript状态为 响应式状态。在上述示例中,我们可以通过ref
来定义foo
、bar
以及baz
这三个状态,如下。
<script setup lang="ts">
import { ref } from 'vue'
const foo = ref('foo')
const bar = ref('bar')
const baz = ref('baz')
</script>
<template>
<h1 class="text-4xl">
<span class="bg-gradient-to-r from-[#ec695c] to-[#61c454] bg-[length:0_2px]
bg-no-repeat bg-right-bottom hover:bg-[length:100%_2px] transition-[background-size]
duration-500 ease-linear hover:bg-left-bottom">
{{ foo }}
</span>
</h1>
<div class="inline-block px-3 py-1.5 m-1.5 rounded-md bg-gray-100 text-gray-800
hover:bg-gray-200 transition-colors duration-300 text-base">
<span>Bar: {{ bar }}</span>
<span>baz: {{ baz }}</span>
</div>
</template>
响应式本质
与多数JavaScript框架类似,Vue 的渲染系统基于虚拟DOM(Virtual DOM
)实现。虚拟DOM是由虚拟节点(vnode
)组成的树状数据结构,其本质是通过运行时的数据格式对UI视图进行的抽象化描述。在 Vue中,开发者仅需提供虚拟DOM的描述信息,Vue内部的渲染管线便会自动执行一系列工作,最终将虚拟DOM树映射为浏览器可识别的真实DOM树。
在 Vue 中,创建视图对应的虚拟DOM的方式有很多种,主要包括以下几种:
- 声明式描述UI的模板语法
- 基于特定 DSL(
JSX
、TSX
等)的编写方式 - 直接编写
render
函数。
其中,前两种方式都是第三种方式的“语法糖”,它们最终都会编译成render
函数。render
函数的核心作用是通过调用渲染函数h
(即createVNode
的简写形式)创建一个个虚拟节点(vnode
),并将这些虚拟节点按照视图的结构关系进行组织,最终返回与视图对应的一棵完整的虚拟 DOM 树。
由于视图的渲染逻辑最终可以通过render
函数体现,因此视图与响应式状态之间的关系可转化为render
函数与响应式状态的关系。具体而言,render
函数在执行过程中会访问并依赖响应式状态,当被依赖的响应式状态发生更改后,render
函数会被重新执行。
本文不会阐述Vue渲染系统的具体实现,而是探究Vue的响应式系统。在Vue中,视图的响应式更新只是响应式系统的典型应用,其中render
函数会产生一个副作用,即更新视图对应的虚拟DOM,因此这类函数也常常被称为副作用函数(effect
)。副作用函数所使用的响应式状态,被称为该副作用函数的依赖(dependency
);而副作用函数本身,也可称为其依赖的一个订阅者(subscriber
)。
综上所述,Vue响应式的本质就是:当副作用函数中的依赖发生变更时,副作用函数重新执行。
响应式系统实现
以下内容将深入探究 Vue 响应式系统的具体源码实现,所参照的 Vue 版本为3.5.17
。需要说明的是,Vue 响应式系统的目录为./packages/reactivity
。
最简单的响应式系统
以下为一个最简响应式系统的测试用例:其具体场景为,在副作用函数effect
中仅依赖一个内部值为基础类型的ref
响应式状态。
import { effect, ref } from '../reactivity'
const foo = ref('Foo')
effect(() => {
console.log('foo value is: ' + foo.value)
})
foo.value = 'New Foo'
预期结果
该测试用例的预期结果为先后打印如下内容:
'foo value is: Foo'
'foo value is: New Foo'
在原生JavaScript中,不存在任何机制可以追踪普通变量(非属性)的值的读写操作,这使得响应式的实现无法直接完成。为此,Vue 设计了ref
API,它可以创建一个包装器Ref
对象,该对象通过value属性来存储JavaScript值。
对于对象的属性,我们可借助getter
与setter
轻松拦截其读写操作,以此实现最基础的响应式功能。核心实现思想如下:
维护一个全局变量
activeSub
,用于存储当前正在运行的副作用函数,以便在响应式状态被读取时建立关联关系。拦截
Ref
对象value属性的读操作:通过另一属性sub
存储当前运行状态的副作用函数activeSub
,即订阅者(如果存在);拦截Ref
对象value属性的写操作:主动调用sub属性中存储的副作用函数(如果存在)。
export let activeSub: Function | undefined = undefined
export function effect(fn: Function): void {
activeSub = fn
activeSub()
activeSub = undefined
}
import { ReactiveFlags } from './constant'
import { activeSub } from "./effect";
class RefImpl<T> {
public _value: T
public readonly [ReactiveFlags.IS_REF] = true
public sub?: Function
constructor(value: T) {
this._value = value
}
get value(): T {
// track
this.sub = activeSub
return this._value
}
set value(value: T) {
this._value = value
// trigger
if (this.sub) {
this.sub()
}
}
}
export function ref<T>(value: T) {
return new RefImpl<T>(value)
}
export enum ReactiveFlags {
IS_REF = '_v_isRef'
}
双向链表存储
对于一个Ref
对象而言,其存储的响应式状态往往可能被多个副作用函数所使用,例如以下示例。
import { effect, ref } from '../reactivity'
const foo = ref('Foo')
effect(() => {
console.log('foo value is: ' + foo.value)
})
effect(() => {
console.log('foo: ' + foo.value)
})
foo.value = 'New Foo'
预期结果
该测试用例的预期结果为先后打印如下内容:
'foo value is: Foo'
'foo: Foo'
'foo value is: New Foo'
'foo: New Foo'
在这种情况下,Ref
对象需要通过集合类容器来存放多个订阅者。由于该关系涉及较多写操作,Vue选择了双向链表这一数据结构,其核心实现思想如下:
收集依赖:读取
Ref
对象的value属性前,采用双向链表存储副作用函数,链表节点抽象为Link。其中,头节点属性为subsHead
,尾节点属性为subs
。触发更新:写入
Ref
对象的value属性后,遍历双向链表并执行每个节点中存储的副作用函数。
import { ReactiveFlags } from './constant'
import { activeSub } from './effect'
class RefImpl<T> {
public _value: T
public readonly [ReactiveFlags.IS_REF] = true
public subs: Link | undefined
public subsHead: Link | undefined
constructor(value: T) {
this._value = value
}
get value(): T {
this.track()
return this._value
}
set value(value: T) {
this._value = value
this.trigger()
}
private track() {
if (!activeSub) {
return
}
const link = new Link(activeSub)
if (!this.subs) {
this.subs = this.subsHead = link
return
}
this.subs.nextSub = link
link.prevSub = this.subs
this.subs = link
}
private trigger() {
let link: Link | undefined = this.subsHead
while (link) {
link.sub()
link = link.nextSub
}
}
}
export function ref<T>(value: T) {
return new RefImpl<T>(value)
}
class Link {
public nextSub: Link | undefined
public prevSub: Link | undefined
constructor(public sub: Function) {
}
}
effect嵌套
以下是一个副作用函数嵌套执行的测试用例。
import { ref, effect } from '../reactivity'
const foo = ref('Foo')
effect(() => {
effect(() => {
console.log('foo value is: ', foo.value)
})
console.log('foo: ', foo.value)
})
foo.value = 'New Foo'
预期结果
该测试用例的预期结果为先后打印如下内容:
'foo value is: Foo'
'foo: Foo'
'foo value is: New Foo'
'foo value is: New Foo'
foo: New Foo
对于该测试用例,上述响应式系统实现会出现不符合预期的结果,主要存在以下两个问题:
内层
effect
执行前后,会重新设置全局副作用变量activeSub
并将其置为undefined
,此时外层effect的依赖收集尚未完成,进而导致依赖收集出现混乱。对于外层
effect
而言,其每次重新执行时都会生成一个新的effect
。当Ref
对象触发更新时,若采用一边遍历一边调用的策略,会使得新生成的effect
立即执行,而非在下次更新时执行,最终造成重复更新的情况。
针对上述两个effect
嵌套问题,可采用以下解决方案:
任一
effect
执行前,先用局部变量保存activeSub
的原值,待执行完成后,再恢复该原值。Ref
对象触发更新时,先将所有副作用函数收集到一个数组中,最后依次执行数组中的函数。
export let activeSub: Function | undefined = undefined
export function effect(fn: Function): void {
let prevSub = activeSub
activeSub = fn
activeSub()
activeSub = prevSub
}
import { ReactiveFlags } from './constant'
import { activeSub } from './effect'
class RefImpl<T> {
public _value: T
public readonly [ReactiveFlags.IS_REF] = true
public subs: Link | undefined
public subsHead: Link | undefined
constructor(value: T) {
this._value = value
}
get value(): T {
this.track()
return this._value
}
set value(value: T) {
this._value = value
this.trigger()
}
private track() {
if (!activeSub) {
return
}
const link = new Link(activeSub)
if (!this.subs) {
this.subs = this.subsHead = link
return
}
this.subs.nextSub = link
link.prevSub = this.subs
this.subs = link
}
private trigger() {
let link: Link | undefined = this.subsHead
const pendingSubs: Function[] = []
while (link) {
pendingSubs.push(link.sub)
link = link.nextSub
}
pendingSubs.forEach(pendingSub => pendingSub())
}
}
export function ref<T>(value: T) {
return new RefImpl<T>(value)
}
class Link {
public nextSub: Link | undefined
public prevSub: Link | undefined
constructor(public sub: Function) {
}
}
面向对象(OOP)
在Vue的响应式系统中,各项职能的抽象划分十分清晰。基于此,我们可借助TypeScript强大的面向对象编程(OOP)特性,对现有代码进行改造,主要包括将effect
抽象为Subscriber
,将响应式状态抽象为Dep
。
export interface Subscriber {
notify: () => void
}
export let activeSub: ReactiveEffect | undefined = undefined
export class ReactiveEffect implements Subscriber {
constructor(public fn: Function) {
}
run(): void {
const prevSub = activeSub
try {
activeSub = this
activeSub.fn()
} finally {
activeSub = prevSub
}
}
notify(): void {
this.fn()
}
}
export function effect(fn: Function): void {
const e = new ReactiveEffect(fn)
e.run()
}
import { activeSub, type Subscriber } from './effect'
/**
* 链表节点
*/
export class Link {
public nextSub: Link | undefined
public prevSub: Link | undefined
constructor(public sub: Subscriber) {
}
}
/**
* 依赖
*/
export class Dep {
public subs: Link | undefined
public subsHead: Link | undefined
/**
* 收集依赖
*/
public track(): void {
if (!activeSub) {
return
}
const link = new Link(activeSub)
if (!this.subs) {
this.subs = this.subsHead = link
return
}
this.subs.nextSub = link
link.prevSub = this.subs
this.subs = link
}
/**
* 触发更新
*/
public trigger(): void {
let link: Link | undefined = this.subsHead
const pendingSubs: Subscriber[] = []
while (link) {
pendingSubs.push(link.sub)
link = link.nextSub
}
pendingSubs.forEach(pendingSub => pendingSub.notify())
}
}
import { ReactiveFlags } from './constant'
import { Dep } from './dep'
/**
* ref接口
*/
interface Ref<T> {
get value(): T
set value(val: T)
}
/**
* ref实现
*/
class RefImpl<T> implements Ref<T> {
public _value: T
public readonly [ReactiveFlags.IS_REF] = true
public dep: Dep = new Dep()
constructor(value: T) {
this._value = value
}
get value(): T {
this.dep.track()
return this._value
}
set value(value: T) {
this._value = value
this.dep.trigger()
}
}
export function ref<T>(value: T) {
return new RefImpl<T>(value)
}
export enum ReactiveFlags {
IS_REF = '_v_isRef'
}
未完待续
双向链接、节点复用、分支切换、依赖清理,状态位运算、批量操作等。