Vue 的响应式系统是其最核心的特性之一,它让我们通过简单地声明数据与视图的关系,而不需要手动操作 DOM。
本文我通过对 Vue2 的源码的学习和理解来串一下整体设计。
响应式系统的基本原理
Vue2 的响应式系统基于 JS 的 Object.defineProperty API 实现。
他的系统设计目标很简单:当数据变化时,自动更新与之相关的视图。
核心模块
Vue 的响应式系统主要由三个核心部分组成:
Observer(观察者):负责将普通 JavaScript 对象转换为响应式对象
Dep(依赖收集器):用于收集和管理依赖
Watcher(监听器):代表一个依赖,当数据变化时被通知
源码解析
1. Observer - 数据的"间谍"
Observer 类位于 src/core/observer/index.js,它的主要工作是将普通对象的属性转换为 getter/setter:
// 简化版 Observer 类
export class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
// 给对象添加 __ob__ 属性,指向 Observer 实例
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 数组的特殊处理...
} else {
// 对象的处理:遍历对象的每个属性,转换为 getter/setter
this.walk(value)
}
}
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
2.defineReactive - 响应式转换的核心
defineReactive 函数是实现响应式的核心,它使用 Object.defineProperty 重新定义对象的属性:
// 简化版 defineReactive
export function defineReactive(obj, key, val) {
// 每个属性都有自己的依赖收集器
const dep = new Dep()
// 获取属性的当前值
val = val || obj[key]
// 如果值是对象,递归使其响应式
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// getter: 读取属性时收集依赖
get: function reactiveGetter() {
// 如果有当前正在计算的 watcher,就收集它作为依赖
if (Dep.target) {
dep.depend() // 收集依赖
if (childOb) {
childOb.dep.depend() // 对象本身也作为依赖
}
}
return val
},
// setter: 修改属性时通知依赖更新
set: function reactiveSetter(newVal) {
if (newVal === val) return
val = newVal
// 如果新值是对象,也要使其响应式
childOb = observe(newVal)
// 通知所有依赖进行更新
dep.notify()
}
})
}
这段代码就像给每个属性安装了"感应器":当你读取属性时,Vue 会记录谁在使用这个属性;当你修改属性时,Vue 会通知所有用到这个属性的地方进行更新。
3. Dep - 依赖收集器
Dep 类位于 src/core/observer/dep.js,它的职责是收集和管理依赖,以及在数据变化时通知这些依赖:
// 简化版 Dep
export default class Dep {
constructor() {
this.subs = [] // 存储依赖的数组
}
// 添加依赖
addSub(sub) {
this.subs.push(sub)
}
// 移除依赖
removeSub(sub) {
const index = this.subs.indexOf(sub)
if (index > -1) {
this.subs.splice(index, 1)
}
}
// 收集当前 watcher 作为依赖
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知所有依赖更新
notify() {
// 复制一份依赖数组,防止更新过程中数组变化
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 通知每个 watcher 更新
}
}
}
// 全局唯一的当前正在计算的 watcher
Dep.target = null
Dep 就像一个通讯录,记录了谁对某个数据感兴趣,当数据变化时,它会逐个通知这些"订阅者"。
4. Watcher - 变化的观察者
Watcher 类位于 src/core/observer/watcher.js,它代表一个依赖,可以是组件的渲染函数、计算属性或用户使用 $watch 创建的观察者:
// 简化版 Watcher
export default class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm
// 表达式或函数,用于获取监听的值
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) // 解析表达式
}
this.cb = cb // 值变化时的回调函数
this.deps = [] // 记录这个 watcher 依赖了哪些 dep
this.depIds = new Set()
// 立即执行一次 getter,收集依赖
this.value = this.get()
}
// 获取值并收集依赖
get() {
// 设置当前正在计算的 watcher
Dep.target = this
let value
try {
// 执行 getter,这会触发属性的 getter,从而收集依赖
value = this.getter.call(this.vm, this.vm)
} finally {
// 清除当前 watcher
Dep.target = null
}
return value
}
// 添加依赖
addDep(dep) {
const id = dep.id
if (!this.depIds.has(id)) {
this.depIds.add(id)
this.deps.push(dep)
dep.addSub(this)
}
}
// 更新
update() {
// 异步更新队列
queueWatcher(this)
}
// 实际执行更新的方法
run() {
const oldValue = this.value
const newValue = this.get()
if (newValue !== oldValue) {
// 调用回调函数
this.cb.call(this.vm, newValue, oldValue)
}
}
}
Watcher 就像是一个"侦探",负责监视数据的变化,并在变化时执行特定的操作,如更新视图。
响应式系统的工作流程
现在用一个简单的例子来说下整个流程:
new Vue({
el: '#app',
data: {
message: 'Hello'
}
})
1.初始化阶段:
Vue 实例化时,对 data 中的 message 属性调用 defineReactive
这会为 message 创建一个 Dep 实例,并设置 getter/setter
2.依赖收集阶段:
当渲染模板
<div>{{ message }}</div>
时,会创建一个渲染 Watcher在渲染过程中,会读取 message 属性,触发其 getter
getter 中发现有当前 Watcher (Dep.target),于是调用 dep.depend() 收集依赖
这样,message 的 Dep 就收集了渲染 Watcher 作为依赖
3.更新阶段:
当我们执行 vm.message = 'Hi' 时,触发 message 的 setter
setter 中调用 dep.notify() 通知依赖更新
Dep 通知所有收集的 Watcher 执行 update 方法
渲染 Watcher 重新渲染组件,更新视图
响应式系统的巧妙之处
Vue 响应式系统的巧妙之处在于: 自动依赖收集:不需要手动声明依赖关系,Vue 会在数据被访问时自动建立依赖关系 精确更新:只有真正依赖某个数据的视图才会在该数据变化时更新,而不是整个应用重新渲染 深度响应:对象的嵌套属性也能被监听,因为 Observer 会递归地将嵌套对象也变成响应式的
响应式系统的局限性
Vue 2 响应式系统也有一些局限: 不能检测对象属性的添加或删除:这是因为 Object.defineProperty 只能劫持已经存在的属性 数组变化的部分限制:Vue 重写了数组的变异方法(如 push、pop 等),但不能检测通过索引设置数组项或修改数组长度的变化 这些限制在 Vue 3 中通过使用 ES6 的 Proxy 代替 Object.defineProperty 得到了解决。