Skip to content

Vue深入解析 ​

Vue.js 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。在深入学习 Vue 之前,我们已经掌握了 Vue 的基本用法,本文将进一步探讨 Vue 的核心原理和高级特性。

1. 双向绑定原理 ​

Vue 最核心的功能之一就是数据的双向绑定。在 Vue 中,数据模型仅仅是普通的 JavaScript 对象。当修改数据时,视图会自动更新,反之亦然。

1.1 Vue 2.x 的实现原理 ​

在 Vue 2.x 中,双向绑定主要通过 Object.defineProperty() 方法实现。该方法可以直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

javascript
// 简化的响应式实现
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value');
      return val;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return;
      console.log('set value');
      val = newVal;
    }
  });
}

// 使用示例
const data = {};
defineReactive(data, 'name', 'Vue');
console.log(data.name); // get value, Vue
data.name = 'React'; // set value

1.2 Vue 3.x 的实现原理 ​

Vue 3.x 使用 Proxy 替代了 Object.defineProperty(),解决了后者的一些限制,如无法监听数组索引的变化、无法监听对象属性的添加和删除等问题。

javascript
// Vue 3 使用 Proxy 实现响应式
function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      console.log('get value');
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      console.log('set value');
      return Reflect.set(target, key, value, receiver);
    }
  };

  return new Proxy(target, handler);
}

// 使用示例
const data = reactive({ name: 'Vue' });
console.log(data.name); // get value, Vue
data.name = 'React'; // set value

1.3 依赖收集与派发更新 ​

Vue 的响应式系统还包括依赖收集和派发更新两个重要环节。依赖收集发生在数据被访问时,派发更新发生在数据被修改时。

javascript
// 简化的依赖收集与派发更新
class Dep {
  constructor() {
    this.subscribers = new Set();
  }

  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect);
    }
  }

  notify() {
    this.subscribers.forEach(effect => effect());
  }
}

let activeEffect = null;

function watchEffect(effect) {
  activeEffect = effect;
  effect();
  activeEffect = null;
}

const dep = new Dep();

// 模拟响应式数据访问
watchEffect(() => {
  dep.depend();
  console.log('执行副作用函数');
});

// 触发更新
dep.notify(); // 执行副作用函数

1.4 v-model 的实现机制 ​

v-model 是 Vue 提供的语法糖,本质上是 :value 和 @input 的组合。

vue
<!-- 普通写法 -->
<input :value="message" @input="message = $event.target.value">

<!-- v-model 语法糖 -->
<input v-model="message">

对于不同的表单元素,v-model 的实现略有不同:

  • 文本框和文本域:绑定 value 属性,监听 input 事件
  • 复选框:绑定 checked 属性,监听 change 事件
  • 单选按钮:绑定 checked 属性,监听 change 事件
  • 选择框:绑定 value 属性,监听 change 事件

2. nextTick 原理 ​

在 Vue 中,数据变化后 DOM 更新是异步执行的。nextTick 就是在下次 DOM 更新循环结束之后执行延迟回调。

2.1 异步更新队列 ​

Vue 在更新 DOM 时是异步执行的,只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

javascript
// 简化的异步更新队列实现
const callbacks = [];
let pending = false;

function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

let timerFunc;

// 优先使用 Promise
if (typeof Promise !== 'undefined') {
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
  };
} else {
  // Fallback to setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

export function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    timerFunc();
  }
}

2.2 事件循环与微任务/宏任务 ​

JavaScript 的事件循环机制决定了 nextTick 的执行时机。微任务(microtask)总是在宏任务(macrotask)之前执行。

javascript
console.log('start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise');
});

console.log('end');

// 输出顺序:
// start
// end
// Promise
// setTimeout

2.3 nextTick 的多种实现方式 ​

Vue 的 nextTick 支持多种异步实现方式,按优先级依次为:

  1. Promise
  2. MutationObserver
  3. setImmediate
  4. setTimeout
javascript
// 简化的 nextTick 实现
export const nextTick = (function () {
  const callbacks = []
  let pending = false
  let timerFunc

  function nextTickHandler () {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }

  if (typeof Promise !== 'undefined') {
    timerFunc = () => {
      Promise.resolve().then(nextTickHandler)
    }
  } else if (typeof MutationObserver !== 'undefined') {
    let counter = 1
    const observer = new MutationObserver(nextTickHandler)
    const textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (cb) {
    callbacks.push(cb)
    if (!pending) {
      pending = true
      timerFunc()
    }
  }
})()

2.4 应用场景与最佳实践 ​

nextTick 主要用于在 DOM 更新后执行某些操作:

javascript
// 修改数据
this.message = 'changed'

// DOM 还未更新
this.$nextTick(function () {
  // DOM 现在更新了
  this.$refs.input.focus()
})

// 作为 Promise 使用 (Vue 2.1.0 起新增)
this.$nextTick()
  .then(function () {
    // DOM 更新了
  })

3. diff 算法 ​

Vue 的虚拟 DOM 实现了高效的 diff 算法,使得视图更新更加高效。

3.1 虚拟 DOM 概念 ​

虚拟 DOM 是对真实 DOM 的抽象表示,本质上是一个 JavaScript 对象。

javascript
// 虚拟 DOM 结构示例
const vnode = {
  tag: 'div',
  props: {
    id: 'app',
    class: 'container'
  },
  children: [
    {
      tag: 'p',
      props: {},
      children: ['Hello Vue!']
    }
  ]
}

3.2 patch 过程详解 ​

patch 过程是比较新旧虚拟节点并更新真实 DOM 的过程。

javascript
// 简化的 patch 过程
function patch(oldVnode, vnode) {
  if (sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode)
  } else {
    const elm = oldVnode.elm
    const parent = nodeOps.parentNode(elm)
    createElm(vnode)

    if (parent !== null) {
      nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm))
      nodeOps.removeChild(parent, elm)
    }
  }
}

function patchVnode(oldVnode, vnode) {
  if (oldVnode === vnode) return

  const elm = vnode.elm = oldVnode.elm

  if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  const oldCh = oldVnode.children
  const ch = vnode.children

  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) updateChildren(elm, oldCh, ch)
    } else if (isDef(ch)) {
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1)
    } else if (isDef(oldCh)) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
}

3.3 同层比较策略 ​

Vue 的 diff 算法采用同层比较策略,只比较同一层级的节点,时间复杂度为 O(n)。

3.4 key 的重要性与优化 ​

在列表渲染时,为每个节点提供唯一的 key 值可以提高 diff 效率:

vue
<!-- 推荐 -->
<ul>
  <li v-for="item in items" :key="item.id">{{ item.text }}</li>
</ul>

<!-- 不推荐 -->
<ul>
  <li v-for="item in items">{{ item.text }}</li>
</ul>

4. 响应式系统 ​

Vue 的响应式系统是其核心特性之一,让我们能够以声明式的方式处理数据变化。

4.1 Vue 2.x 的响应式原理 ​

Vue 2.x 通过 Object.defineProperty() 实现响应式,但存在一些限制:

javascript
// Vue 2.x 响应式的限制
const vm = new Vue({
  data() {
    return {
      items: ['a', 'b', 'c']
    }
  }
})

// 以下操作不是响应式的
vm.items[1] = 'x'
vm.items.length = 0

// 正确的操作方式
Vue.set(vm.items, 1, 'x')
// 或者
vm.items.splice(1, 1, 'x')

vm.items.splice(0)

4.2 Vue 3.x 的 Proxy 重构 ​

Vue 3.x 使用 Proxy 重构了响应式系统,解决了 Vue 2.x 的诸多限制:

javascript
// Vue 3.x 响应式没有上述限制
import { reactive } from 'vue'

const state = reactive({
  items: ['a', 'b', 'c']
})

// 以下操作都是响应式的
state.items[1] = 'x'
state.items.length = 0

4.3 computed 和 watch 的实现 ​

computed 是基于响应式依赖进行缓存的计算属性:

javascript
// computed 的实现原理
class ComputedRefImpl {
  constructor(getter) {
    this._getter = getter
    this._value = undefined
    this._dirty = true
    this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
        }
      }
    })
  }

  get value() {
    if (this._dirty) {
      this._value = this.effect()
      this._dirty = false
    }
    return this._value
  }
}

watch 用于观察和响应 Vue 实例上的数据变化:

javascript
// watch 的实现原理
function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue = INITIAL_WATCHER_VALUE
  const job = () => {
    const newValue = effect.run()
    if (hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }

  const effect = new ReactiveEffect(getter, job)
  if (options.immediate) {
    job()
  } else {
    oldValue = effect.run()
  }
}

4.4 响应式系统的局限性与解决方案 ​

尽管 Vue 3.x 的响应式系统已经很完善,但仍有一些需要注意的地方:

  1. 对象属性的添加和删除需要使用特定 API
  2. Map、Set 等集合类型的响应式支持有限
  3. 解构赋值会失去响应性
javascript
// 解决方案:使用 toRefs
import { reactive, toRefs } from 'vue'

const state = reactive({
  foo: 1,
  bar: 2
})

// 解构会失去响应性
const { foo, bar } = state

// 使用 toRefs 保持响应性
const { foo, bar } = toRefs(state)

5. keep-alive 实现原理 ​

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件在切换时不被销毁,而是被缓存起来。

5.1 缓存机制 ​

keep-alive 通过缓存组件实例来避免重复创建和销毁:

javascript
// 简化的 keep-alive 实现
export default {
  name: 'keep-alive',
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created() {
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed() {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted() {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render() {
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    const componentOptions = vnode && vnode.componentOptions

    if (componentOptions) {
      const name = getComponentName(componentOptions)
      const { include, exclude } = this

      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key

      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }

    return vnode || (slot && slot[0])
  }
}

5.2 生命周期钩子处理 ​

被 keep-alive 包裹的组件会多出两个生命周期钩子:activated 和 deactivated。

javascript
export default {
  name: 'MyComponent',
  data() {
    return {
      count: 0
    }
  },
  activated() {
    // 组件激活时调用
    console.log('组件被激活')
    // 可以在这里恢复定时器、重新获取数据等
  },
  deactivated() {
    // 组件失活时调用
    console.log('组件被缓存')
    // 可以在这里暂停定时器、保存状态等
  },
  methods: {
    startTimer() {
      this.timer = setInterval(() => {
        this.count++
      }, 1000)
    }
  },
  activated() {
    // 重新启动定时器
    this.startTimer()
  },
  deactivated() {
    // 清除定时器
    clearInterval(this.timer)
  }
}

5.3 LRU 缓存策略 ​

当缓存数量超过 max 设置的值时,keep-alive 会使用 LRU(Least Recently Used)策略清除最久未使用的缓存:

javascript
// LRU 缓存清除逻辑
function pruneCacheEntry(cache, key, keys, current) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

// 当缓存数量超过 max 时清除最早缓存的组件
if (this.max && keys.length > parseInt(this.max)) {
  pruneCacheEntry(cache, keys[0], keys, this._vnode)
}

5.4 include/exclude 匹配规则 ​

可以通过 include 和 exclude 属性控制哪些组件应该被缓存:

vue
<!-- 基本用法 -->
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 数组语法 -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 动态组件 -->
<keep-alive :include="includedComponents">
  <router-view></router-view>
</keep-alive>

6. 插槽原理 ​

插槽(Slot)是 Vue 实现内容分发的 API,用于组件之间的内容传递。

6.1 普通插槽 vs 作用域插槽 ​

普通插槽只能传递静态内容,而作用域插槽可以传递数据给父组件使用:

vue
<!-- 子组件 MyComponent.vue -->
<template>
  <div>
    <!-- 普通插槽 -->
    <slot></slot>

    <!-- 具名插槽 -->
    <slot name="header"></slot>

    <!-- 作用域插槽 -->
    <slot name="footer" :user="user" :time="currentTime"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: { name: 'John', age: 30 },
      currentTime: new Date().toLocaleString()
    }
  }
}
</script>

<!-- 父组件使用 -->
<template>
  <MyComponent>
    <!-- 普通插槽内容 -->
    <p>这是默认插槽内容</p>

    <!-- 具名插槽 -->
    <template #header>
      <h1>这是头部内容</h1>
    </template>

    <!-- 作用域插槽 -->
    <template #footer="{ user, time }">
      <p>用户: {{ user.name }}, 时间: {{ time }}</p>
    </template>
  </MyComponent>
</template>

6.2 插槽的编译过程 ​

Vue 在编译阶段会将插槽内容转换为函数,在运行时动态生成:

javascript
// 编译前的模板
<my-component>
  <template #default>
    <p>Hello</p>
  </template>
</my-component>

// 编译后的渲染函数
function render() {
  return createElement('my-component', {
    scopedSlots: {
      default: () => createElement('p', 'Hello')
    }
  })
}

6.3 插槽的渲染机制 ​

插槽的渲染分为编译时和运行时两个阶段:

  1. 编译时:将插槽内容编译为函数
  2. 运行时:在子组件中调用这些函数生成 VNode
javascript
// 简化的插槽渲染机制
export default {
  render(h) {
    // 获取插槽内容
    const slots = this.$slots
    const scopedSlots = this.$scopedSlots

    return h('div', [
      // 渲染默认插槽
      slots.default || [],

      // 渲染具名插槽
      slots.header || [],

      // 渲染作用域插槽
      scopedSlots.footer && scopedSlots.footer({
        user: this.user,
        time: this.currentTime
      }) || []
    ])
  }
}

6.4 动态插槽名 ​

Vue 2.6.0+ 支持动态插槽名:

vue
<template>
  <MyComponent>
    <template #[dynamicSlotName]>
      <p>动态插槽内容</p>
    </template>
  </MyComponent>
</template>

<script>
export default {
  data() {
    return {
      dynamicSlotName: 'header'
    }
  }
}
</script>

7. 父子组件传参 ​

组件间通信是 Vue 开发中的重要话题,合理的通信方式能让代码更清晰易维护。

7.1 props-down, events-up ​

这是 Vue 组件通信的基本原则:

  • 父组件通过 props 向子组件传递数据
  • 子组件通过事件向父组件传递消息
vue
<!-- 父组件 -->
<template>
  <child-component
    :message="parentMessage"
    @child-event="handleChildEvent">
  </child-component>
</template>

<script>
export default {
  data() {
    return {
      parentMessage: 'Hello from parent'
    }
  },
  methods: {
    handleChildEvent(data) {
      console.log('收到子组件消息:', data)
    }
  }
}
</script>

<!-- 子组件 -->
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="sendToParent">发送消息给父组件</button>
  </div>
</template>

<script>
export default {
  props: ['message'],
  methods: {
    sendToParent() {
      this.$emit('child-event', 'Hello from child')
    }
  }
}
</script>

7.2 $attrs 和 $listeners ​

在组件层级较深时,可以通过 $attrs 和 $listeners 实现属性和事件的透传:

vue
<!-- 中间组件 -->
<template>
  <child-component v-bind="$attrs" v-on="$listeners"></child-component>
</template>

<script>
export default {
  inheritAttrs: false
}
</script>

7.3 provide/inject 跨层级通信 ​

provide/inject 用于祖先组件向所有子孙组件注入数据:

javascript
// 祖先组件
export default {
  provide() {
    return {
      theme: this.theme,
      changeTheme: this.changeTheme
    }
  },
  data() {
    return {
      theme: 'light'
    }
  },
  methods: {
    changeTheme(theme) {
      this.theme = theme
    }
  }
}

// 子孙组件
export default {
  inject: ['theme', 'changeTheme'],
  computed: {
    currentTheme() {
      return this.theme
    }
  },
  methods: {
    switchToDark() {
      this.changeTheme('dark')
    }
  }
}

7.4 EventBus 与全局状态管理 ​

对于非父子关系的组件通信,可以使用事件总线或状态管理方案:

javascript
// 创建事件总线
export const EventBus = new Vue()

// 组件 A 发送事件
EventBus.$emit('custom-event', data)

// 组件 B 监听事件
EventBus.$on('custom-event', (data) => {
  // 处理数据
})

对于复杂应用,建议使用 Vuex 或 Pinia 等状态管理库。

8. 其他核心知识 ​

8.1 生命周期详细解析 ​

Vue 实例从创建到销毁会经历一系列生命周期钩子:

javascript
export default {
  // 创建阶段
  beforeCreate() {
    // 实例初始化之后,数据观测和事件配置之前
  },
  created() {
    // 实例创建完成,已完成数据观测、属性和方法的运算,watch/event 事件回调
  },

  // 挂载阶段
  beforeMount() {
    // 挂载开始之前被调用
  },
  mounted() {
    // 实例挂载到 DOM 上之后调用
  },

  // 更新阶段
  beforeUpdate() {
    // 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前
  },
  updated() {
    // 数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用
  },

  // 销毁阶段
  beforeDestroy() {
    // 实例销毁之前调用
  },
  destroyed() {
    // 实例销毁后调用
  }
}

8.2 组件化设计思想 ​

组件化是 Vue 的核心思想之一,合理的设计能让组件更易复用和维护:

  1. 单一职责原则:一个组件只负责一个功能
  2. 开放封闭原则:对扩展开放,对修改封闭
  3. 依赖倒置原则:依赖抽象而非具体实现
vue
<!-- 好的设计 -->
<template>
  <div class="user-card">
    <user-avatar :src="user.avatar" />
    <user-info :name="user.name" :email="user.email" />
    <user-actions @edit="handleEdit" @delete="handleDelete" />
  </div>
</template>

<!-- 不好的设计 -->
<template>
  <div class="user-card">
    <img :src="user.avatar" />
    <div>
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
    <div>
      <button @click="handleEdit">编辑</button>
      <button @click="handleDelete">删除</button>
    </div>
  </div>
</template>

8.3 编译优化策略 ​

Vue 3.x 在编译阶段做了很多优化:

  1. 静态提升:将静态节点提升到渲染函数之外
  2. 预字符串化:将连续的静态节点预字符串化
  3. 缓存事件处理函数:避免不必要的函数创建
  4. Block Tree:只更新动态节点
javascript
// 静态提升示例
const _hoisted_1 = { class: "static-class" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "静态内容", -1)

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _hoisted_2,
    _createElementVNode("p", null, _ctx.dynamicContent, 1 /* TEXT */)
  ]))
}

8.4 性能监控与调优 ​

Vue 提供了一些工具来帮助监控和优化性能:

  1. Vue DevTools:浏览器插件,用于调试 Vue 应用
  2. Performance API:浏览器提供的性能监控 API
  3. webpack-bundle-analyzer:分析打包文件大小
javascript
// 使用 Performance API 监控组件渲染性能
export default {
  async mounted() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        console.log(entry.name, entry.duration)
      }
    })

    observer.observe({ entryTypes: ['measure'] })

    performance.mark('render-start')
    await this.fetchData()
    performance.mark('render-end')
    performance.measure('render-duration', 'render-start', 'render-end')
  }
}

通过深入了解这些 Vue 的核心原理和高级特性,我们可以更好地利用 Vue 构建高性能、可维护的应用程序。在实际开发中,我们应该根据项目需求选择合适的技术方案,并持续关注 Vue 生态的发展。