3.5版本响应式
在vue3.5中,响应式系统由三个类构成,Dep, Subscriber, Link, Subscriber就是之前讲的Effect, 在3.5中 Effect 继承实现了Subscriber, 所以Effect和Subscriber是等价的。在3.5中新增了Link,主要是用来连接Dep和Subscriber的,3.5中Dep和Subscriber不再直接联系,而是通过中间层Link进行连接。常见的 Dep 有 ref, reactive, computed,常见的 Subscriber 有computed, watch, watchEffect, render函数等
在3.5版本中,Dep和Subscriber依然是多对多的关系,只是这种关系通过Link来维护
接下来将结合3.5源码讲述相比3.5之前的版本,响应式系统有哪些变化。
首先,我们先看下源码中对 Dep, Subscriber, Link类中有哪些重要的属性,先有个印象就行
export class Link {
nextDep?: Link // 下一个 dep
prevDep?: Link // 上一个 dep
nextSub?: Link // 下一个 Subscriber
prevSub?: Link // 上一个 Subscriber
constructor(
// Link节点对应的 Subscriber
public sub: Subscriber,
// Link节点对应的 Dep
public dep: Dep,
) {
}
}
export class Dep {
// sub属性,指向队尾最后一个Subscriber
subs?: Link = undefined
// 收集依赖
track()
}
export interface Subscriber extends DebuggerOptions {
// Subscriber中订阅的第一个dep
deps?: Link
// Subscriber中订阅的最后一个dep
depsTail?: Link
// 执行自身的更新逻辑
notify()
}依赖收集
我们以下面这个例子举例, 看完之后你就会知道上面的每个属性都代表什么意思了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { createApp, computed, ref, watchEffect } from './vue.runtime.esm-bundler.js'
const count1 = ref(1)
const count2 = ref(2)
watchEffect(() => {
console.log(count1.value + count2.value);
})
watchEffect(() => {
console.log(count1.value + count2.value);
})
count1.value ++
</script>
</body>
</html>watchEffect 是 Subscriber的一种,当脚本开始执行的时候,我们会先执行第一个 watchEffect 函数,自然就会执行 effect中的run函数(effect 是 Subscriber的具体实现,两者等价, 后面出现的effect都为 Subscriber的具体实现), 执行run函数的时候,我们会把当前effect的实例,即Subscriber放到全局上, 然后执行我们传给 watchEffect的函数, 而传给watchEffect的函数中,我们访问到了count1和count2, 会对这两个ref实例进行依赖收集
export class ReactiveEffect<T = any>
implements Subscriber, ReactiveEffectOptions
{
run(): T {
// 将当前的 Subscriber 实例放到全局上
activeSub = this
shouldTrack = true
// 执行传给 watchEffect 的函数,开始对 count1 和 count2进行依赖收集
return this.fn()
}
}接下来我们开始依赖收集的过程, 我们先访问 count1
class RefImpl<T = any> {
// 每一个 ref上都有一个dep用来依赖收集
dep: Dep = new Dep()
// 当访问到 count1 会进来这里
get value() {
// 让 count1 收集当前的 Subscriber
this.dep.track()
return this._value
}
}
export class Dep {
track(): Link | undefined {
if (!activeSub || !shouldTrack || activeSub === this.computed) {
return
}
// activeLink每次进来都是空的,会在 effect.run方法执行后给清空掉
let link = this.activeLink
// 成立
if (link === undefined || link.sub !== activeSub) {
// 初始化link节点,并把 dep和当前的 subscriber实例传进去
link = this.activeLink = new Link(activeSub, this)
// 此时第一个 watchEffect 还没有 订阅过 dep,所以是空的,条件成立
if (!activeSub.deps) {
// 由于是第一个收集的dep, 让当前的 subscriber的 deps队头和队尾都指向新建的link节点
activeSub.deps = activeSub.depsTail = link
} else {
// 这个分支暂时还走不到
link.prevDep = activeSub.depsTail
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
}
// 调用 addSub方法,维护 dep和 link的关系
addSub(link)
}
return link
}
}
// 记住,link节点保存到当前响应式变量对应的dep,以及正在执行的 Subscriber实例
function addSub(link: Link) {
// count1对应的dep还没有subs属性
const currentTail = link.dep.subs
if (currentTail !== link) {
link.prevSub = currentTail
if (currentTail) currentTail.nextSub = link
}
// 主要执行这一句,让 count1的dep.subs属性指向新创建的link
link.dep.subs = link
}记住,link节点保存到当前响应式变量对应的dep,以及正在执行的 Subscriber实例
我们来画个图,看看此刻 Subscriber, Dep, Link之间的关系
此时的响应式系统角色有 第1个watcherEffect, count1, Link1

Link1 节点中保存了count1对应的dep以及watchEffect1, 分别用的是 sub 和 dep属性, 每个link节点都这样,后面不再讲
watchEffect是Subscriber,它有两个属性 deps和 depsTail,deps指向 watchEffect订阅的第一个dep对应的Link,这里就是Link1, depsTail指向watchEffect订阅的最后一个dep对应的Link,这里由于只有一个,因此也是 Link1
count1是Dep,它有一个属性 subs,指向最后一个收集它的watchEffect对应的Link节点,这里是Link1
为什么要这样做? 本质上就是维护一个双向链表 往下看就知道了
接下来访问count2, 进行依赖收集
class RefImpl<T = any> {
// 每一个 ref上都有一个dep用来依赖收集
dep: Dep = new Dep()
// 当访问到 count2 会进来这里
get value() {
// 让 count2 收集当前的 Subscriber
this.dep.track()
return this._value
}
}
export class Dep {
track(): Link | undefined {
if (!activeSub || !shouldTrack || activeSub === this.computed) {
return
}
// activeLink每次进来都是空的,会在 effect.run方法执行后给清空掉
let link = this.activeLink
// 成立
if (link === undefined || link.sub !== activeSub) {
// 初始化link节点,并把 dep和当前的 subscriber实例传进去
link = this.activeLink = new Link(activeSub, this)
// 结合上面的图,此时 watchEffect1 已经订阅了 count1, 不成立
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link
} else {
// 走这里
// 让新创建的Link2的 prevDep属性指向activeSub的最后一个link,目前是Link1
link.prevDep = activeSub.depsTail
// 让 Link1 的 nextDep 指向 Link2
activeSub.depsTail!.nextDep = link
// 让 Link2 成为 activeSub 即 watchEffect1 的队尾节点
activeSub.depsTail = link
}
// 调用 addSub方法,维护 dep和 link的关系
addSub(link)
}
return link
}
}
// 记住,link节点保存到当前响应式变量对应的dep,以及正在执行的 Subscriber实例
function addSub(link: Link) {
// count2对应的dep还没有subs属性
const currentTail = link.dep.subs
if (currentTail !== link) {
link.prevSub = currentTail
if (currentTail) currentTail.nextSub = link
}
// 主要执行这一句,让 count2的dep.subs属性指向新创建的link2
link.dep.subs = link
}此时的图是这样的

接下来我们访问 watchEffect2, 同样会对 count1 和 count2进行依赖收集
// 第2个 watchEffect, watcheEffect2
watchEffect(() => {
console.log(count1.value + count2.value);
})count1依赖收集
class RefImpl<T = any> {
// 每一个 ref上都有一个dep用来依赖收集
dep: Dep = new Dep()
// 当访问到 count1 会进来这里
get value() {
// 让 count1 收集当前的 Subscriber
this.dep.track()
return this._value
}
}
export class Dep {
track(): Link | undefined {
if (!activeSub || !shouldTrack || activeSub === this.computed) {
return
}
// activeLink每次进来都是空的,会在 effect.run方法执行后给清空掉
let link = this.activeLink
// 成立
if (link === undefined || link.sub !== activeSub) {
// 初始化link3节点,并把 dep和当前的 subscriber实例传进去
link = this.activeLink = new Link(activeSub, this)
// 此时 watchEffect2 刚刚执行,还没有订阅任何变量 成立
if (!activeSub.deps) {
// 走这里
activeSub.deps = activeSub.depsTail = link
} else {
link.prevDep = activeSub.depsTail
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
}
// 调用 addSub方法,维护 dep和 link的关系
addSub(link)
}
return link
}
}
// 记住,link节点保存到当前响应式变量对应的dep,以及正在执行的 Subscriber实例
function addSub(link: Link) {
// count1对应的dep已经有subs了,因为之前收集过 watchEffect1了,结合上面的图,此时currentTail就是 Link1
const currentTail = link.dep.subs
// Link1 不等于 Link3 成立
if (currentTail !== link) {
// 让 Link3 的 prevSub 指向 Link1
link.prevSub = currentTail
// 让 Link1 的 nextSub指向 Link3
if (currentTail) currentTail.nextSub = link
}
// 让 count1的dep.subs属性指向新创建的link3
link.dep.subs = link
}此时的图是这样子的

接下来 watchEffect2 访问 count2
class RefImpl<T = any> {
// 每一个 ref上都有一个dep用来依赖收集
dep: Dep = new Dep()
// 当访问到 count2 会进来这里
get value() {
// 让 count2 收集当前的 Subscriber
this.dep.track()
return this._value
}
}
export class Dep {
track(): Link | undefined {
if (!activeSub || !shouldTrack || activeSub === this.computed) {
return
}
// activeLink每次进来都是空的,会在 effect.run方法执行后给清空掉
let link = this.activeLink
// 成立
if (link === undefined || link.sub !== activeSub) {
// 初始化link4节点,并把 dep和当前的 subscriber实例传进去
link = this.activeLink = new Link(activeSub, this)
// 此时 watchEffect2 已经订阅过 count1, 不成立
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link
} else {
// 让 link4.prevDep指向 activeSub.depsTail,结合上面的图,此时是 Link3
// 因此是让 Link4.prevDep 指向 Link3
link.prevDep = activeSub.depsTail
// 让 Link3.nextDep指向 link4
activeSub.depsTail!.nextDep = link
// 让 watchEffect2的队尾 depsTail指向 Link4
activeSub.depsTail = link
}
// 调用 addSub方法,维护 dep和 link的关系
addSub(link)
}
return link
}
}
// 记住,link节点保存到当前响应式变量对应的dep,以及正在执行的 Subscriber实例
function addSub(link: Link) {
// count2对应的dep已经有subs了,因为之前收集过 watchEffect1了,结合上面的图,此时currentTail就是 Link2
const currentTail = link.dep.subs
// Link2 不等于 Link4 成立
if (currentTail !== link) {
// 让 Link4 的 prevSub 指向 Link2
link.prevSub = currentTail
// 让 Link2 的 nextSub指向 Link4
if (currentTail) currentTail.nextSub = link
}
// 让 count2的dep.subs属性指向新创建的link4
link.dep.subs = link
}此时的图是这样子的

至此,依赖收集的过程结束
为什么要维护这个结构呢? 因为当响应式变量发生变化的时候,可以通过响应式变量的dep属性,访问到dep.subs
举个例子,当count1发生变化的时候,我们就可以取出它对应的dep,进而通过dep.subs访问到 dep维护的最后一个Link节点,也就是Link3, 而Link3的sub属性,恰好就是watchEffect2, 再通过 Link3 的 prevSub就可以访问到 Link1, 再通过 Link1的sub属性访问到 watchEffect1,这个也是依赖触发更新的过程
export class Dep {
notify(): void {
// 从队尾到队头依次取出对应的Subscriber执行
for (let link = this.subs; link; link = link.prevSub) {
link.sub.notify()
}
}
}清除不用的依赖
换个例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { createApp, computed, ref } from './vue.runtime.esm-bundler.js'
const app = createApp({
template: `
<p>{{ doubleCount }}</p>
<button @click="changeFlag">切换flag</button>
`,
setup() {
const count1 = ref(1)
const count2 = ref(10)
const flag = ref(true)
const doubleCount = computed(() => {
console.log('computed');
if(flag.value) {
return count1.value * 2
} else {
return count2.value * 2
}
})
const changeFlag = () => {
flag.value = !flag.value
}
return {
count1,
count2,
flag,
doubleCount,
changeFlag,
}
}
})
app.mount('#app')
</script>
</body>
</html>当依赖收集完毕后,我们的响应式系统是这样的
