从零到一打造 Vue3 响应式系统 Day 4 - 核心概念:收集依赖、触发更新

前言

JavaScript 复制代码
const count = ref(0)

effect(() => {
  console.log('count.value ==>', count.value);
})

setTimeout(() => {
  count.value++
}, 1000)

昨天我们的目标是让一段简单的 refeffect 代码能够自动响应。

  1. 进入页面输出 count.value ==> 0
  2. 一秒后自动输出 count.value ==> 1

然而,我们初次实现时遇到了问题:无法正确取值 (undefined),也无法在值变更后触发更新。

为了解决这个问题,我们要思考 ref 需要做些什么:

  1. 当获取值时,ref 要怎么知道是谁在读取它?
  2. 当触发更新后,ref 又要怎么知道该通知谁?

让 Ref 知道谁在读取

TypeScript 复制代码
// 原始代码
class RefImpl {
  _value;
  constructor(value){
    this._value = value
  }
}

现在要加入 getter 和 setter,让 count.value 能正常运作:

TypeScript 复制代码
class RefImpl {
  _value;
  
  constructor(value){
    this._value = value
  }
  
  // 新增 getter:读取 value 时触发
  get value(){
    console.log('有人读取了 value!')
    return this._value
  }
  
  // 新增 setter:设置 value 时触发
  set value(newValue){
    console.log('有人修改了 value!')
    this._value = newValue
  }
}

现在 count.value 看起来可以正常返回值了,但此时它还是不知道是谁在读取、需要通知谁。

Effect 函数

TypeScript 复制代码
export function effect(fn){
  fn()
}

这时候我们需要一个地方来存储当前正在执行的 effect 函数。

TypeScript 复制代码
// effect.ts
// 用于保存当前正在执行的 effect 函数
export let activeSub;

export function effect(fn){
  activeSub = fn
  activeSub()
  activeSub = undefined
}

这个新版的 effect 函数做了三件事:

  1. 注册副作用: 在执行传入的函数 fn 之前,先将它赋值给全局变量 activeSub
  2. 执行副作用: 立即执行 fn()。如果在执行过程中读取了某个 ref.value,这个 ref 就能通过 activeSub 知道是谁在读取它。
  3. 清除副作用: 执行完毕后,必须将 activeSub 清空 (设为 undefined)。这一点非常重要,它能确保只有在 effect 的执行期间,读取 ref 的行为才会被视为依赖收集。

收集依赖实现

现在我们要让 ref 能够:

  1. 在被读取时,记录是谁在读取(依赖收集)
  2. 在被修改时,通知所有读取者(触发更新)

我们可以在 getter 读取值的时候,判断 activeSub 是否存在,来确认当前情况是否需要收集依赖。

TypeScript 复制代码
// ref.ts
import { activeSub } from './effect'

class RefImpl {
  _value;
  subs;  // 新增:用于存储订阅者
  
  constructor(value){
    this._value = value
  }
  
  // 新增 getter:读取 value 时触发
  get value(){
    // 依赖收集:如果存在 activeSub,就记录下来
    if(activeSub){
      this.subs = activeSub
    }
    return this._value
  }
  
  // 新增 setter:设置 value 时触发
  set value(newValue){
    // 触发更新:如果存在订阅者,就执行它
    if(this.subs){
      this.subs()  // 重新执行 effect
    } // 可简写为 this.subs?.()
  }
}

为了方便在后续的系统中判断一个变量是否为 ref 对象,我们可以新增一个辅助函数 isRef 和一个内部标记:

TypeScript 复制代码
enum ReactiveFlags {
  IS_REF = '__v_isRef'
}

class RefImpl {
  _value;
  subs;  // 新增:用于存储订阅者
  [ReactiveFlags.IS_REF] = true
  
  ...
}

export function isRef(value){
  return !!(value && value[ReactiveFlags.IS_REF])
}

现在,让我们将所有部分串联起来,完整地模拟一遍执行流程。


完整执行流程

页面初始化与依赖收集

刚开始进入页面。

JavaScript 复制代码
import { ref, effect } from '../dist/reactivity.esm.js'

const count = ref(0)

程序执行:const count = ref(0)

  • 执行 ref(0),创建一个 RefImpl 实例。

  • 此时 count 实例的内部状态为:

    • _value: 0
    • 没有任何订阅者:subs: undefined
    • 带有一个内部标记:__v_isRef: true

调用 effect 函数,并传入匿名函数 fn 作为参数。

JavaScript 复制代码
effect(() => {
  console.log('effect', count.value)
})

进入 effect 函数内部

TypeScript 复制代码
export let activeSub;

export function effect(fn){
  activeSub = fn
  activeSub()
  activeSub = undefined
}
  1. 设置 activeSub activeSub 被赋值为 fn,即 activeSub = fn

  2. 立即执行 fn()

    1. 执行 console.log('effect', count.value)

    2. 这触发了 count 实例的 get value()

    3. 进入 getter 内部:

      • if(activeSub) 条件成立,因为 activeSub 正是我们的 fn

        JavaScript

        ini 复制代码
        if(activeSub){
           this.subs = activeSub
        }
    4. 执行"依赖收集"this.subs = activeSub

    5. 现在 count 实例通过 subs 属性,记住了是 fn 在依赖它。

    6. getter 返回 this._value (也就是 0)。

    7. console.log 输出:effect 0

  3. activeSub = undefined (执行完毕后清空,表示当前没有正在执行的 effect)。

此时:

  1. count.subs 就是传入 effect 的那个函数。
  2. 依赖关系建立:counteffect(fn)

一秒之后

  • set value(newValue) 被调用,this._value = 1

  • this.subs?.() 被执行,即:如果存在订阅者,就调用它(这里就是前面存起来的 effect 函数)。

  • 触发更新: effect 函数再次执行。

    • console.log('effect', count.value) → 再次读取 getter。
    • 此时 activeSubundefined,所以不会重复收集依赖。
    • 这次是直接执行 effect 函数的本体,而不是 再次经过 effect(fn) 的包装流程,所以第二次之后执行 effectactiveSubundefined
    • console.log 输出:effect 1

这样,我们就完成了响应式依赖收集的最小可行版本。


完整代码

ref.ts

TypeScript 复制代码
import { activeSub } from './effect'

enum ReactiveFlags {
  IS_REF = '__v_isRef'
}

class RefImpl {
  _value; // 保存实际值
   // ref 标记,证明这是一个 ref 对象
  [ReactiveFlags.IS_REF] = true

  subs
  constructor(value){
    this._value = value
  }

  // 收集依赖
  get value(){ 
    // 当有人访问时,可以获取 activeSub
    if(activeSub){
      // 当存在 activeSub 时存储它,以便更新后触发
      this.subs = activeSub
    }
    return this._value
  }

  // 触发更新
  set value(newValue){ 
    this._value = newValue
    // 通知 effect 重新执行,获取最新的 value
    this.subs?.()
  }
}

export function ref(value){
  return new RefImpl(value)
}

export function isRef(value){
  return !!(value && value[ReactiveFlags.IS_REF])
}

effect.ts

TypeScript 复制代码
// 用于保存当前正在执行的 effect 函数
export let activeSub;

export function effect(fn){
  activeSub = fn
  activeSub()
  activeSub = undefined
}

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

相关推荐
跟橙姐学代码2 小时前
不要再用 print() 了!Python logging 库才是调试的终极武器
前端·python
ze_juejin2 小时前
JavaScript 中预防 XSS(跨站脚本攻击)
前端
我是天龙_绍2 小时前
🐴 记住了,节流(throttle)与防抖(debounce)
前端
凡二人2 小时前
Flip-js 优雅的处理元素结构变化的动画(解读)
前端·typescript
争当第一摸鱼前端2 小时前
Electron中的下载操作
前端
sjin2 小时前
React 源码 - Commit Phase 的工作细节
前端
FisherYu2 小时前
AI环境搭建pytorch+yolo8搭建
前端·计算机视觉
学前端搞口饭吃2 小时前
react reducx的使用
前端·react.js·前端框架
aidingni8883 小时前
掌握 JavaScript 中的 Map 和 Set
前端·javascript