提醒

👇👇---👇👇微信联系我👇👇---👇👇

欢迎大家加微信私信交流

Skip to content

Vue Reactivity

概述

​ 截至目前,Vue 凭借强大的功能、较低的学习成本、以及丰富的生态等显著优势,在国内外 Web 开源框架领域中占据了相当大的市场份额。可以说,如果想要快速高效地构建Web应用,Vue的易用性使其已经成为了不少开发者的“不二之选”。Vue的易用性主要依托其两大核心特性实现,具体如下:

  1. 声明式渲染:Vue基于标准HTML拓展了一套简单易懂的模版语法,可以使开发者声明式地描述视图与状态之间的关系。

​ 举个例子,现有一个View页面,它包含了foobarbaz三个JavaScript状态。

状态视图关系

​ 那么我们可以轻松地利用Vue模版语法描述出该View页面视图,如下。

vue
<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>
  1. 响应式:Vue会自动追踪视图所依赖的状态,并在这些状态发生变化时更新视图。

​ 在上述示例中,View页面视图模版中使用了foobarbaz这状态,Vue会通过内部机制自动完成对这些状态依赖的收集工作。在此基础上,一旦foobarbaz中任意一个状态的值发生了变更,View页面视图将随之进行对应的更新操作。通常,我们将这种更新称为 响应式更新

视图响应式更新

​ 需要说明的是,在Vue中,并非所有的JavaScript状态都是响应式的。响应式状态的构建,必须通过Vue提供的特定API定义,如refreactivecomputed等。通常,我们也称这些特殊的JavaScript状态为 响应式状态。在上述示例中,我们可以通过ref来定义foobar以及baz这三个状态,如下。

vue
<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 的渲染系统基于虚拟DOMVirtual DOM)实现。虚拟DOM是由虚拟节点(vnode)组成的树状数据结构,其本质是通过运行时的数据格式对UI视图进行的抽象化描述。在 Vue中,开发者仅需提供虚拟DOM的描述信息,Vue内部的渲染管线便会自动执行一系列工作,最终将虚拟DOM树映射为浏览器可识别的真实DOM树。

​ 在 Vue 中,创建视图对应的虚拟DOM的方式有很多种,主要包括以下几种:

  1. 声明式描述UI的模板语法
  2. 基于特定 DSL(JSXTSX等)的编写方式
  3. 直接编写render函数。

​ 其中,前两种方式都是第三种方式的“语法糖”,它们最终都会编译成render函数。render函数的核心作用是通过调用渲染函数h(即createVNode的简写形式)创建一个个虚拟节点(vnode),并将这些虚拟节点按照视图的结构关系进行组织,最终返回与视图对应的一棵完整的虚拟 DOM 树。

​ 由于视图的渲染逻辑最终可以通过render函数体现,因此视图与响应式状态之间的关系可转化为render函数与响应式状态的关系。具体而言,render函数在执行过程中会访问并依赖响应式状态,当被依赖的响应式状态发生更改后,render函数会被重新执行。

render函数与响应式状态

​ 本文不会阐述Vue渲染系统的具体实现,而是探究Vue的响应式系统。在Vue中,视图的响应式更新只是响应式系统的典型应用,其中render函数会产生一个副作用,即更新视图对应的虚拟DOM,因此这类函数也常常被称为副作用函数effect)。副作用函数所使用的响应式状态,被称为该副作用函数的依赖dependency);而副作用函数本身,也可称为其依赖的一个订阅者subscriber)。

​ 综上所述,Vue响应式的本质就是:当副作用函数中的依赖发生变更时,副作用函数重新执行

响应式系统实现

​ 以下内容将深入探究 Vue 响应式系统的具体源码实现,所参照的 Vue 版本为3.5.17。需要说明的是,Vue 响应式系统的目录为./packages/reactivity

最简单的响应式系统

​ 以下为一个最简响应式系统的测试用例:其具体场景为,在副作用函数effect中仅依赖一个内部值为基础类型的ref响应式状态。

typescript
import { effect, ref } from '../reactivity'

const foo = ref('Foo')

effect(() => {
    console.log('foo value is: ' + foo.value)
})

foo.value = 'New Foo'
预期结果

​ 该测试用例的预期结果为先后打印如下内容:

  1. 'foo value is: Foo'
  2. 'foo value is: New Foo'

​ 在原生JavaScript中,不存在任何机制可以追踪普通变量(非属性)的值的读写操作,这使得响应式的实现无法直接完成。为此,Vue 设计了refAPI,它可以创建一个包装器Ref对象,该对象通过value属性来存储JavaScript值。

​ 对于对象的属性,我们可借助gettersetter轻松拦截其读写操作,以此实现最基础的响应式功能。核心实现思想如下:

  1. 维护一个全局变量activeSub,用于存储当前正在运行的副作用函数,以便在响应式状态被读取时建立关联关系。

  2. 拦截Ref对象value属性的读操作:通过另一属性sub存储当前运行状态的副作用函数activeSub,即订阅者(如果存在);拦截Ref对象value属性的写操作:主动调用sub属性中存储的副作用函数(如果存在)。

存储单个订阅者

typescript
export let activeSub: Function | undefined = undefined

export function effect(fn: Function): void {
    activeSub = fn
    activeSub()
    activeSub = undefined
}
typescript
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)
}
typescript
export enum ReactiveFlags {
    IS_REF = '_v_isRef'
}

双向链表存储

​ 对于一个Ref对象而言,其存储的响应式状态往往可能被多个副作用函数所使用,例如以下示例。

typescript
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'
预期结果

​ 该测试用例的预期结果为先后打印如下内容:

  1. 'foo value is: Foo'
  2. 'foo: Foo'
  3. 'foo value is: New Foo'
  4. 'foo: New Foo'

​ 在这种情况下,Ref对象需要通过集合类容器来存放多个订阅者。由于该关系涉及较多写操作,Vue选择了双向链表这一数据结构,其核心实现思想如下:

  1. 收集依赖:读取Ref对象的value属性前,采用双向链表存储副作用函数,链表节点抽象为Link。其中,头节点属性为subsHead,尾节点属性为subs

  2. 触发更新:写入Ref对象的value属性后,遍历双向链表并执行每个节点中存储的副作用函数。

双向链表存储订阅者

typescript
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嵌套

​ 以下是一个副作用函数嵌套执行的测试用例。

typescript
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'
预期结果

​ 该测试用例的预期结果为先后打印如下内容:

  1. 'foo value is: Foo'
  2. 'foo: Foo'
  3. 'foo value is: New Foo'
  4. 'foo value is: New Foo'
  5. foo: New Foo

​ 对于该测试用例,上述响应式系统实现会出现不符合预期的结果,主要存在以下两个问题:

  1. 内层effect执行前后,会重新设置全局副作用变量activeSub并将其置为undefined,此时外层effect的依赖收集尚未完成,进而导致依赖收集出现混乱。

  2. 对于外层effect而言,其每次重新执行时都会生成一个新的effect。当Ref对象触发更新时,若采用一边遍历一边调用的策略,会使得新生成的effect立即执行,而非在下次更新时执行,最终造成重复更新的情况。

​ 针对上述两个effect嵌套问题,可采用以下解决方案:

  1. 任一effect执行前,先用局部变量保存activeSub的原值,待执行完成后,再恢复该原值。

  2. Ref对象触发更新时,先将所有副作用函数收集到一个数组中,最后依次执行数组中的函数。

typescript
export let activeSub: Function | undefined = undefined

export function effect(fn: Function): void {
    let prevSub = activeSub 
    activeSub = fn
    activeSub()
    activeSub = prevSub 
}
typescript
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

typescript
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()
}
typescript
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())
    }
}
typescript
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)
}
typescript
export enum ReactiveFlags {
    IS_REF = '_v_isRef'
}

未完待续

​ 双向链接、节点复用、分支切换、依赖清理,状态位运算、批量操作等。