
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」,一起跟日安当同学。