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,子组件作为整体节点处理