从零到一打造 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)
    • 因为当前 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」,一起跟日安当同学。

相关推荐
江拥羡橙4 小时前
Vue和React怎么选?全面比对
前端·vue.js·react.js
楼田莉子6 小时前
Qt开发学习——QtCreator深度介绍/程序运行/开发规范/对象树
开发语言·前端·c++·qt·学习
暮之沧蓝6 小时前
Vue总结
前端·javascript·vue.js
木易 士心6 小时前
Promise深度解析:前端异步编程的核心
前端·javascript
im_AMBER6 小时前
Web 开发 21
前端·学习
又是忙碌的一天6 小时前
前端学习day01
前端·学习·html
Joker Zxc6 小时前
【前端基础】20、CSS属性——transform、translate、transition
前端·css
excel6 小时前
深入解析 Vue 3 源码:computed 的底层实现原理
前端·javascript·vue.js
大前端helloworld7 小时前
前端梳理体系从常问问题去完善-框架篇(react生态)
前端
不会算法的小灰7 小时前
HTML简单入门—— 基础标签与路径解析
前端·算法·html