Vue 2.x 响应式系统与组件更新
Vue 2.x 组件级响应式原理和 Virtual DOM diff 机制
问题
Vue 2.x 的响应式系统是如何工作的?为什么说它是”组件级响应式 + 组件内部 vdom diff”?
解答
响应式原理
Vue 2.x 使用 Object.defineProperty 劫持数据的 getter/setter:
// 简化版响应式实现
function defineReactive(obj, key, val) {
const dep = new Dep() // 每个属性都有一个依赖收集器
Object.defineProperty(obj, key, {
get() {
// 收集依赖(当前正在渲染的组件 Watcher)
if (Dep.target) {
dep.depend()
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
// 通知所有依赖更新
dep.notify()
}
})
}
// 依赖收集器
class Dep {
constructor() {
this.subs = [] // 存储 Watcher
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
Dep.target = null // 当前正在计算的 Watcher
组件级响应式
每个组件实例都有一个对应的 Watcher:
// 组件挂载时创建 Watcher
class Watcher {
constructor(vm, updateComponent) {
this.vm = vm
this.getter = updateComponent
this.value = this.get()
}
get() {
Dep.target = this // 设置当前 Watcher
const value = this.getter.call(this.vm) // 执行渲染,触发 getter
Dep.target = null
return value
}
update() {
// 数据变化时,重新渲染组件
queueWatcher(this) // 异步队列,批量更新
}
run() {
this.get() // 重新执行 updateComponent
}
}
// 组件挂载
function mountComponent(vm) {
const updateComponent = () => {
// 1. 调用 render 生成新 VNode
const vnode = vm._render()
// 2. patch 对比更新 DOM
vm._update(vnode)
}
// 一个组件对应一个 Watcher
new Watcher(vm, updateComponent)
}
组件内部 Virtual DOM Diff
当组件数据变化时,只在该组件内部进行 diff:
// _update 方法
Vue.prototype._update = function(vnode) {
const vm = this
const prevVnode = vm._vnode
vm._vnode = vnode
if (!prevVnode) {
// 首次渲染
vm.$el = patch(vm.$el, vnode)
} else {
// 更新:对比新旧 VNode
vm.$el = patch(prevVnode, vnode)
}
}
// 简化版 patch
function patch(oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
// 同类型节点,进行 diff
patchVnode(oldVnode, vnode)
} else {
// 不同类型,直接替换
const parent = oldVnode.elm.parentNode
createElm(vnode)
parent.replaceChild(vnode.elm, oldVnode.elm)
}
return vnode.elm
}
function patchVnode(oldVnode, vnode) {
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
// 对比子节点
if (oldCh && ch) {
updateChildren(elm, oldCh, ch) // diff 算法
} else if (ch) {
addVnodes(elm, ch)
} else if (oldCh) {
removeVnodes(elm, oldCh)
}
}
更新流程图
数据变化
↓
触发 setter
↓
dep.notify() 通知组件 Watcher
↓
Watcher 加入异步队列(去重)
↓
nextTick 执行队列
↓
Watcher.run() → updateComponent()
↓
vm._render() 生成新 VNode
↓
vm._update() → patch(oldVnode, newVnode)
↓
组件内部 diff,更新 DOM
异步更新队列
const queue = []
let waiting = false
function queueWatcher(watcher) {
const id = watcher.id
// 去重:同一个 Watcher 只入队一次
if (!queue.find(w => w.id === id)) {
queue.push(watcher)
}
if (!waiting) {
waiting = true
// 异步执行,批量更新
nextTick(flushSchedulerQueue)
}
}
function flushSchedulerQueue() {
queue.forEach(watcher => watcher.run())
queue.length = 0
waiting = false
}
关键点
- Object.defineProperty:Vue 2.x 通过劫持 getter/setter 实现响应式,无法检测属性新增/删除和数组索引变化
- 组件级 Watcher:每个组件一个 Watcher,数据变化只触发所属组件更新,而非全局更新
- 依赖收集:渲染时访问数据触发 getter,将当前组件 Watcher 收集到属性的 Dep 中
- 异步批量更新:同一事件循环内的多次数据变化会合并,通过 nextTick 异步执行一次更新
- 组件内 diff:更新时只在当前组件的 VNode 树内进行 diff,子组件作为整体节点处理
目录