Vue源码解析_数据响应式原理

什么是数据响应式

数据响应式就是:当数据发生变化时,使用这个数据的地方会自动更新

对比示例

1
2
3
4
5
6
7
8
9
10
// 普通JavaScript - 不会自动更新
let message = 'Hello'
message = 'World' // 页面不会自动更新

// Vue响应式 - 会自动更新
const vm = new Vue({
data: { message: 'Hello' },
template: '<div>{{ message }}</div>'
})
vm.message = 'World' // 页面自动更新

核心思想

  1. 数据驱动:数据变化,视图跟着变
  2. 自动追踪:Vue自动知道哪些地方用了这个数据
  3. 批量更新:多次数据变化合并成一次DOM更新

基本流程

  1. 数据劫持:拦截对数据的访问和修改
  2. 依赖收集:记录哪些地方使用了这个数据
  3. 派发更新:数据变化时通知所有依赖的地方
  4. 视图更新:重新渲染相关的DOM

Vue2.0的响应式原理

Vue2.0使用Object.defineProperty来实现数据响应式,这是ES5的特性,兼容性较好。

Object的变化侦测

基本原理

Vue2.0通过Object.defineProperty为对象的每个属性定义getter和setter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function defineReactive(obj, key, val) {
const dep = new Dep()

Object.defineProperty(obj, key, {
get() {
dep.depend() // 收集依赖
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify() // 派发更新
}
})
}

依赖收集和派发更新

依赖收集:当访问数据时,getter被触发,收集当前的Watcher
派发更新:当数据变化时,setter被触发,通知所有依赖的Watcher

Watcher如何将自己添加到依赖管理器?

当创建Watcher实例时,依赖收集的完整过程如下:

  1. 实例化Watcher:执行构造函数,调用this.get()方法
  2. 设置全局目标Dep.target = this,将当前Watcher实例赋给全局唯一对象
  3. 读取数据this.getter.call(vm, vm),触发数据的getter
  4. 收集依赖:getter中调用dep.depend(),从Dep.target获取Watcher并存入依赖数组
  5. 释放目标Dep.target = null,清空全局目标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Watcher的get方法
get() {
Dep.target = this // 1. 设置全局目标
const value = this.getter.call(this.vm, this.vm) // 2. 读取数据,触发getter
Dep.target = null // 3. 释放全局目标
return value
}

// defineReactive中的getter
get() {
dep.depend() // 从Dep.target获取Watcher并收集
return val
}

// Dep的depend方法
depend() {
if (Dep.target) {
Dep.target.addDep(this) // 将当前Dep添加到Watcher的依赖列表
}
}

简单总结

  • Watcher先把自己设置到全局唯一位置(Dep.target
  • 然后读取数据,触发getter
  • getter从全局位置读取Watcher,收集到Dep中
  • 数据变化时,Dep通知所有Watcher更新
  • 通过这种方式,Watcher可以主动订阅任意数据的变化

深度侦测

递归为对象的所有属性设置响应式:

1
2
3
4
5
6
7
8
function observe(obj) {
if (!obj || typeof obj !== 'object') return

Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
observe(obj[key]) // 递归处理嵌套对象
})
}

局限性

  1. 无法检测属性的添加和删除

    1
    2
    3
    4
    5
    6
    data.age = 25        // ✗ 检测不到
    delete data.name // ✗ 检测不到

    // 解决方案
    Vue.set(data, 'age', 25)
    Vue.delete(data, 'name')
  2. 无法检测数组索引变化

    1
    2
    arr[0] = 'x'         // ✗ 检测不到
    arr.length = 0 // ✗ 检测不到
  3. 性能问题:需要递归遍历所有属性

Array的变化侦测

由于Object.defineProperty无法检测数组索引变化,Vue2.0重写了数组的变异方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 重写数组方法
const arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

arrayMethods.forEach(method => {
const original = Array.prototype[method]

Object.defineProperty(arrayProto, method, {
value: function(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
ob.dep.notify() // 通知更新
return result
}
})
})

局限性

  • 无法检测索引变化:arr[0] = 'x'
  • 无法检测长度变化:arr.length = 0
  • 解决方案:使用Vue.set(arr, 0, 'x')arr.splice(0, 1, 'x')

Vue3.0的响应式原理

Vue3.0使用ES6的Proxy来重写响应式系统,解决了Vue2.0的诸多局限性。

Proxy的基本概念

Proxy可以创建一个对象的代理,拦截对该对象的各种操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const target = { name: '张三', age: 25 }
const handler = {
get(target, prop) {
console.log(`读取属性:${prop}`)
return target[prop]
},
set(target, prop, value) {
console.log(`设置属性:${prop},新值:${value}`)
target[prop] = value
return true
}
}

const proxy = new Proxy(target, handler)
console.log(proxy.name) // 读取属性:name
proxy.age = 26 // 设置属性:age,新值:26

Vue3.0的响应式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key) // 依赖收集
const result = Reflect.get(target, key, receiver)
if (isObject(result)) {
return reactive(result) // 递归创建响应式
}
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
},

deleteProperty(target, key) {
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, key) // 派发更新
}
return result
}
})
}

Proxy的优势

  1. 支持动态属性data.age = 25
  2. 支持数组索引变化arr[0] = 'x'
  3. 支持更多数据结构:Map、Set等
  4. 更好的性能:按需创建响应式

Proxy的局限性

  1. 兼容性问题:不支持IE浏览器
  2. 无法检测原始值:需要使用ref

响应式系统的核心组件

Dep(依赖收集器)

负责管理依赖关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Dep {
constructor() {
this.subs = [] // 存储所有依赖的Watcher
}

depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}

notify() {
this.subs.forEach(sub => sub.update())
}
}

Watcher(观察者)

负责观察数据变化并执行更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.cb = cb
this.getter = parsePath(expOrFn)
this.value = this.get() // 构造函数中调用get方法
}

get() {
Dep.target = this // 设置全局目标
const value = this.getter.call(this.vm, this.vm) // 读取数据,触发getter
Dep.target = null // 释放全局目标
return value
}

update() {
const oldValue = this.value
const newValue = this.get()
if (oldValue !== newValue) {
this.value = newValue
this.cb.call(this.vm, newValue, oldValue)
}
}
}

Watcher的工作流程

  1. 构造函数中调用get()方法
  2. get()方法设置Dep.target = this
  3. 读取数据触发getter,getter收集依赖
  4. 清空Dep.target
  5. 数据变化时,update()方法执行回调更新视图

Observer(观察者)

负责将普通对象转换为响应式对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()

if (Array.isArray(value)) {
this.observeArray(value)
} else {
this.walk(value)
}
}

walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}

完整的响应式流程

1. 初始化阶段

Vue实例创建时,调用initData初始化数据:

1
2
3
4
5
6
7
function initData(vm) {
let data = vm.$options.data
data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}

// 观察data,使其变为响应式
observe(data, true)
}

2. 数据劫持阶段

observe函数为每个对象创建Observer实例:

1
2
3
4
5
6
7
8
9
10
11
12
function observe(value, asRootData) {
if (!isObject(value) || value instanceof VNode) return

let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value)
}

return ob
}

3. 依赖收集阶段

模板编译时创建Watcher观察数据:

1
2
3
4
5
6
7
function mountComponent(vm, el) {
const updateComponent = () => {
vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {}, true)
}

4. 派发更新阶段

数据变化时触发setter,通知所有依赖的Watcher:

1
2
3
4
function setter(newVal) {
val = newVal
dep.notify() // 通知所有依赖
}

5. 异步更新队列

Vue使用异步更新队列优化性能:

1
2
3
4
5
6
7
8
9
10
11
12
function queueWatcher(watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
queue.push(watcher)

if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}

响应式流程图

1
2
3
4
初始化:Vue实例创建 → initData → observe → new Observer → defineReactive
依赖收集:模板编译 → new Watcher → get() → getter触发 → dep.depend()
派发更新:数据变化 → setter触发 → dep.notify() → watcher.update()
异步更新:update() → queueWatcher() → nextTick() → DOM更新

依赖收集的详细流程

  1. Watcher创建new Watcher() → 构造函数调用get()
  2. 设置目标Dep.target = this → 将Watcher设为全局目标
  3. 读取数据this.getter() → 触发数据的getter
  4. 收集依赖dep.depend() → 从Dep.target获取Watcher并收集
  5. 释放目标Dep.target = null → 清空全局目标
  6. 数据变化:setter触发 → dep.notify() → 通知所有Watcher更新

实际运行示例

1
2
3
4
5
6
7
8
9
// 1. 创建Vue实例
const vm = new Vue({
data: { message: 'Hello Vue' },
template: '<div>{{ message }}</div>'
})

// 2. 初始化:observe(data) → defineReactive(data, 'message')
// 3. 模板编译:创建Watcher → 读取data.message → 收集依赖
// 4. 数据变化:vm.message = 'World' → setter触发 → 通知Watcher → 更新视图

总结

Vue2.0特点

  • 基于Object.defineProperty:兼容性好,但无法检测动态属性
  • 数组方法重写:支持push、pop等操作
  • 局限性:无法检测属性添加/删除、数组索引变化

Vue3.0特点

  • 基于Proxy:功能更强大,支持动态属性
  • 支持数组索引变化arr[0] = 'x'
  • 更好的性能:按需创建响应式

核心要点

  1. 响应式是Vue的核心:数据驱动视图更新
  2. 核心组件:Dep(依赖收集器)、Watcher(观察者)、Observer(观察者)
  3. 完整流程:初始化 → 数据劫持 → 依赖收集 → 派发更新 → 异步更新