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

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

相关推荐
福兮说6 小时前
前端下载大文件,点了按钮半天没反应:fetch + blob 的三个问题和三种替代写法
前端·javascript·性能优化·文件下载
GreenTea6 小时前
我把 Agent 的 while 循环拆掉了:一种你可能没想到的 Agent 架构
前端·后端·架构
科技观察哨6 小时前
六足平台选型与纳米定位系统集成:HEB-640六自由度位移台在半导体光刻对准中的参数边界与国产替代评估
前端·人工智能
niucloud-admin6 小时前
JAVA V6 多商户商城 开发文档——web前端
前端
颜进强7 小时前
16 · NestJS LifecycleEvents 生命周期事件:五个钩子、三条边界,和 rag-server 的优雅停机
前端·后端·ai编程
今年下半年8 小时前
【VUE】整合腾讯地图、自定义区域边界、村委名称及资产统计(放大显示资产点位)
前端·javascript·vue.js
郑州光合科技余经理8 小时前
同城电商系统:库存变更怎么同步到订单
java·开发语言·前端·后端·uni-app·php·ai编程
前端snow10 小时前
ai agent --- DeepAgents 中间件
前端
data analyse 45611 小时前
报表能自定义的国内统计工具有哪些
前端·数据分析