
javascript
import { ref, effect } from '../dist/reactivity.esm.js'
const count = ref(0)
effect(() => {
console.log('effect1', count.value)
})
effect(() => {
console.log('effect2', count.value)
})
setTimeout(() => {
count.value = 1
}, 1000)
昨天,我们了解了链表的核心概念,现在要把这些概念结合起来。
首先让我们从一个常见的场景开始:当一个响应式数据 (ref
) 同时被多个 effect
依赖时,会发生什么?
我们预期它会输出如下:
arduino
console.log('effect1', 0)
console.log('effect2', 0)
// 1秒后
console.log('effect1', 1)
console.log('effect2', 1)
但实际上我们得到的是:
arduino
console.log('effect1', 0)
console.log('effect2', 0)
// 1秒后
console.log('effect2', 1)
发生什么事?
结果很明显:我们上次的 ref
实现,只能让 this.subs
属性一次记住一个订阅者,导致后来的 effect
覆盖了前面的。这会造成以下问题:
- 每次有新的
effect
订阅时,会覆盖掉前一个。 - 导致只有最后一个
effect
能收到更新通知。
JavaScript
get value(){
if(activeSub){
this.subs = activeSub
}
return this._value
}
第一个 effect
加入
-
执行
console.log('effect1', 0)
-
收集依赖
effect(fn1)
,此时activeSub = fn1
,然后立即执行fn1()
。 -
fn1
读取count.value
→ 进入 getter:activeSub
存在 →this.subs = activeSub
(把subs
指向fn1
)。- 返回
0
,所以打印出effect1 0
。
-
effect(fn1)
结束,把activeSub
清空为undefined
。
第二个 effect
加入
-
执行
console.log('effect2', 0)
-
收集依赖
effect(fn2)
,此时activeSub = fn2
,并执行fn2()
。 -
fn2
读取count.value
→ 进入 getter:activeSub
存在 →this.subs = activeSub
覆盖掉fn1
,现在subs === fn2
。- 返回
0
,打印出effect2 0
。
-
effect(fn2)
结束,把activeSub
清空为undefined
。
一秒后更新触发
JavaScript
set value(newValue){
this._value = newValue
this.subs?.()
}
- 执行
count.value = 1
。 - 进入 setter:
this._value = 1
。 - 调用
this.subs?.()
→ 直接调用当前存在于subs
的函数fn2
。 - 因为只有
fn2
被调用,所以只打印出console.log('effect2', 1)
。
问题解决方案
接下来我们运用上次讲的双向链表,来处理订阅者被覆盖的问题:
TypeScript
// ref.ts
// 定义链表节点结构
interface Link {
// 保存 effect
sub: Function
// 下一个节点
nextSub: Link
// 上一个节点
prevSub: Link
}
class RefImpl {
_value;
[ReactiveFlags.IS_REF] = true
subs: Link // 订阅者链表的头节点
subsTail: Link // 订阅者链表的尾节点
constructor(value){
this._value = value
}
get value(){
if(activeSub){
// 创建节点
const newLink: Link = {
sub: activeSub,
nextSub: undefined,
prevSub: undefined
}
/**
* 关联链表关系
* 1. 如果存在尾节点,表示链表中已有节点,在链表尾部新增。
* 2. 如果不存在尾节点,表示这是第一次关联链表,第一个节点既是头节点也是尾节点。
*/
if(this.subsTail){
this.subsTail.nextSub = newLink
newLink.prevSub = this.subsTail
this.subsTail = newLink
} else {
this.subs = newLink
this.subsTail = newLink
}
}
return this._value
}
set value(newValue){
this._value = newValue
// 获取头节点
let link = this.subs
let queuedEffect = []
// 遍历整个链表的每一个节点
// 把每个节点里的 effect 函数放进数组
// 注意不是放入节点本身,而是放入节点里的 sub 属性(即 effect 函数)
while (link){
queuedEffect.push(link.sub)
link = link.nextSub
}
// 触发更新
queuedEffect.forEach(effect => effect())
}
}
解决后执行流程
初始化

- 初始化,在走到
effect
之前,头尾节点都是undefined
。
第一个 effect
加入

-
effect(fn1)
访问count
。 -
activeSub = effect1
,立即执行effect1()
。 -
effect1
读取count.value
→ 进入get
:activeSub
存在 → 创建newLink(effect1)
。- 因为当前
subsTail
为undefined
,所以把头节点和尾节点都指向newLink(effect1)
。
-
输出
effect1 0
。 -
清除
activeSub
:activeSub = undefined
。
第二个 effect
加入

-
effect(fn2)
访问count
。 -
activeSub = effect2
,执行effect2()
。 -
effect2
读取count.value
→ 触发getter
:-
activeSub
存在 → 创建newLink(effect2)
。 -
这次
subsTail
存在 (指向effect1
的节点),所以把newLink(effect2)
挂在尾端:effect1
节点的nextSub
指向effect2
节点。effect2
节点的prevSub
指向effect1
节点。subsTail
更新为effect2
节点。
-
-
输出
effect2 0
。 -
清除
activeSub
:activeSub = undefined
。
一秒后更新触发
-
执行
count.value = 1
。 -
触发
setter
,this._value = 1
。 -
从头节点 开始遍历链表,把每个节点的
sub
(也就是 effect 函数) 放入queuedEffect
数组:- 先推入
effect1
,再推入effect2
。
- 先推入
-
queuedEffect.forEach(fn => fn())
依次执行:- 先运行
effect1()
→ 打印effect1 1
。 - 再运行
effect2()
→ 打印effect2 1
。
- 先运行
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。