游乐游手机版
首页/前端开发/文章详情

Vue2 Object变化侦测:从getter/setter到Dep/Watcher/Observer

时间:2026-07-09 06:58
Vue2通过Object defineProperty拦截属性读写,结合Observer、Dep和Watcher实现响应式:读取时收集依赖,修改时触发通知。Observer递归转换对象属性,defineReactive设置getter setter,Dep管理订阅列表,Watcher执行更新。该机制精准追踪数据变化并通知组件,再经虚拟DOMdiff完成视图更

许多前端开发者在初次接触 Vue 响应式原理时,常会被 ObserverDepWatcherdefineReactive 这几个概念绕晕。

归根结底,它们都指向同一个核心问题:当数据发生变化时,Vue 如何精确地知道哪些部分依赖了该数据,并及时通知它们完成更新。这正是 Vue 2 中 Object 变化侦测的关键所在。

Vue 2 中 Object 的变化侦测:从 getter/setter 到 Dep、Watcher、Observer

当一个对象属性发生变化时,Vue 是如何识别出谁在使用它,并精准触发相应更新的?

1. 什么是变化侦测?

Vue 的渲染机制是声明式的。模板中写一个 h1 标签并利用 {{ name }} 进行动态插值,Vue 便会自动根据 name 的值生成对应的 DOM。然而,当 name 改变后,Vue 凭什么判断页面需要重新渲染?这正是变化侦测所要解答的问题。

变化侦测大体可分为两种思路。一种是“拉”模式:状态变化后,框架只知道“可能有内容变了”,于是重新执行计算并通过对比来找出实际需要更新的部分,React 的虚拟 DOM diff 就属于这一类。另一种是“推”模式:状态变更的瞬间,主动通知所有依赖该状态的地方。Vue 2 的响应式系统采用的就是后者。

Vue 的优势在于:它并不等到最后才去猜测哪个地方需要更新,而是在数据被读取时就记录下“谁使用了我”,待数据被修改时直接通知这些使用者。一句话概括:

数据读取时收集依赖,数据修改时触发依赖。

2. Vue 2 如何追踪 Object 的变化?

在 Vue 2 中,对象属性的变化主要依赖 Object.defineProperty 实现。它可以将一个普通属性转换为带有 getter 和 setter 的属性:

function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
    }
  })
}

这样一来,读取 data[key] 时会触发 get 方法,修改时则触发 set 方法。但这还不够——仅仅知道属性被读或被改,仍无法完成真正的响应式。真正的难点在于:读取时需要明确“谁”在读,修改时需要通知这些读取者执行更新。这就引出了 Vue 响应式系统中的第一个核心角色:Dep

3. Dep:每个属性专属的“订阅管理器”

你可以把 Dep 理解为一个订阅中心。每一个响应式属性都拥有一个属于自己的 Dep 实例,当属性被读取时就将读取者收集进来,当属性变化时再通知所有订阅者。简化版的代码如下:

class Dep {
  constructor() {
    this.subs = []
  }
  depend() {
    if (Dep.target) {
      this.subs.push(Dep.target)
    }
  }
  notify() {
    this.subs.forEach(sub => sub.update())
  }
}

接着,我们把它放回 defineReactive 中:

function defineReactive(data, key, val) {
  const dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      dep.depend()
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      dep.notify()
    }
  })
}

逻辑瞬间变得清晰:

get  -> dep.depend()  -> 收集依赖
set  -> dep.notify()  -> 通知依赖

但下一个问题随之而来:Dep.target 到底是什么?答案就是:Watcher

4. Watcher:真正需要被通知的执行者

Vue 不会直接将 DOM、模板或用户回调本身塞进 Dep,而是统一抽象为一个对象:Watcher。你可以把 Watcher 理解为“响应式系统中的订阅者”。例如:

vm.$watch('user.name', function(newVal, oldVal) {
  console.log(newVal, oldVal)
})

底层会创建一个 Watcher 实例。它的职责非常明确:读取 user.name → 触发对应的 getter → 在 getter 中把自己收集到 Dep 中 → 当数据发生变更时执行自身的 update 方法 → 最终调用用户注册的回调函数。

简化后的 Watcher 实现可以这样理解:

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.getter = parsePath(expOrFn)
    this.cb = cb
    this.value = this.get()
  }
  get() {
    Dep.target = this
    const value = this.getter(this.vm)
    Dep.target = null
    return value
  }
  update() {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

这里最核心的技巧在于 get() 方法。它先将当前 Watcher 设置为全局目标:Dep.target = this,然后再执行数据的读取。读取数据时会触发 getter,而 getter 里会调用 dep.depend(),于是这个 Watcher 就被成功纳入对应属性的 Dep 中。

这就是 Vue 2 依赖收集的精髓:Watcher 先把自己挂载到全局目标上,再主动读取数据。数据的 getter 被触发后,就能反向将这个 Watcher 收集起来。

5. parsePath:将字符串路径转换为读取函数

当我们编写 vm.$watch('a.b.c', callback) 时,Vue 需要根据字符串 'a.b.c' 获取到实际的值。简化实现如下:

function parsePath(path) {
  const segments = path.split('.')
  return function(obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

实现逻辑非常简单:将 'a.b.c' 按点号分割成 ['a','b','c'],然后逐层读取属性。需要注意的是,这个读取过程会依次触发每一层属性的 getter,因此 Watcher 不仅订阅了最终的 c 属性,它在读取链路中经过的每一层属性都有可能参与依赖收集。

6. Observer:将整个对象转换为响应式对象

前面介绍的 defineReactive 只能处理单个属性,但 Vue 的 data 通常是一个完整的嵌套对象:

data() {
  return {
    user: {
      name: 'Tom',
      age: 18
    }
  }
}

Vue 需要递归地将每个属性都转化为响应式属性,这正是 Observer 的职责所在:

class Observer {
  constructor(value) {
    this.value = value
    if (!Array.isArray(value)) {
      this.walk(value)
    }
  }
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}

与此同时,defineReactive 内部也需要递归处理子对象:

function defineReactive(data, key, val) {
  if (typeof val === 'object' && val !== null) {
    new Observer(val)
  }
  const dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      dep.depend()
      return val
    },
    set(newVal) {
      if (newVal === val) return
      if (typeof newVal === 'object' && newVal !== null) {
        new Observer(newVal)
      }
      val = newVal
      dep.notify()
    }
  })
}

一个普通对象经过 Observer 处理后,内部的每一个属性都会被转换为 getter/setter。可以这样理解它们各自的分工:

  • Observer:负责将对象加工成响应式对象;
  • defineReactive:负责将单个属性转换为 getter/setter;
  • Dep:负责保存该属性的依赖列表;
  • Watcher:负责在数据变更后执行相应的更新逻辑。

7. 整体流程串接

假设模板中使用了

{{ user.name }}

,Vue 会为该组件创建一个渲染 Watcher

首次渲染时,流程如下:

Watcher 开始渲染
   ↓
读取 user.name
   ↓
触发 user 和 name 的 getter
   ↓
getter 调用 dep.depend()
   ↓
当前 Watcher 被收集进 Dep

之后当我们执行 this.user.name = 'Jerry' 时,流程变为:

触发 name 的 setter
   ↓
setter 调用 dep.notify()
   ↓
Dep 通知所有 Watcher
   ↓
Watcher.update()
   ↓
组件重新渲染
   ↓
生成新的虚拟 DOM
   ↓
diff 后更新真实 DOM

这就是 Vue 2 对象响应式的主链路。

8. 为什么 Vue 2 检测不到新增属性和删除属性?

Object.defineProperty 有一个天然限制:它只能拦截已经存在的属性。比如初始化时 data 返回 { user: {} },之后执行 this.user.name = 'Tom',这个 name 是后来才添加的,初始化阶段 Vue 并没有见过它,因此也就没有机会为其设置 getter/setter。所以 Vue 2 无法自动侦测新增属性。

删除属性也面临同样的问题:delete this.user.name 不会触发任何已定义的 setter,因此 Vue 2 同样无法直接感知。这就是为什么 Vue 2 需要提供 Vue.set()Vue.delete() 或实例方法 this.$set()this.$delete()。它们的本质就是绕过 Object.defineProperty 的限制,手动补充响应式处理与依赖通知。

9. Vue 2 为什么还要引入虚拟 DOM?

既然 Vue 已经能够精确知道哪个数据发生了变化,那为什么 Vue 2 仍然要使用虚拟 DOM?原因在于,粒度太细也会带来成本。如果每个数据都直接绑定到具体的 DOM 节点,依赖关系会变得非常庞大,内存占用与维护开销都会显著上升。所以 Vue 2 选择了一个折中方案:数据变化时通知组件,组件内部再通过虚拟 DOM diff 找出真正需要更新的 DOM。

也就是说,Vue 2 并没有完全放弃“推”的模式,而是将更新粒度从“具体 DOM 节点”提升到了“组件级别”。这样一来,既保留了响应式依赖追踪的优势,又避免了过细粒度带来的巨大依赖开销。

10. 总结

Vue 2 中 Object 的变化侦测,可以浓缩为一句话:利用 Object.defineProperty 拦截属性的读写操作;在读取时收集 Watcher,在修改时通知 Watcher。

几个核心角色的职责分别是:

Observer
负责遍历对象,将属性转换为 getter/setter。
defineReactive
负责处理单个属性,使其具备响应式能力。
Dep
负责保存依赖,相当于每个属性自己的订阅列表。
Watcher
是真正的订阅者。它读取数据时被收集,数据变化时被通知。

完整流程为:

初始化 data
   ↓
Observer 遍历对象
   ↓
defineReactive 转换属性
   ↓
Watcher 读取数据
   ↓
getter 收集依赖
   ↓
数据被修改
   ↓
setter 触发通知
   ↓
Dep 通知 Watcher
   ↓
Watcher 执行更新

这就是 Vue 2 响应式系统的基本骨架。理解这条链路后,再去查阅 Vue 源码中的 ObserverDepWatcher,就不会觉得它们是几个孤立的类,而是一套完整的订阅发布系统:数据负责暴露变化,Dep 负责管理依赖,Watcher 负责响应变化,视图更新只是 Watcher 被触发后的最终结果。

来源:https://juejin.cn/post/7656379123401539634
上一篇AI重塑应用开发:Vue与Vite给出新答案 下一篇腾讯云TRTC UniApp SDK从入门到上线指南
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
如何用拖放在Flask看板应用中更新数据库状态
前端开发 · 2026-07-09

如何用拖放在Flask看板应用中更新数据库状态

在Flask看板应用中,通过HTML5拖放API结合前端fetch异步POST请求,将拖拽任务的task_id和target_state发送至后端路由;后端接收JSON后更新数据库并提交事务,实现跨列拖拽时实时更新状态。需注意将事件绑定到容器而非子元素,并在ondragover中阻止默认行为以确保drop触发。

如何用CSS :has()选择器快速实现悬停span显示相邻div
前端开发 · 2026-07-09

如何用CSS :has()选择器快速实现悬停span显示相邻div

利用CSS的:has()伪类选择器,通过` bar:has(> baz:hover)+ qux`实现悬停内联元素时显示相邻块级容器,解决了传统相邻兄弟选择器无法跨层级匹配的局限,可用于悬停图标显示详情框等交互场景。需注意浏览器兼容性和性能优化,避免过度使用影响渲染效率。

display:block居中段落文本的响应式实现方法
前端开发 · 2026-07-09

display:block居中段落文本的响应式实现方法

通过`display:block`与`max-width`限制宽度,配合`margin:auto`使段落块水平居中并实现响应式布局;同时用CSS统一控制段落间距,避免脱离文档流,提升可读性与维护性。

JavaScript代码去重:高效避免重复编写的技巧
前端开发 · 2026-07-09

JavaScript代码去重:高效避免重复编写的技巧

将重复代码中的变化点提取为参数,利用ES6模板字符串动态生成选择器,可消除重复、保持一致性。注意参数安全性,必要时进行白名单校验;批量化场景可扩展为数组形式;现代框架中更推荐数据驱动或组件化封装替代手动选择器。

使用容器查询单位cqw实现表单输入框相对于祖父容器百分比宽度
前端开发 · 2026-07-09

使用容器查询单位cqw实现表单输入框相对于祖父容器百分比宽度

利用CSS容器查询单位cqw,表单输入框宽度可直接基于祖父容器计算,绕过中间包装器尺寸干扰,实现简洁响应式布局,无需硬编码或JavaScript,显著降低维护成本,提升嵌套结构下的布局灵活性。