Vue3源码:track依赖追踪原理

Vue3源码:track依赖追踪原理

track(debugInfo?: DebuggerEventExtraInfo): Link | undefined { if (!activeSub || !shouldTrack || activeSub === this.computed) { return } ...

2024年12月13日
1.4千字
track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
  if (!activeSub || !shouldTrack || activeSub === this.computed) {
    return
  }

  let link = this.activeLink
  if (link === undefined || link.sub !== activeSub) {
    link = this.activeLink = new Link(activeSub, this)

    // add the link to the activeEffect as a dep (as tail)
    if (!activeSub.deps) {
      activeSub.deps = activeSub.depsTail = link
    } else {
      link.prevDep = activeSub.depsTail
      activeSub.depsTail!.nextDep = link
      activeSub.depsTail = link
    }

    addSub(link)
  } else if (link.version === -1) {
    // reused from last run - already a sub, just sync version
    link.version = this.version

    // If this dep has a next, it means it's not at the tail - move it to the
    // tail. This ensures the effect's dep list is in the order they are
    // accessed during evaluation.
    if (link.nextDep) {
      const next = link.nextDep
      next.prevDep = link.prevDep
      if (link.prevDep) {
        link.prevDep.nextDep = next
      }

      link.prevDep = activeSub.depsTail
      link.nextDep = undefined
      activeSub.depsTail!.nextDep = link
      activeSub.depsTail = link

      // this was the head - point to the new head
      if (activeSub.deps === link) {
        activeSub.deps = next
      }
    }
  }

  if (__DEV__ && activeSub.onTrack) {
    activeSub.onTrack(
      extend(
        {
          effect: activeSub,
        },
        debugInfo,
      ),
    )
  }

  return link
}
  • activeSub是一个effect文件中的全局变量,用来记录所有的副作用映射关系



eg

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">{{ count }}</button>
  <div id="count-2">{{ count }}</div>
  <div id="count-3">{{ count }}</div>
</template>

<style>
button {
  color: red;
}
</style>

这个例子中,count变量会触发依赖追踪三次。

这三次触发依赖追踪,都有什么样的区别

这三次触发依赖追踪本质上是相同的,但它们属于同一个渲染效果(render effect)中的不同访问点。让我详细解释一下:

对于这个模板:

<template>
  <button @click="count++">{{ count }}</button>
  <div id="count-2">{{ count }}</div>
  <div id="count-3">{{ count }}</div>
</template>

这三次 track 的执行顺序是从上到下的,它们的主要区别在于:

  1. 第一次 track(button 中的 count)
// 第一次 track 时
link = this.activeLink = new Link(activeSub, this)
// 此时 activeSub.deps 为空,所以这个 link 会成为 deps 链表的第一个节点
if (!activeSub.deps) {
  activeSub.deps = activeSub.depsTail = link
}
  1. 第二次 track(count-2 div 中的 count)
// 第二次 track 时
link = this.activeLink = new Link(activeSub, this)
// 此时 activeSub.deps 已存在,新的 link 会被添加到链表尾部
link.prevDep = activeSub.depsTail
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
  1. 第三次 track(count-3 div 中的 count)
// 第三次 track 时
// 与第二次类似,继续添加到链表尾部
link = this.activeLink = new Link(activeSub, this)
link.prevDep = activeSub.depsTail
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link

主要区别点:

  1. 依赖链表的位置不同
    • 第一个成为头节点(deps)
    • 第二个和第三个分别追加到尾部(depsTail)
  1. Link 对象的关联关系不同
    • 第一个 link 的 prevDep 为 undefined
    • 后续的 link 会与前一个 link 建立双向链接
  1. 在开发环境下的调试信息
    • 每次 track 时的调用栈位置不同
    • 对应的模板位置不同

但它们有以下共同点:

  1. 都属于同一个渲染效果(同一个 activeSub)
  2. 都追踪同一个响应式数据(count)
  3. 当 count 发生变化时,这三个位置都会触发更新

这种依赖追踪的设计使得:

  • Vue 能够精确知道哪些模板位置依赖了这个响应式数据
  • 当数据变化时能够精确地更新相关的 DOM 节点
  • 通过双向链表结构,可以高效地管理和清理依赖关系

Vue是如何知道那些模板位置依赖了这个响应式数据的?它是怎么讲两者联系起来的?

让我解释 Vue 是如何将模板位置和响应式数据关联起来的:

核心机制是通过"副作用(Effect)"作为桥梁。当 Vue 编译模板时,会为整个组件创建一个渲染函数,这个渲染函数会被包装在一个 Effect 中。

让我们通过一个简化的例子来说明:

<template>
  <button>{{ count }}</button>
</template>

Vue 会将这个模板编译成类似这样的渲染函数:

// 简化的渲染函数示意
function render() {
  return h('button', null, count.value)  // 访问 count.value 时会触发 track
}

// 创建渲染 effect
const effect = new ReactiveEffect(render, {
  scheduler: queueJob,
  scope: componentScope,
  // ...其他配置
})

关联过程如下:

  1. 创建依赖关系
// 当执行 render 函数时
count.value  // 触发 get 操作
↓
track(target, TrackOpTypes.GET, 'value')  // 进入 track 函数
↓
dep.track()  // dep 是与 count.value 关联的 Dep 实例
↓
// 在 Dep 类中创建 Link
link = new Link(activeSub, this)  // activeSub 就是当前的渲染 effect
  1. 双向链接
// Dep 和 Effect 之间建立双向链接
// Effect 侧
effect.deps = link        // effect 记录它依赖了哪些响应式数据
effect.depsTail = link

// Dep 侧
dep.subs = link          // dep 记录哪些 effect 依赖了它
  1. 更新触发
// 当 count.value 改变时
count.value = newValue
↓
trigger(target, TriggerOpTypes.SET, 'value')
↓
dep.notify()  // 通知所有依赖的 effects
↓
// 遍历所有 links,调用对应的 effect
for (let link = this.subs; link; link = link.prevSub) {
  link.sub.notify()  // sub 就是 effect
}

关键点:

  1. 位置追踪
  • 渲染函数本身就包含了模板位置信息
  • Effect 在执行时会建立与具体 DOM 节点的关联
  • 当数据变化触发重渲染时,会更新对应的 DOM 节点
  1. 依赖管理
class Link {
  version: number
  nextDep?: Link   // 用于 Effect 的依赖链表
  prevDep?: Link
  nextSub?: Link   // 用于 Dep 的订阅者链表
  prevSub?: Link
  
  constructor(
    public sub: Subscriber,  // Effect
    public dep: Dep         // 响应式数据的 dep
  ) {
    // ...
  }
}
  1. 精确更新
  • 每个响应式数据都知道哪些 Effect 依赖它(通过 dep.subs)
  • 每个 Effect 都知道它依赖哪些数据(通过 effect.deps)
  • 数据变化时,只会触发相关的 Effect 更新

这种机制确保了:

  • 响应式数据和模板位置之间的精确关联
  • 高效的依赖追踪和更新
  • 最小化不必要的重渲染









文章评论区

欢迎留言交流

未登录,请先注册或登录后发表评论。

Leave comment