Published on

Vue3响应式系统深度解析

Authors
  • Name
    Twitter

Vue 3的响应式系统可以概括为以下几个关键步骤:

  1. 创建响应式数据
  2. 依赖收集
  3. 设置副作用
  4. 触发更新
  5. 调度更新
  6. 组件更新
  7. 界面更新

让我们详细探讨每个步骤的实现。

1. 创建响应式数据

Vue 3使用reactiveref两个函数来创建响应式数据。

function reactive(target) {
  // 使用 Proxy 创建响应式对象
  return new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      track(target, key)  // 进行依赖收集
      return result
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) {
        trigger(target, key)  // 触发更新
      }
      return result
    }
  })
}

function ref(value) {
  // 创建一个包含 value 属性的对象
  const refObject = {
    get value() {
      track(refObject, 'value')  // 进行依赖收集
      return value
    },
    set value(newValue) {
      if (value !== newValue) {
        value = newValue
        trigger(refObject, 'value')  // 触发更新
      }
    }
  }
  return refObject
}

2. 依赖收集

当访问响应式数据时,系统会自动收集依赖:

let activeEffect  // 当前激活的副作用函数
const targetMap = new WeakMap()  // 存储所有的依赖关系

function track(target, key) {
  if (activeEffect) {
    // 获取当前对象的依赖图
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    // 获取特定 key 的依赖集合
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    // 将当前副作用函数添加到依赖集合中
    dep.add(activeEffect)
  }
}

3. 设置副作用

effect函数用于设置副作用,比如组件的渲染函数:

function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn  // 设置当前活跃的副作用函数
    fn()  // 执行副作用函数,这将触发 getter,从而收集依赖
    activeEffect = null  // 清除当前活跃的副作用函数
  }
  effectFn()  // 立即执行一次以收集初始依赖
  return effectFn
}

副作用的设置和使用主要发生在以下四个场景:

  1. 组件渲染

    • 相关 API: setup(), <script setup>
    • 描述: 组件的渲染函数被自动包装在一个 effect 中
  2. 计算属性

    • 相关 API: computed()
    • 描述: 计算属性内部使用 effect 来追踪依赖并缓存结果
  3. 侦听器

    • 相关 API: watch(), watchEffect()
    • 描述: 用于观察响应式数据变化并执行回调
  4. 自定义副作用

    • 相关 API: 直接使用 effect() (通常在底层库中使用)
    • 描述: 用于创建自定义的响应式行为

当这些副作用函数执行时,它们会访问响应式数据,从而触发 track 函数进行依赖收集。这就建立了数据和副作用之间的联系。之后,当响应式数据变化时,相关的副作用函数就会被重新执行。

4. 触发更新

当响应式数据变化时,会触发相关的副作用:

function trigger(target, key) {
  // 获取目标对象的依赖图
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  // 获取特定 key 的依赖集合
  const dep = depsMap.get(key)
  if (dep) {
    // 遍历执行所有的副作用函数
    dep.forEach(effectFn => {
      if (effectFn.scheduler) {
        // 如果有调度器,则使用调度器来执行
        effectFn.scheduler()
      } else {
        // 否则直接执行
        effectFn()
      }
    })
  }
}

5. 调度更新

Vue 3使用异步队列来批量处理更新,提高性能:

const queue = new Set()  // 使用 Set 来去重任务
let isFlushing = false
const p = Promise.resolve()  // 创建一个 Promise 以使用微任务

function queueJob(job) {
  queue.add(job)
  if (!isFlushing) {
    // 如果还没有刷新队列,则安排一次刷新
    isFlushing = true
    p.then(flushJobs)
  }
}

function flushJobs() {
  // 执行队列中的所有任务
  for (const job of queue) {
    job()
  }
  queue.clear()  // 清空队列
  isFlushing = false  // 重置刷新标志
}

6. 组件更新

组件的更新函数被包装在effect中:

const componentUpdateFn = () => {
  const vnode = render()  // 生成新的虚拟 DOM
  patch(prevVNode, vnode)  // 对比新旧虚拟 DOM 并更新实际 DOM
}

effect(() => {
  componentUpdateFn()
}, {
  scheduler: queueJob  // 使用 queueJob 作为调度器,实现异步更新
})

7. 界面更新

最后,通过虚拟DOM的diff算法,高效地更新实际DOM:

function patch(oldVNode, newVNode) {
  if (oldVNode.tag !== newVNode.tag) {
    // 如果节点类型变了,直接替换整个节点
    oldVNode.el.parentNode.replaceChild(createElement(newVNode), oldVNode.el)
  } else {
    const el = newVNode.el = oldVNode.el
    // 更新节点的属性
    updateProperties(el, oldVNode.props, newVNode.props)
    // 更新子节点
    updateChildren(el, oldVNode.children, newVNode.children)
  }
}

总结

Vue 3的响应式系统通过精心设计的流程,实现了高效的数据-视图同步:

  1. 使用Proxy或包装对象创建响应式数据。
  2. 通过依赖收集建立数据与副作用之间的关联。
  3. 利用effect函数包装副作用,实现自动依赖追踪。
  4. 在数据变化时触发相关副作用。
  5. 使用异步队列批量处理更新,优化性能。
  6. 组件更新函数被包装为响应式效果,自动响应数据变化。
  7. 通过虚拟DOM和diff算法高效更新实际DOM。