主题模式
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 value1.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 value1.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
// setTimeout2.3 nextTick 的多种实现方式
Vue 的 nextTick 支持多种异步实现方式,按优先级依次为:
- Promise
- MutationObserver
- setImmediate
- 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 = 04.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 的响应式系统已经很完善,但仍有一些需要注意的地方:
- 对象属性的添加和删除需要使用特定 API
- Map、Set 等集合类型的响应式支持有限
- 解构赋值会失去响应性
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 插槽的渲染机制
插槽的渲染分为编译时和运行时两个阶段:
- 编译时:将插槽内容编译为函数
- 运行时:在子组件中调用这些函数生成 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 的核心思想之一,合理的设计能让组件更易复用和维护:
- 单一职责原则:一个组件只负责一个功能
- 开放封闭原则:对扩展开放,对修改封闭
- 依赖倒置原则:依赖抽象而非具体实现
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 在编译阶段做了很多优化:
- 静态提升:将静态节点提升到渲染函数之外
- 预字符串化:将连续的静态节点预字符串化
- 缓存事件处理函数:避免不必要的函数创建
- 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 提供了一些工具来帮助监控和优化性能:
- Vue DevTools:浏览器插件,用于调试 Vue 应用
- Performance API:浏览器提供的性能监控 API
- 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 生态的发展。