从零到一打造 Vue3 响应式系统 Day 6 - 响应式核心:链表实装应用

首页 编程分享 JQUERY丨JS丨VUE 正文

我是日安 转载 编程分享 2025-09-16 20:02:41

简介 昨天,我们了解了链表的核心概念,现在要把这些概念结合起来。 首先让我们从一个常见的场景开始:当一个响应式数据 (ref) 同时被多个 effect 依赖时,会发生什么? 我们预期它会输出如下: 但实际


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 依赖时,会发生什么?

我们预期它会输出如下:

console.log('effect1', 0)
console.log('effect2', 0)
// 1秒后
console.log('effect1', 1)
console.log('effect2', 1)

但实际上我们得到的是:

console.log('effect1', 0)
console.log('effect2', 0)
// 1秒后
console.log('effect2', 1)

发生什么事?

结果很明显:我们上次的 ref 实现,只能让 this.subs 属性一次记住一个订阅者,导致后来的 effect 覆盖了前面的。这会造成以下问题:

  • 每次有新的 effect 订阅时,会覆盖掉前一个。
  • 导致只有最后一个 effect 能收到更新通知。
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

一秒后更新触发

set value(newValue){ 
    this._value = newValue
    this.subs?.()
  }
  • 执行 count.value = 1
  • 进入 setter:this._value = 1
  • 调用 this.subs?.()直接调用当前存在于 subs 的函数 fn2
  • 因为只有 fn2 被调用,所以只打印出 console.log('effect2', 1)

问题解决方案

接下来我们运用上次讲的双向链表,来处理订阅者被覆盖的问题:

// 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)
    • 因为当前 subsTailundefined,所以把头节点和尾节点都指向 newLink(effect1)
  • 输出 effect1 0

  • 清除 activeSubactiveSub = 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

  • 清除 activeSubactiveSub = undefined

一秒后更新触发

  • 执行 count.value = 1

  • 触发 setterthis._value = 1

  • 头节点开始遍历链表,把每个节点的 sub (也就是 effect 函数) 放入 queuedEffect 数组:

    • 先推入 effect1,再推入 effect2
  • queuedEffect.forEach(fn => fn()) 依次执行:

    • 先运行 effect1() → 打印 effect1 1
    • 再运行 effect2() → 打印 effect2 1

想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。

转载链接:https://juejin.cn/post/7549468811348606995


Tags:


本篇评论 —— 揽流光,涤眉霜,清露烈酒一口话苍茫。


    声明:参照站内规则,不文明言论将会删除,谢谢合作。


      最新评论




ABOUT ME

Blogger:袅袅牧童 | Arkin

Ido:PHP攻城狮

WeChat:nnmutong

Email:nnmutong@icloud.com

标签云