Vue · 55/70
1. Composition API 逻辑复用 2. 微信小程序与 Vue 的区别 3. React Fiber 架构与 Vue 的设计差异 4. 渐进式框架的理解 5. React 和 Vue 的技术差异 6. React 和 Vue 的区别 7. setup 中获取组件实例 8. SPA 首屏加载优化 9. 单页应用如何提高加载速度 10. 模板预编译原理 11. 什么是虚拟DOM 12. Vite 的实现原理 13. VNode 的属性 14. Vue 组件中的原生事件监听器需要手动销毁吗 15. Vue 数组元素修改与视图更新 16. Vue 项目中封装 axios 17. 打破 Vue scoped 样式隔离 18. Vue 组件和插件的区别 19. Vue 组件通信方式 20. 虚拟 DOM 的实现原理 21. Computed 与 Watch 对比 22. Vue 项目跨域解决方案 23. Vue CSS scoped 的实现原理 24. Vue 组件渲染过程 25. Vue 自定义指令的使用场景 26. Vue data 为什么必须是函数 27. Vue 项目部署与 404 问题解决 28. Vue 组件错误统一监听 29. Vue Diff 算法:Vue2 vs Vue3 30. 手写 Vue 事件机制 31. Vue 中定义全局方法 32. Vue 框架理解 33. Vue.nextTick 原理与应用 34. Vue Mixin 的理解与应用 35. Vue2 对象新增属性不响应 36. Vue.observable 实现响应式状态管理 37. Vue 父组件监听子组件生命周期 38. Keep-Alive 实现原理 39. Vue 生命周期钩子 40. Vue 项目优化实践 41. Vue 性能优化 42. Vue 权限管理实现方案 43. Vue 大型项目的结构和组件划分 44. ref、toRef、toRefs 的区别与使用场景 45. Vue 渲染过程 46. Vue-Router 路由模式原理 47. Vue SSR 服务器端渲染实现 48. v-for 中 key 的作用 49. Vue slot 插槽的使用 50. Vue 模板编译原理 51. v-model 参数用法 52. v-if 与 v-show 区别 53. Vue 版本性能分析 54. Vue 1.x 响应式系统 55. Vue 2.x 响应式系统与组件更新 56. Vue2 数组变化检测的限制与解决方案 57. Vue2 响应式原理 58. Composition API vs Options API 59. Vue3 设置全局变量 60. watch 与 watchEffect 的区别 61. Vue3 响应式原理与优势 62. Vue 3 Proxy 响应式与性能优化 63. Vue3 实现 Modal 组件 64. Vuex 辅助函数的使用 65. Vue 3 的 Tree Shaking 特性 66. Vuex 数据刷新丢失问题 67. Vue3 新特性 68. Vuex 与 Pinia 状态管理 69. Vuex 的五种属性及其作用 70. Vuex 是什么?

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