Skip to content

Vue 2 核心原理详解

1. 响应式原理

1.1 什么是响应式

响应式是指当数据发生变化时,视图会自动更新,无需手动操作 DOM。Vue 的响应式系统是其核心特性之一,它使得数据和视图之间的同步变得简单和自动化。

1.2 实现原理

Vue 2 的响应式系统基于 Object.defineProperty 实现,主要包括以下几个部分:

1.2.1 数据劫持

javascript
function defineReactive(obj, key, val) {
  // 递归处理嵌套对象
  if (typeof val === 'object' && val !== null) {
    observe(val)
  }

  // 创建依赖收集器
  const dep = new Dep()

  // 劫持属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 收集依赖
      Dep.target && dep.addSub(Dep.target)
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      // 递归处理新值
      if (typeof newVal === 'object' && newVal !== null) {
        observe(newVal)
      }
      // 通知依赖更新
      dep.notify()
    }
  })
}

function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return
  }
  new Observer(obj)
}

class Observer {
  constructor(obj) {
    this.walk(obj)
  }

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

1.2.2 依赖收集

javascript
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)
    }
  }

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

1.2.3 Watcher

javascript
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.cb = cb
    this.expOrFn = expOrFn
    this.deps = []
    this.depIds = new Set()

    // 初始化时获取值,触发依赖收集
    this.value = this.get()
  }

  get() {
    Dep.target = this
    const value = this.expOrFn.call(this.vm, this.vm)
    Dep.target = null
    return value
  }

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

  addDep(dep) {
    if (!this.depIds.has(dep.id)) {
      this.depIds.add(dep.id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
}

1.3 响应式系统的工作流程

  1. 初始化:Vue 实例创建时,会对 data 中的所有属性进行递归遍历,使用 Object.defineProperty 劫持每个属性的 getter 和 setter。
  2. 依赖收集:当组件渲染时,会创建 Watcher 实例,触发 getter,将当前 Watcher 实例添加到对应属性的依赖收集器中。
  3. 数据更新:当数据发生变化时,会触发 setter,通知依赖收集器中的所有 Watcher 实例更新。
  4. 视图更新:Watcher 实例收到通知后,会重新计算值并触发回调函数,更新视图。

1.4 响应式系统的局限性

  1. 对象新增属性:Vue 无法检测到对象新增的属性,需要使用 Vue.set 或 this.$set。
  2. 数组索引和长度变化:Vue 无法检测到数组通过索引直接修改元素或修改数组长度的操作,需要使用数组方法(如 push、splice 等)。
  3. 深层嵌套对象:深层嵌套对象的响应式处理会影响性能。

1.5 解决方案

  1. 使用 Vue.set 或 this.$set:用于添加响应式属性。
  2. 使用数组方法:使用 Vue 包装的数组方法来修改数组。
  3. 扁平化状态:尽量使用扁平化的状态结构,减少深层嵌套。

2. 虚拟 DOM

2.1 什么是虚拟 DOM

虚拟 DOM(Virtual DOM)是一种编程概念,它是对真实 DOM 的抽象表示,本质上是一个 JavaScript 对象,用于描述 DOM 节点的结构和属性。

2.2 为什么需要虚拟 DOM

  1. 性能优化:直接操作真实 DOM 会导致页面重排和重绘,影响性能。虚拟 DOM 可以批量处理更新,减少直接操作 DOM 的次数。
  2. 跨平台:虚拟 DOM 是平台无关的,可以在不同平台(如浏览器、服务器、移动端)上使用。
  3. 简化 DOM 操作:通过操作虚拟 DOM,开发者可以更简洁地描述 UI 的变化。

2.3 虚拟 DOM 的结构

javascript
// 虚拟 DOM 节点的结构
const vnode = {
  tag: 'div', // 标签名
  data: {
    // 属性和事件等
    class: 'container',
    onclick: handleClick
  },
  children: [
    // 子节点
    {
      tag: 'p',
      data: {},
      children: ['Hello World']
    }
  ],
  text: null, // 文本节点内容
  elm: null, // 对应的真实 DOM 元素
  key: null // 用于 diff 算法的 key
}

2.4 虚拟 DOM 的创建

Vue 2 中,虚拟 DOM 的创建主要通过 createElement 函数实现:

javascript
function createElement(tag, data, children) {
  return {
    tag,
    data,
    children,
    text: null,
    elm: null,
    key: data && data.key
  }
}

在模板编译过程中,Vue 会将模板转换为渲染函数,渲染函数会调用 createElement 来创建虚拟 DOM。

3. Diff 算法

3.1 什么是 Diff 算法

Diff 算法是一种比较新旧虚拟 DOM 树差异的算法,用于高效地更新真实 DOM。Vue 2 的 Diff 算法采用了同层比较的策略,只比较同一层级的节点,不跨层级比较。

3.2 Diff 算法的原理

  1. 同层比较:只比较同一层级的节点,不跨层级比较。
  2. key 匹配:使用 key 来标识节点,提高匹配效率。
  3. 就地更新:尽可能复用现有节点,减少 DOM 操作。

3.3 Diff 算法的实现

3.3.1 基本流程

javascript
function patch(oldVnode, newVnode) {
  // 如果新旧节点相同,直接返回
  if (sameVnode(oldVnode, newVnode)) {
    patchVnode(oldVnode, newVnode)
  } else {
    // 如果新旧节点不同,替换整个节点
    const oldElm = oldVnode.elm
    const parentElm = oldElm.parentNode
    createElm(newVnode, parentElm, oldElm)
    parentElm.removeChild(oldElm)
  }
}

function sameVnode(a, b) {
  return a.key === b.key && a.tag === b.tag
}

function patchVnode(oldVnode, newVnode) {
  const elm = (newVnode.elm = oldVnode.elm)
  const oldCh = oldVnode.children
  const newCh = newVnode.children

  // 如果新旧节点相同,直接返回
  if (oldVnode === newVnode) return

  // 如果新旧节点都是文本节点,更新文本
  if (
    oldVnode.text !== null &&
    newVnode.text !== null &&
    oldVnode.text !== newVnode.text
  ) {
    elm.textContent = newVnode.text
  } else {
    // 如果旧节点有子节点,新节点没有子节点,删除所有子节点
    if (oldCh && !newCh) {
      elm.innerHTML = ''
    }
    // 如果旧节点没有子节点,新节点有子节点,添加所有子节点
    else if (!oldCh && newCh) {
      newCh.forEach(child => {
        createElm(child, elm)
      })
    }
    // 如果新旧节点都有子节点,比较子节点
    else if (oldCh && newCh) {
      updateChildren(elm, oldCh, newCh)
    }
  }
}

3.3.2 子节点比较算法

javascript
function updateChildren(parentElm, oldCh, newCh) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let newEndIdx = newCh.length - 1
  let oldStartVnode = oldCh[0]
  let newStartVnode = newCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx

  // 四指针算法
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 跳过空节点
    if (!oldStartVnode) {
      oldStartVnode = oldCh[++oldStartIdx]
    } else if (!oldEndVnode) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (!newStartVnode) {
      newStartVnode = newCh[++newStartIdx]
    } else if (!newEndVnode) {
      newEndVnode = newCh[--newEndIdx]
    }
    // 情况 1: 旧开始节点和新开始节点相同
    else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    }
    // 情况 2: 旧结束节点和新结束节点相同
    else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    // 情况 3: 旧开始节点和新结束节点相同
    else if (sameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode)
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    // 情况 4: 旧结束节点和新开始节点相同
    else if (sameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode)
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    }
    // 情况 5: 以上情况都不匹配,使用 key 查找
    else {
      if (!oldKeyToIdx) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      }
      const idxInOld = oldKeyToIdx[newStartVnode.key]
      if (!idxInOld) {
        // 新节点不存在于旧节点中,创建新节点
        createElm(newStartVnode, parentElm, oldStartVnode.elm)
      } else {
        // 新节点存在于旧节点中,移动节点
        const vnodeToMove = oldCh[idxInOld]
        patchVnode(vnodeToMove, newStartVnode)
        oldCh[idxInOld] = undefined
        parentElm.insertBefore(vnodeToMove.elm, oldStartVnode.elm)
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }

  // 处理剩余节点
  if (oldStartIdx > oldEndIdx) {
    // 旧节点遍历完毕,添加新节点
    const anchor = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null
    addVnodes(parentElm, anchor, newCh, newStartIdx, newEndIdx)
  } else if (newStartIdx > newEndIdx) {
    // 新节点遍历完毕,删除旧节点
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

function createKeyToOldIdx(children, beginIdx, endIdx) {
  const map = {}
  for (let i = beginIdx; i <= endIdx; i++) {
    const key = children[i].key
    if (key) {
      map[key] = i
    }
  }
  return map
}

3.4 Diff 算法的优化策略

  1. 同层比较:只比较同一层级的节点,不跨层级比较,减少比较次数。
  2. key 匹配:使用 key 来标识节点,提高匹配效率,避免不必要的 DOM 操作。
  3. 四指针算法:通过四个指针(旧开始、旧结束、新开始、新结束)来优化比较过程,减少 DOM 移动操作。
  4. 就地更新:尽可能复用现有节点,减少 DOM 创建和删除操作。

4. 相关问题及解决方案

4.1 响应式相关问题

4.1.1 问题:对象新增属性不响应

原因:Vue 2 的响应式系统是基于 Object.defineProperty 实现的,只能劫持对象已有的属性,无法检测到新增的属性。

解决方案

  • 使用 Vue.set 或 this.$set 方法添加属性
  • 使用展开运算符创建新对象
javascript
// 方法 1: 使用 Vue.set
Vue.set(vm.user, 'age', 30)

// 方法 2: 使用展开运算符
vm.user = { ...vm.user, age: 30 }

4.1.2 问题:数组索引和长度变化不响应

原因:Vue 2 对数组的处理是通过重写数组方法实现的,无法检测到通过索引直接修改元素或修改数组长度的操作。

解决方案

  • 使用 Vue 包装的数组方法(push、pop、shift、unshift、splice、sort、reverse)
  • 使用 Vue.set 或 this.$set 修改数组元素
javascript
// 方法 1: 使用数组方法
vm.items.push('new item')
vm.items.splice(0, 1, 'updated item')

// 方法 2: 使用 Vue.set
Vue.set(vm.items, 0, 'updated item')

4.1.3 问题:深层嵌套对象性能问题

原因:深层嵌套对象的响应式处理会导致大量的 Object.defineProperty 调用,影响性能。

解决方案

  • 扁平化状态结构
  • 使用 Object.freeze 冻结不需要修改的对象
  • 考虑使用 Vue 3 的 Proxy 实现(Vue 3 已解决此问题)

4.2 虚拟 DOM 相关问题

4.2.1 问题:虚拟 DOM 性能优化

原因:虚拟 DOM 的创建和 diff 过程也会消耗性能,特别是在大型应用中。

解决方案

  • 使用 key 提高 diff 效率
  • 避免不必要的组件渲染
  • 使用 shouldComponentUpdate 或 PureComponent
  • 合理使用 v-if 和 v-show

4.2.2 问题:虚拟 DOM 与真实 DOM 同步问题

原因:虚拟 DOM 的更新可能与真实 DOM 不同步,导致操作真实 DOM 时出现问题。

解决方案

  • 使用 this.$nextTick 确保 DOM 更新后再操作
  • 避免直接操作真实 DOM,尽量通过数据驱动

4.3 Diff 算法相关问题

4.3.1 问题:key 的使用

原因:不正确的 key 使用会导致 diff 算法效率降低,甚至出现错误。

解决方案

  • 使用唯一且稳定的 key
  • 避免使用索引作为 key(特别是在列表排序、过滤时)
  • 避免使用随机值作为 key

4.3.2 问题:大量节点的 diff 性能

原因:当节点数量过多时,diff 过程会变得缓慢。

解决方案

  • 使用虚拟滚动
  • 分页加载
  • 合理拆分组件
  • 使用 shouldComponentUpdate 优化

5. 实现原理的深入理解

5.1 响应式系统的实现细节

5.1.1 依赖收集的实现

依赖收集是响应式系统的核心,它确保了只有真正使用到数据的组件会在数据变化时更新。

javascript
// 依赖收集的关键代码
function defineReactive(obj, key, val) {
  const dep = new Dep()

  Object.defineProperty(obj, key, {
    get() {
      // 收集依赖
      if (Dep.target) {
        dep.addSub(Dep.target)
      }
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      // 通知依赖更新
      dep.notify()
    }
  })
}

5.1.2 Watcher 的工作原理

Watcher 是连接数据和视图的桥梁,它负责监听数据变化并更新视图。

javascript
// Watcher 的关键代码
class Watcher {
  get() {
    // 将当前 Watcher 设为全局目标
    Dep.target = this
    // 执行表达式,触发 getter,收集依赖
    const value = this.expOrFn.call(this.vm, this.vm)
    // 清除全局目标
    Dep.target = null
    return value
  }

  update() {
    // 异步更新,避免频繁更新
    queueWatcher(this)
  }

  run() {
    // 重新计算值
    const oldValue = this.value
    const newValue = this.get()
    // 触发回调
    if (newValue !== oldValue) {
      this.cb.call(this.vm, newValue, oldValue)
    }
  }
}

5.2 虚拟 DOM 的实现细节

5.2.1 VNode 的创建

VNode 是虚拟 DOM 的基本单位,它描述了一个 DOM 节点的结构。

javascript
// VNode 的创建
function createVNode(tag, data, children, text, elm) {
  return {
    tag,
    data,
    children,
    text,
    elm,
    key: data && data.key
    // 其他属性...
  }
}

5.2.2 渲染函数的生成

Vue 会将模板编译为渲染函数,渲染函数会创建虚拟 DOM。

javascript
// 模板
;<div id='app'>
  <p>{{ message }}</p>
</div>

// 编译后的渲染函数
function render() {
  return createVNode('div', { id: 'app' }, [
    createVNode('p', {}, [this.message])
  ])
}

5.3 Diff 算法的实现细节

5.3.1 同层比较的实现

同层比较是 Diff 算法的核心策略,它大大减少了比较的复杂度。

javascript
// 同层比较的关键代码
function patchVnode(oldVnode, newVnode) {
  // 处理文本节点
  if (oldVnode.text !== null && newVnode.text !== null) {
    if (oldVnode.text !== newVnode.text) {
      oldVnode.elm.textContent = newVnode.text
    }
  } else {
    // 处理子节点
    const oldCh = oldVnode.children
    const newCh = newVnode.children
    if (oldCh && newCh) {
      // 比较子节点
      updateChildren(oldVnode.elm, oldCh, newCh)
    } else if (newCh) {
      // 添加子节点
      addVnodes(oldVnode.elm, null, newCh, 0, newCh.length - 1)
    } else if (oldCh) {
      // 删除子节点
      removeVnodes(oldVnode.elm, oldCh, 0, oldCh.length - 1)
    }
  }
}

5.3.2 四指针算法的实现

四指针算法是 Vue 2 Diff 算法的核心优化,它通过四个指针来减少 DOM 操作。

javascript
// 四指针算法的关键代码
function updateChildren(parentElm, oldCh, newCh) {
  let oldStartIdx = 0,
    newStartIdx = 0
  let oldEndIdx = oldCh.length - 1,
    newEndIdx = newCh.length - 1
  let oldStartVnode = oldCh[0],
    newStartVnode = newCh[0]
  let oldEndVnode = oldCh[oldEndIdx],
    newEndVnode = newCh[newEndIdx]

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 各种匹配情况的处理...
  }

  // 处理剩余节点...
}

6. 性能优化建议

6.1 响应式优化

  1. 使用扁平化状态:减少深层嵌套对象,提高响应式系统的性能。
  2. 使用 Object.freeze:冻结不需要修改的对象,避免响应式处理的开销。
  3. 合理使用 computed:对于复杂的计算逻辑,使用 computed 缓存计算结果。
  4. 避免频繁修改数据:批量修改数据,减少触发更新的次数。

6.2 虚拟 DOM 优化

  1. 使用 key:为列表项提供唯一的 key,提高 diff 算法的效率。
  2. 避免不必要的渲染:使用 shouldComponentUpdate 或 PureComponent 避免不必要的渲染。
  3. 合理使用 v-if 和 v-show:根据使用场景选择合适的指令。
  4. 拆分组件:将大型组件拆分为小型组件,减少 diff 的范围。

6.3 Diff 算法优化

  1. 使用稳定的 key:使用唯一且稳定的 key,避免不必要的 DOM 操作。
  2. 避免频繁修改列表结构:尽量使用就地更新,避免列表结构的大幅变化。
  3. 使用虚拟滚动:对于长列表,使用虚拟滚动减少 DOM 节点数量。
  4. 分页加载:对于大量数据,使用分页加载减少一次性渲染的节点数量。

7. 总结

Vue 2 的核心原理包括响应式系统、虚拟 DOM 和 Diff 算法,它们共同构成了 Vue 高效、灵活的前端框架。

  • 响应式系统:基于 Object.defineProperty 实现,通过数据劫持和依赖收集,实现数据和视图的自动同步。
  • 虚拟 DOM:通过抽象真实 DOM,减少直接操作 DOM 的次数,提高性能。
  • Diff 算法:通过同层比较、key 匹配和四指针算法,高效地更新真实 DOM。

理解这些核心原理,有助于我们更好地使用 Vue,编写高性能的应用。同时,我们也需要注意这些原理的局限性,采取相应的优化策略,以确保应用的性能和可维护性。

在 Vue 3 中,这些原理有了进一步的改进,如使用 Proxy 替代 Object.defineProperty 实现响应式系统,使用更高效的 Diff 算法等,进一步提高了框架的性能和灵活性。

基于 VitePress 的本地知识库