双向数据绑定原理
v-model 本质上是一个语法糖,它可以看作是 value 属性和 input 事件的组合。它允许开发者以更简洁的方式实现数据的双向绑定。
语法糖(Syntactic sugar)指的是一种在计算机语言中添加的语法,这种语法对语言的功能并没有影响,但是可以使程序员写出更简洁、更易读的代码。它就像糖衣一样,让代码看起来更甜美。
基本原理
v-model 实际上是结合了 v-bind 和 v-on 这两个指令来实现的:
- v-bind:将 Vue 实例中的数据绑定到表单元素的 value 属性上。
- v-on:监听表单元素的 input 事件,当用户输入发生变化时,将新的值更新回 Vue 实例的数据中。
具体实现过程
- 编译阶段:
- Vue 在编译模板时,会将 v-model 指令解析成 v-bind 和 v-on 指令。
- 对于不同的表单元素(如 input、textarea、select),会根据其类型选择合适的 v-bind 属性和 v-on 事件。
- 运行时:
- 数据到视图:当 Vue 实例的数据发生变化时,v-bind 会将新的值同步到表单元素的 value 属性上,从而更新视图。
- 视图到数据:当用户在表单元素中输入内容时,v-on 监听到的 input 事件会触发一个更新函数,将新的值同步回 Vue 实例的数据中。
不同标签的默认行为
input 和 textarea
- 属性:value
- 事件:input
select:
- 属性:value
- 事件:change
checkbox 和 radio:
- 属性:checked
- 事件:change
自定义 v-model
Vue 允许开发者通过 model 属性来自定义 v-model 的行为。可以通过 prop 和 event 属性来自定义绑定的属性和触发的事件。
<template>
<div>
<custom-input v-model:title="message"></custom-input>
<p>{{ message }}</p>
</div>
</template>
<script>
import CustomInput from './CustomInput.vue'
export default {
components: {
CustomInput,
},
data() {
return {
message: '',
}
},
}
</script>
<!-- CustomInput.vue -->
<template>
<input :value="title" @input="$emit('update:title', $event.target.value)" />
</template>
<script>
export default {
props: ['title'],
}
</script>
- v-model:title 指定了 prop 为 title,event 为 update:title。
- CustomInput 组件通过 props 接收 title 属性。
- 当用户在 input 中输入内容时,触发 input 事件,并通过 $emit 更新父组件的 message 数据。
Vue的单向数据流
指的是数据从父组件流向子组件,而子组件不能直接修改父组件的数据。这种机制确保了数据流向的清晰和可预测性,避免了复杂的状态管理问题。
特性
- 父到子的单向绑定:父组件的 prop 更新会传递到子组件,但子组件不能直接修改父组件的 prop。
- 防止意外状态修改:单向数据流防止子组件意外改变父组件的状态,从而避免应用中数据流向混乱。
- 数据刷新机制:每次父组件更新时,子组件中的所有 prop 都会刷新为最新值。
子组件修改 prop 的正确方式
如果子组件需要修改从父组件传递的 prop,应该通过 $emit 派发一个自定义事件,由父组件来修改数据。
<!-- 子组件 -->
<template>
<div>
<p>当前值:{{ value }}</p>
<button @click="updateValue">更新值</button>
</div>
</template>
<script>
export default {
props: ['value'],
methods: {
updateValue() {
// 通过 $emit 派发一个自定义事件,通知父组件更新数据
this.$emit('update:value', newValue);
}
}
}
</script>
<!-- 父组件 -->
<template>
<div>
<child-component :value="parentValue" @update:value="parentValue = $event" />
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
parentValue: '初始值'
};
}
}
</script>
常见场景和解决方案
- 将 prop 用作初始值
如果 prop 用于传递初始值,而子组件希望将其作为本地数据使用,可以在子组件中定义一个本地的 data 属性,并将 prop 用作其初始值。
<template>
<div>
<p>计数:{{ counter }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script>
export default {
props: ['initialCounter'],
data() {
return {
// 使用 prop 作为本地数据的初始值
counter: this.initialCounter
};
},
methods: {
increment() {
this.counter++;
}
}
}
</script>
- 转换原始值
如果 prop 以原始值传入且需要进行转换,可以使用 prop 的值来定义一个计算属性。
<template>
<div>
<p>标准化大小:{{ normalizedSize }}</p>
</div>
</template>
<script>
export default {
props: ['size'],
computed: {
// 使用 prop 的值定义计算属性
normalizedSize() {
return this.size.trim().toLowerCase();
}
}
}
</script>
Vue 的异步更新队列
Vue 在更新 DOM 时是异步执行的。当数据发生变化时,Vue 不会立即更新 DOM,而是将这些更新操作放入一个队列中。
在下一个事件循环中,Vue 会批量处理这些更新操作,从而提高性能。
为什么需要异步更新队列?
- 性能优化:避免频繁的 DOM 更新,减少不必要的计算和操作。
- 一致性:确保视图与数据的一致性,避免中间状态的视图更新。
- 避免闪烁:减少页面闪烁,提高用户体验。
工作原理
- 数据变化:当响应式数据发生变化时,Vue 的 Watcher 监听器会检测到这些变化。
- 加入队列:Vue 将更新操作放入一个异步队列中,并在队列中去重,避免重复更新。
- 异步执行:在下一个事件循环的微任务阶段,Vue 会批量处理队列中的更新操作,触发 DOM 渲染。
<template>
<div>
<h1 @click="updateMessage">{{ message }}</h1>
</div>
</template>
<script>
export default {
data() {
return {
message: '原始值',
};
},
methods: {
updateMessage() {
this.message = '修改后的值1';
this.message = '修改后的值2';
this.message = '修改后的值3';
console.log('数据已更新,但 DOM 还未更新');
this.$nextTick(() => {
console.log('DOM 已更新');
});
},
},
};
</script>
使用 nextTick,Vue 提供了 this.$nextTick 方法,允许在 DOM 更新完成后执行回调。这确保了在数据更新后,可以立即获取到更新后的 DOM
Vue中的合并对象
使用 Object.assign()
Object.assign() 是 JavaScript 的内置方法,用于将多个对象的属性复制到一个目标对象上。它不会修改原始对象,而是返回一个新的对象。
data() {
return {
originalObject: {
name: 'John',
age: 30,
},
newObject: {
city: 'New York',
},
};
},
methods: {
mergeObjects() {
// 使用 Object.assign() 合并对象
this.originalObject = Object.assign({}, this.originalObject, this.newObject);
console.log(this.originalObject);
// 输出: { name: 'John', age: 30, city: 'New York' }
},
},
使用 Vue.set()
Vue.set() 是 Vue 提供的一个方法,用于添加或更新对象的属性,并确保这些属性是响应式的。它可以直接修改对象的属性。
data() {
return {
originalObject: {
name: 'John',
age: 30,
},
newObject: {
city: 'New York',
},
};
},
methods: {
mergeObjects() {
// 使用 Vue.set() 合并对象
Object.keys(this.newObject).forEach(key => {
Vue.set(this.originalObject, key, this.newObject[key]);
});
console.log(this.originalObject);
// 输出: { name: 'John', age: 30, city: 'New York' }
},
},
使用扩展运算符
在现代 JavaScript 中,还可以使用扩展运算符 ... 来合并对象。这种方法简单且直观。
data() {
return {
originalObject: {
name: 'John',
age: 30,
},
newObject: {
city: 'New York',
},
};
},
methods: {
mergeObjects() {
// 使用扩展运算符合并对象
this.originalObject = { ...this.originalObject, ...this.newObject };
console.log(this.originalObject);
// 输出: { name: 'John', age: 30, city: 'New York' }
},
},
注意事项
- 响应式:Vue.set() 确保添加的属性是响应式的,而 Object.assign() 和扩展运算符需要确保整个对象被替换,以确保 Vue 能够检测到变化。
- 性能:扩展运算符和 Object.assign() 创建新的对象,而 Vue.set() 直接修改现有对象。
- 适用场景:根据具体需求选择合适的方法。如果需要确保响应式,建议使用 Vue.set()。
setup 语法糖
介绍 <script setup>
是 Vue 3 提供的一种语法糖,用于更简洁地书写组件,特别是在使用 Composition API 时。 它将 setup 函数的内容直接放置在 <script>
标签内,省略了显式定义 setup 函数的步骤,从而减少了模板代码的冗余,使代码更加简洁明了
<script setup>
import { ref, onMounted } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
onMounted(() => {
console.log('Component mounted')
})
</script>
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
原理
1、编译阶段的转换
- 所有在
<script setup>
中声明的变量和函数,都会被自动包含在 setup 函数的作用域中。 - 编译器会自动生成 setup 函数,并将
<script setup>
中的代码包裹在其中
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
会被编译器转换为:
setup() {
import { ref } from 'vue';
const count = ref(0);
return { count };
}
2、自动暴露变量到模板
在 <script setup>
中导入的组件,无需显式注册到 components 选项中。这是通过以下机制实现的:
- 编译器会解析
<script setup>
中的 import 语句,识别出所有导入的组件。 - 这些组件会被自动注册为本地组件,可以直接在模板中使用
3、自动注册组件 在 <script setup>
中导入的组件,无需显式注册到 components 选项中。这是通过以下机制实现的: 编译器会解析 <script setup>
中的 import 语句,识别出所有导入的组件。 这些组件会被自动注册为本地组件,可以直接在模板中使用
4、defineProps 和 defineEmits 的实现 defineProps 和 defineEmits 是<script setup>
中的两个特殊宏,用于声明组件的 props 和 emits:
- defineProps 和 defineEmits 在编译阶段会被替换为对应的 props 和 emits 选项。
- 编译器会解析这些宏的调用,并生成对应的 props 和 emits 选项
<script setup>
const props = defineProps({
title: String
});
const emit = defineEmits(['increment']);
</script>
会被编译器转换为:
export default {
props: {
title: String,
},
emits: ['increment'],
setup(props, { emit }) {
return { props, emit }
},
}
5、顶层作用域的处理 在 <script setup>
中,所有变量和函数都处于顶层作用域,无需手动管理作用域。这是通过编译器的处理实现的: 编译器会自动处理变量和函数的作用域,确保它们在模板中可用
主要特性
1、无需显式 return: 在 <script setup>
中定义的变量和函数自动在模板中可用,无需手动 return。 所有定义的变量和方法都可以直接在模板中使用。
2、自动导入组件: 在 <script setup>
中导入的组件不需要显式注册到 components 选项中,Vue 会自动将其注册为本地组件。
<script setup>
import MyComponent from './MyComponent.vue';
</script>
<template>
<MyComponent />
</template>
3、声明 props 和 emits: 可以直接使用 defineProps 和 defineEmits 函数来声明组件的 props 和 emits,无需通过 props 和 emits 选项显式定义。
<script setup>
const props = defineProps({
title: String,
count: Number,
})
const emit = defineEmits(['increment'])
const handleClick = () => {
emit('increment')
}
</script>
<template>
<div>
<h1>{{ props.title }}</h1>
<p>{{ props.count }}</p>
<button @click="handleClick">Increment</button>
</div>
</template>
4、顶层作用域: 在 <script setup>
中定义的所有变量和函数都在顶层作用域中声明,无需手动管理作用域,Vue 会自动处理依赖关系。
5、组合式 API 和模板语法更简洁: 由于所有内容都直接声明在 <script setup>
中,减少了样板代码,并且可以直接使用 Vue 的响应式系统和生命周期钩子,无需过多的配置。
nextTick
nextTick 是 Vue.js 提供的一个方法,用于在 DOM 更新完成后执行回调函数。
使用场景
确保 DOM 更新完成,在数据更新后,确保 DOM 已经更新完成,再执行某些操作。
this.message = 'New Message'
this.$nextTick(() => {
console.log(this.$el.textContent) // 输出: New Message
})
优化性能,将多个回调函数合并到一个事件循环中,减少不必要的 DOM 操作。
this.message = 'New Message'
this.$nextTick(() => {
// 执行一些操作
})
this.$nextTick(() => {
// 执行另一些操作
})
实现原理
nextTick 的实现基于 JavaScript 的异步机制,主要使用了以下几种方法:
- Promise(微任务)
- MutationObserver(微任务)
- setImmediate(宏任务)
- setTimeout(宏任务,作为兜底方案)
Vue 的 nextTick 实现会根据环境自动选择最优的方法。以下是 nextTick 的核心实现逻辑:
let callbacks = [];
let pending = false;
function flushCallbacks() {
callbacks.forEach(callback => callback());
callbacks = [];
pending = false;
}
let timerFunc;
// 尝试使用 Promise
if (typeof Promise !== 'undefined' && Promise.resolve) {
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== 'undefined') {
// 尝试使用 MutationObserver
let counter = 1;
const textNode = document.createTextNode('');
const observer = new MutationObserver(flushCallbacks);
observer.observe(textNode, { characterData: true });
timerFunc = () => {
textNode.data = counter = (counter + 1) % 2;
};
} else if (typeof setImmediate !== 'undefined') {
// 尝试使用 setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 兜底方案:使用 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
callbacks.push(cb);
if (!pending) {
pending = true;
timerFunc();
}
}
工作原理
- 回调收集:nextTick 将所有回调收集到一个数组 callbacks 中。
- 异步调度:根据环境选择最优的异步方法(Promise、MutationObserver、setImmediate 或 setTimeout)。
- 回调执行:在下一次事件循环中,执行所有收集的回调。
优先级
- 微任务优先:Promise 和 MutationObserver 属于微任务,优先级高于宏任务。
- 宏任务兜底:如果微任务不可用,则使用 setImmediate 或 setTimeout。
Vue单页面和多页面
对比项 | 单页面应用(SPA) | 传统多页面应用(MPA) |
---|---|---|
定义 | 一个主页面,通过路由动态加载页面片段,局部刷新。 | 多个独立页面,页面跳转时整页刷新。 |
用户体验 | 页面切换流畅,交互效果炫酷。 | 页面跳转速度慢,用户体验较差。 |
性能 | 减少服务器压力,仅加载必要资源。 | 每次跳转重新加载资源,增加服务器压力。 |
SEO | 不友好,需要额外优化(如 SSR)。 | 友好,搜索引擎爬虫易于索引。 |
导航功能 | 需手动实现前进、后退功能。 | 浏览器自带前进、后退功能。 |
初次加载 | 首屏加载时间较长,但可通过懒加载优化。 | 每次跳转加载整个页面,速度较慢。 |
复杂度 | 前端逻辑复杂,对开发者要求高。 | 逻辑简单,适合小型项目。 |
适用场景 | 复杂交互型应用(如管理后台、在线商城)。 | 简单静态网站或内容型网站。 |
前后端分离 | 职责分明,便于团队协作。 | 前后端耦合度高,协作困难。 |
开发效率 | 组件化开发,代码复用性高。 | 代码复用性低。 |
浏览器兼容性 | 依赖现代浏览器,兼容性较差。 | 不依赖复杂 JavaScript,兼容性更好。 |
Vue路由实现
Hash 模式
location.hash:
- location.hash 的值是 URL 中 # 后面的部分。
- 当 location.hash 发生变化时,浏览器不会重新加载页面,也不会发送请求到服务器。
- Vue Router 通过监听 hashchange 事件来检测路由的变化,并更新视图。
工作流程:
- 用户点击链接或调用 router.push 方法,URL 的 hash 部分发生变化。
- 浏览器触发 hashchange 事件。
- Vue Router 监听到 hashchange 事件,解析新的 hash 值。
- 根据新的 hash 值匹配路由规则,更新视图。
history 模式
HTML5 History API:
- 主要使用 history.pushState() 和 history.replaceState() 方法。
- history.pushState():在浏览器历史记录中添加一个新记录,不会重新加载页面。
- history.replaceState():替换当前历史记录,不会重新加载页面。
- 浏览器会触发 popstate 事件,当用户点击浏览器的前进或后退按钮时。
HTML5 History API:
- 主要使用 history.pushState() 和 history.replaceState() 方法。
- history.pushState():在浏览器历史记录中添加一个新记录,不会重新加载页面。
- history.replaceState():替换当前历史记录,不会重新加载页面。
- 浏览器会触发 popstate 事件,当用户点击浏览器的前进或后退按钮时。
route和router的区别?
$router
$router 是 Vue Router 的实例对象,用于管理整个应用的路由。它是一个全局对象,包含了许多关键的方法和属性,用于导航和路由管理。
push:向 history 栈添加一个新的记录。
this.$router.push('home');
this.$router.push({ path: 'home' });
this.$router.push({ name: 'user', params: { userId: 123 }});
this.$router.push({ path: 'register', query: { plan: '123' }});
go:页面路由跳转,前进或后退。
this.$router.go(-1); // 后退
this.$router.go(1); // 前进
replace:替换当前的页面,不会向 history 栈添加新的记录。
this.$router.replace('home');
this.$router.replace({ path: 'home' });
**route**
route 是一个对象,表示当前的路由信息,包含了当前 URL 解析得到的信息。它是一个响应式对象,用于访问当前路由的状态。
- path:当前路由的路径,总是解析为绝对路径。
- params:包含动态片段和全匹配片段的 key/value 对象。
- query:包含 URL 查询参数的 key/value 对象。
- hash:当前路由的 hash 值(不带 #),如果没有 hash 值,则为空字符串。
- fullPath:完整解析后的 URL,包含查询参数和 hash 的完整路径。
- matched:包含当前匹配的路径中所有片段所对应的配置参数对象的数组。
- name:当前路径的名字。
- meta:路由元信息。
路由跳转和location.href
location.href 跳转
通过修改 window.location.href 来实现页面跳转。 简单方便,但会刷新页面,适用于跨域或完全跳转。
- 页面会刷新,重新加载整个页面。
- 适用于跨域跳转或完全跳转到其他网站。
- 会更新浏览器的历史记录。
window.location.href = '/new-page';
路由跳转 通过 Vue Router 提供的导航方法(如 push、replace)来实现页面跳转。 无刷新页面,适用于单页面应用,提供更好的用户体验。
- 页面不会刷新,只更新页面的局部内容。
- 适用于单页面应用(SPA)中的页面跳转。
- 保持浏览器的历史记录,支持前进和后退。
this.$router.push('/new-page');
this.$router.replace('/new-page');
Vue Router钩子函数
全局钩子
beforeEach:在全局范围内拦截导航,在导航确认之前调用。
- 参数:to(目标路由对象)、from(当前路由对象)、next(导航守卫)。
- 作用:通常用于权限验证、页面跳转等。
router.beforeEach((to, from, next) => {
const role = localStorage.getItem('ms_username');
if (!role && to.path !== '/login') {
next('/login');
} else if (to.meta.permission) {
role === 'admin' ? next() : next('/403');
} else {
if (navigator.userAgent.indexOf('MSIE') > -1 && to.path === '/editor') {
Vue.prototype.$alert('vue-quill-editor组件不兼容IE10及以下浏览器,请使用更高版本的浏览器查看', '浏览器不兼容通知', {
confirmButtonText: '确定'
});
} else {
next();
}
}
});
afterEach:在全局范围内拦截导航,在导航完成后调用。
- 参数:to(目标路由对象)、from(当前路由对象)。
- 作用:通常用于页面加载后的操作,如页面标题更新、分析工具追踪等。
router.afterEach((to, from) => {
document.title = to.meta.title || '默认标题';
});
单个路由钩子
在路由配置中为特定路由定义钩子,用于在进入或离开该路由时执行逻辑。
const routes = [
{
path: '/dashboard',
component: Dashboard,
beforeEnter: (to, from, next) => {
// 自定义逻辑
next();
}
}
];
组件路由钩子
beforeRouteEnter:在路由进入组件时调用,此时组件实例尚未创建。参数:to、from、next。
export default {
beforeRouteEnter(to, from, next) {
next(vm => {
if (vm.$route.meta.hasOwnProperty('auth_key') && vm.$route.meta.auth_key !== '') {
if (!vm.hasPermission(vm.$route.meta.auth_key)) {
vm.$router.replace('/admin/noPermission');
}
}
});
}
};
beforeRouteUpdate:在路由更新(参数变化)时调用。参数:to、from、next。
export default {
beforeRouteUpdate(to, from, next) {
// 自定义逻辑
next();
}
};
beforeRouteLeave:在路由离开组件时调用。参数:to、from、next。
export default {
beforeRouteLeave(to, from, next) {
// 自定义逻辑
next();
}
};
Vue Router导航守卫
可以通过使用 Vue Router 提供的导航守卫来保护路由。
导航守卫是一种在路由跳转之前或之后执行的函数,用于验证用户的授权状态或其他条件。
创建路由配置
首先,创建一个路由配置文件,定义需要保护的路由。
// router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Login from './views/Login.vue';
import Home from './views/Home.vue';
import ProtectedPage from './views/ProtectedPage.vue';
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{
path: '/login',
name: 'login',
component: Login,
},
{
path: '/',
name: 'home',
component: Home,
},
{
path: '/protected',
name: 'protected',
component: ProtectedPage,
beforeEnter: (to, from, next) => {
// 路由级守卫
if (isLoggedIn()) {
next();
} else {
next('/login');
}
},
},
],
});
// 全局守卫
router.beforeEach((to, from, next) => {
if (to.name === 'login') {
if (isLoggedIn()) {
next('/');
} else {
next();
}
} else if (!isLoggedIn()) {
next('/login');
} else {
next();
}
});
function isLoggedIn() {
// 检查用户是否登录
return localStorage.getItem('isLoggedIn') === 'true';
}
export default router;
创建登录组件
创建一个登录组件,用于模拟用户登录。
<template>
<div>
<h1>登录页面</h1>
<button @click="login">登录</button>
</div>
</template>
<script>
export default {
methods: {
login() {
// 模拟登录
localStorage.setItem('isLoggedIn', 'true');
this.$router.push('/');
},
},
};
</script>
创建受保护的页面
创建一个受保护的页面,只有登录用户才能访问。
<template>
<div>
<h1>受保护的页面</h1>
<p>只有登录用户才能看到这个页面。</p>
<button @click="logout">退出登录</button>
</div>
</template>
<script>
export default {
methods: {
logout() {
localStorage.setItem('isLoggedIn', 'false');
this.$router.push('/login');
},
},
beforeRouteEnter(to, from, next) {
// 组件内守卫
if (localStorage.getItem('isLoggedIn') === 'true') {
next();
} else {
next('/login');
}
},
};
</script>
工作原理
- 全局守卫:
- 使用 router.beforeEach 定义一个全局守卫,对所有路由进行统一的权限验证。
- 如果用户未登录且访问的不是登录页面,则重定向到登录页面。
- 如果用户已登录且访问的是登录页面,则重定向到主页。
- 路由级守卫:
- 在路由配置中,使用 beforeEnter 守卫对特定路由进行额外的验证。
- 如果用户未登录,则重定向到登录页面。
- 组件内守卫:
- 在组件中,使用 beforeRouteEnter 守卫在组件加载前进行验证。
- 如果用户未登录,则重定向到登录页面。
注意事项
- 登录状态管理:在实际应用中,登录状态通常存储在 Vuex 或 Pinia 中,而不是直接存储在 localStorage 中。
- 安全性:在生产环境中,确保后端也对用户进行验证,避免仅依赖前端验证。
- 性能优化:确保导航守卫的逻辑简洁高效,避免不必要的计算。
Vue组件data是函数的原因
- 避免数据共享:对象是引用类型,若 data 是对象,所有实例共享同一数据,一处修改影响所有实例。
- 确保数据独立:每个组件实例应有独立数据,互不干扰。将 data 定义为函数,每次实例化时调用该函数返回新对象,确保数据独立。
Vuex 使用
Vuex 是 Vue.js 的状态管理模式,用于集中管理应用的状态。
state
存放应用的状态数据。
const store = new Vuex.Store({
state: {
count: 0
}
});
getter
类似于 Vue 的计算属性,用于派生状态。
const store = new Vuex.Store({
getters: {
doubleCount: state => state.count * 2
}
});
action
用于处理异步操作,通过 store.dispatch 触发。
const store = new Vuex.Store({
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment');
}, 1000);
}
}
});
mutation
定义修改状态的方法,必须是同步的。
const store = new Vuex.Store({
mutations: {
increment(state) {
state.count++;
}
}
});
modules
用于将 store 分模块管理,每个模块拥有自己的 state、mutations、actions 和 getters。
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
};
const store = new Vuex.Store({
modules: {
a: moduleA
}
});
// 访问模块状态:store.state.a.count
Vuex的mapState
mapState 是 Vuex 提供的一个辅助函数,用于将 Vuex store 中的 state 映射到组件的计算属性上。
这使得组件可以方便地访问 Vuex store 中的状态,而无需手动编写重复的代码。
工作原理
- 映射状态:mapState 将 store 中的 state 属性映射为组件的计算属性。
- 响应式更新:当 store 中的状态发生变化时,映射的计算属性会自动更新。
- 简化代码:避免手动编写重复的计算属性代码。
基本用法
mapState 可以将 store 中的 state 属性映射为组件的计算属性。
import { mapState } from 'vuex';
export default {
computed: {
// 使用 mapState 映射 store 中的 state
...mapState({
count: state => state.count,
message: state => state.message,
}),
},
};
对象展开运算符
mapState 返回一个对象,可以使用对象展开运算符 ... 将其混入到组件的 computed 属性中。
import { mapState } from 'vuex';
export default {
computed: {
...mapState({
count: state => state.count,
message: state => state.message,
}),
// 其他计算属性
doubleCount() {
return this.count * 2;
},
},
};
映射多个状态
可以同时映射多个状态属性。
import { mapState } from 'vuex';
export default {
computed: {
...mapState(['count', 'message', 'user']),
},
};
命名空间
如果 Vuex store 使用了命名空间,可以在 mapState 中指定命名空间。
import { mapState } from 'vuex';
export default {
computed: {
...mapState('moduleA', {
count: state => state.count,
message: state => state.message,
}),
},
};
Vue 的filter
一种用于格式化数据的工具,可以用于模板中的文本插值和表达式。
全局过滤器
全局过滤器需要在 Vue 实例创建之前注册,以便在整个应用中使用。
Vue.filter('testfilter', function (value, text) {
return value + text;
});
使用:
<template>
<div>{{ test | testfilter('Hello') }}</div>
</template>
<script>
export default {
data() {
return {
test: 'Vue'
};
}
};
</script>
工作原理
- 定义过滤器:使用 Vue.filter 方法定义全局过滤器。过滤器可以接受一个或多个参数。
- 使用过滤器:在模板中,通过 | 符号使用过滤器。过滤器可以链式调用,依次处理数据。
- 性能优化:过滤器的计算结果会被缓存,除非数据发生变化,否则不会重新计算。
注意事项
- 性能:过滤器的计算结果会被缓存,但如果数据频繁变化,可能会影响性能。
- 替代方案:在某些场景下,可以使用计算属性或方法代替过滤器,以获得更好的性能和可维护性。
- 链式调用:过滤器可以链式调用,但需要确保每个过滤器的输出都符合下一个过滤器的输入要求。
局部过滤器
局部过滤器在组件实例对象中定义,仅在该组件中有效。
export default {
data() {
return {
test: 'Vue'
};
},
filters: {
changemsg(val, text) {
return val + text;
}
}
};
使用:
<template>
<div>{{ test | changemsg('Hello') }}</div>
</template>
使用方式
过滤器只能在 {{}}
和 v-bind 中使用。定义时第一个参数是预处理的值,后面的参数是调用时传入的参数。
<template>
<div>
<h3 :title="test | changemsg(1234)">{{ test | changemsg(4567) }}</h3>
<h2>{{ name | filter1 | filter2 | filter3 }}</h2>
</div>
</template>
<script>
export default {
data() {
return {
test: 'Vue',
name: 'John'
};
},
filters: {
changemsg(val, text) {
return val + text;
}
}
};
</script>
Vue CLI 项目中注册多个全局过滤器
在 Vue CLI 项目中,可以通过创建一个单独的文件来定义和暴露过滤器,然后在 main.js 中导入并注册。
// 过滤器文件(filters.js)
const filter1 = function (val) {
return val + '--1';
};
const filter2 = function (val) {
return val + '--2';
};
const filter3 = function (val) {
return val + '--3';
};
export default {
filter1,
filter2,
filter3
};
// 在 main.js 中导入并注册
import Vue from 'vue';
import App from './App.vue';
import filters from './filters/filters.js';
// 注册全局过滤器
Object.keys(filters).forEach(key => {
Vue.filter(key, filters[key]);
});
Vue.config.productionTip = false;
new Vue({
render: h => h(App)
}).$mount('#app');
Vue的computed实现原理
初始化阶段
- 生命周期函数:在组件实例的 beforeCreate 阶段,Vue 开始处理 computed 属性。
- 遍历 computed 配置:Vue 遍历 computed 配置中的所有属性。
- 创建 Watcher:为每个 computed 属性创建一个 Watcher 对象,并传入 computed 配置中的 getter 函数。
- 依赖收集:getter 函数运行时会收集依赖,即 computed 属性所依赖的响应式数据。
延迟执行与缓存机制
- lazy 配置:与渲染函数不同,computed 属性的 Watcher 不会立即执行。它使用了 lazy 配置,允许 Watcher 在需要时才执行。
- 缓存属性:
- value:保存 Watcher 运行的结果,初始值为 undefined。
- dirty:标记当前的 value 是否已经过时(脏值),初始值为 true。
代理模式与访问逻辑
- 挂载到组件实例:Vue 使用代理模式,将 computed 属性挂载到组件实例中。
- 读取逻辑:
- 如果 dirty 为 true,运行 getter 函数,计算依赖,将结果保存在 Watcher 的 value 中,设置 dirty 为 false,然后返回结果。
- 如果 dirty 为 false,直接返回 Watcher 的 value。
依赖变化处理
- 依赖收集:computed 属性的依赖同时收集到组件的 Watcher。
- 触发更新:
- 当 computed 属性的依赖变化时,会触发 computed 属性的 Watcher,此时只需设置 dirty 为 true,不做其他处理。
- 由于依赖同时收集到组件的 Watcher,组件会重新渲染。重新渲染时会读取 computed 属性,由于 dirty 已为 true,会重新运行 getter 进行运算。
setter 处理
- 直接运行 setter:当设置 computed 属性时,直接运行 setter 函数。
v-if 和 v-show
v-if
- 渲染机制:条件不成立时,不渲染 DOM 元素。
- 切换性能:条件变化时,会导致元素的重新渲染,性能开销较大。
- 适用场景:适合条件很少改变的情况,例如根据用户权限显示或隐藏某些功能。
v-show
- 渲染机制:条件不成立时,渲染 DOM 元素,但通过 CSS 的 display: none 隐藏。
- 切换性能:条件变化时,仅切换样式,不重新渲染,性能较好。
- 适用场景:适合需要频繁切换显示状态的情况,例如切换菜单的显示和隐藏。
特性 | v-if | v-show |
---|---|---|
渲染 | 条件为真时才渲染。 | 无论条件真假都渲染,通过 CSS 控制显示。 |
销毁重建 | 切换时销毁重建。 | 切换时保留。 |
性能 | 初始渲染开销小,切换开销大。 | 初始渲染开销大,切换开销小。 |
适用场景 | 切换频率低,条件判断开销大。 | 切换频率高,条件判断开销小。 |
当v-if与v-for一起使用时,v-for具有比v-if更高的优先级,这意味着v-if将分别重复运行于每个v-for循环中。 所以,不推荐v-if和v-for同时使用。如果v-if和v-for一起用的话,vue中的的会自动提示v-if应该放到外层去
v-if、v-show、v-html原理
v-if
基于条件渲染元素及其子节点。条件为假时,元素不会被渲染到 DOM 中。
原理:
- 编译阶段:v-if 指令被编译成 JavaScript 代码,生成一个虚拟 DOM 节点树,其中包含条件判断逻辑。
- 渲染阶段:
- 条件为 true 时,生成相应的虚拟 DOM 并转换为真实 DOM 元素插入页面。
- 条件为 false 时,移除相关的虚拟 DOM 节点以及对应的真实 DOM 元素。
- 切换时:条件变化时,Vue 会执行完整的 DOM 操作(创建或销毁),即插入节点时进行挂载、销毁时调用卸载钩子并从 DOM 中移除。
v-show
始终渲染元素,只是通过 CSS 的 display 属性来控制显示与隐藏。
原理:
- 编译阶段:v-show 指令被转换成一个更新节点 display 样式的函数。
- 渲染阶段:在初次渲染时,v-show 所作用的元素会被创建并插入到 DOM 中。
- 切换时:v-show 仅仅修改元素的 display 样式(display: none 或 display: ''),并不会触发元素的重新挂载或销毁。
v-html
动态渲染 HTML 片段的指令,将表达式的内容作为 innerHTML 插入到元素中。
原理:
- 编译阶段:v-html 指令被编译成更新节点内容的逻辑,将给定的 HTML 字符串解析为真正的 HTML 结构,并插入到目标元素中。
- 渲染阶段:当表达式的值发生变化时,Vue 会更新目标元素的 innerHTML 内容,并将之前的内容完全替换为新的内容。
缺点:
- 存在潜在的 XSS(跨站脚本攻击)风险,如果插入的 HTML 内容是用户输入的或未经过安全过滤的内容,可能会导致安全隐患。
- Vue 无法直接追踪 v-html 插入的 HTML 中的 DOM 变化(例如事件绑定),因此复杂交互时不建议使用。
对比:
指令 | 原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
v-if | 基于条件渲染控制节点的创建与销毁,逻辑复杂,切换时有性能损耗。 | 减少初始渲染开销,不会渲染不需要的元素。 | 频繁切换时,销毁和重新创建 DOM 节点会有较大的性能消耗。 | 需要完全移除或条件性地渲染某些 DOM 节点。 |
v-show | 仅操作 CSS 样式来控制显示与隐藏,切换时开销小。 | 不会触发 DOM 的创建或销毁,频繁切换时性能更好。 | 即使不可见,元素仍然存在于 DOM 中,占用内存。 | 只是简单的显示与隐藏切换(例如折叠面板)。 |
v-html | 通过 innerHTML 属性动态渲染 HTML 片段,使用不当时有安全风险。 | 可以渲染复杂的 HTML 内容。 | 存在 XSS 风险,Vue 无法追踪插入的 HTML 中的 DOM 变化。 | 需要动态插入 HTML 片段,并且内容经过了安全过滤。 |
Vue中的v-pre
跳过这个元素及其子元素的编译过程,直接将原始内容渲染到页面上。这意味着 Vue 不会对这些内容进行模板解析或动态绑定处理。
具体作用
- 跳过编译:Vue 通常会对模板中的内容进行解析和编译,但 v-pre 会让 Vue 完全忽略这个元素及其子元素的内容。
- 显示原始内容:如果元素中包含 Vue 的模板语法(如
{{ message }}
或指令),这些内容会直接显示为原始文本,而不会被解析为动态内容。
使用场景
- 当你需要显示包含 Vue 模板语法的原始内容时(例如文档或代码片段)。
- 当你需要调试 Vue 应用时,避免 Vue 对某些内容进行不必要的编译。
<div v-pre>
{{ message }} <!-- 这里的 {{ message }} 不会被 Vue 解析 -->
<span v-if="isVisible">Hello</span> <!-- 这里的 v-if 也不会被 Vue 处理 -->
</div>
输出:
{{ message }}
<span v-if="isVisible">Hello</span>
Vue中的v-cloak
v-cloak 是 Vue.js 提供的一个内置指令,用于解决首屏加载时的闪屏问题。
它通过在 HTML 元素上设置样式,隐藏未编译的模板,直到 Vue 实例完成编译后才显示内容。
使用方法
在 HTML 元素中使用 v-cloak 指令:
<div v-cloak>
{{ message }}
</div>
在 CSS 中定义 [v-cloak]
样式:
[v-cloak] {
display: none;
}
注意事项
[v-cloak]
样式需要在 Vue 实例编译之前定义,通常放在<link>
引入的 CSS 中,或者直接写在内联样式中。- 如果样式写在 import 引入的 CSS 中,可能会因为加载顺序问题导致样式不生效。
工作原理
- 在 Vue 实例初始化之前,v-cloak 会隐藏绑定的 HTML 元素。
- 当 Vue 实例完成编译后,v-cloak 指令会被移除,绑定的 HTML 元素会显示出来。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue v-cloak 示例</title>
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="app" v-cloak>
<h1>{{ message }}</h1>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@3.0.0/dist/vue.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
message: 'Hello Vue!'
};
}
});
app.mount('#app');
</script>
</body>
</html>
v-for中的key
为了高效地更新虚拟 DOM,Vue.js 要求 v-for 中的每个节点都有一个唯一的 key 属性。
key 是一个特殊的属性,用于给每个节点提供一个唯一的标识。它的主要作用是帮助 Vue.js 的虚拟 DOM Diff 算法正确地识别和更新节点。
为什么需要 key?
- 高效的更新:key 帮助 Vue.js 快速识别哪些节点发生了变化(新增、删除、更新),从而减少不必要的 DOM 操作。
- 正确的渲染:key 确保 Vue.js 能够正确地复用和排序节点,避免因为节点顺序变化导致的渲染错误。
key 的最佳实践
- 唯一性:key 应该是唯一的,最好是使用数据中的唯一标识符(如数据库中的 ID)。
- 避免使用索引:虽然可以使用数组索引作为 key,但不推荐,因为索引会随着数组的变化而变化,可能导致性能问题和渲染错误。
- 动态列表:在动态列表中(如添加、删除、排序),key 的唯一性尤为重要。
<template>
<div>
<ul>
<!-- 使用 v-for 渲染动态列表 -->
<li v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</li>
</ul>
<button @click="addItem">Add Item</button>
<button @click="removeItem">Remove Item</button>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
],
};
},
computed: {
filteredItems() {
// 返回过滤后的列表
return this.items.filter(item => item.id % 2 === 0);
},
},
methods: {
addItem() {
const newItem = { id: this.items.length + 1, name: `Item ${this.items.length + 1}` };
this.items.push(newItem);
},
removeItem() {
if (this.items.length > 0) {
this.items.pop();
}
},
},
};
</script>
Vue的transition
用于管理元素或组件过渡动画的内置组件。它允许你定义进入、离开和列表变化时的过渡效果。
基本用法
transition 组件通过 name 属性定义过渡的名称,并包裹需要动画的内容:
<template>
<transition name="fade">
<div v-if="show">需要动画的内容</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: true
};
}
};
</script>
过渡的类
transition 提供了六个过渡类,用于定义进入和离开时的动画效果:
- v-enter:进入过渡的初始状态,在元素插入前生效,插入后一帧删除。
- v-enter-active:在元素插入前生效,在动画完成后删除。
- v-enter-to:在元素插入后一帧生效,在动画完成后删除。
- v-leave:离开过渡的初始状态,在元素离开时生效,下一帧删除。
- v-leave-active:在离开过渡时生效,在动画完成后删除。
- v-leave-to:离开过渡结束状态,在离开过渡下一帧生效,在动画完成后删除。 这些类会根据 name 属性动态生成,例如 name="fade" 时,类名会变为 fade-enter、fade-enter-active 等。
自定义类
可以通过 transition 的属性直接指定自定义的过渡类:
<template>
<transition
enter-class="enter-class"
enter-active-class="enter-active-class"
enter-to-class="enter-to-class"
leave-class="leave-class"
leave-active-class="leave-active-class"
leave-to-class="leave-to-class"
>
<div v-if="show">需要动画的内容</div>
</transition>
</template>
过渡类型
可以使用 type 属性指定 Vue 监听 transition 或 animation 事件:
<template>
<transition name="fade" type="animation">
<div v-if="show">需要动画的内容</div>
</transition>
</template>
过渡时间
可以通过 duration 属性设置过渡时间,可以是数字或对象:
<template>
<transition name="fade" :duration="1000">
<div v-if="show">需要动画的内容</div>
</transition>
</template>
<template>
<transition name="fade" :duration="{ enter: 1000, leave: 300 }">
<div v-if="show">需要动画的内容</div>
</transition>
</template>
过渡钩子函数
transition 提供了多个钩子函数,用于在过渡的不同阶段执行自定义逻辑:
- before-enter:进入过渡前调用。
- enter:进入过渡时调用,通常用于手动触发动画。
- after-enter:进入过渡完成后调用。
- enter-cancelled:进入过渡取消时调用。
- before-leave:离开过渡前调用。
- leave:离开过渡时调用,通常用于手动触发动画。
- after-leave:离开过渡完成后调用。
- leave-cancelled:离开过渡取消时调用。
<template>
<transition
name="fade"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
<div v-if="show">需要动画的内容</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: true
};
},
methods: {
beforeEnter(el) {
console.log('beforeEnter', el);
},
enter(el, done) {
console.log('enter', el);
done(); // 手动触发动画完成
},
afterEnter(el) {
console.log('afterEnter', el);
},
enterCancelled(el) {
console.log('enterCancelled', el);
},
beforeLeave(el) {
console.log('beforeLeave', el);
},
leave(el, done) {
console.log('leave', el);
done(); // 手动触发动画完成
},
afterLeave(el) {
console.log('afterLeave', el);
},
leaveCancelled(el) {
console.log('leaveCancelled', el);
}
}
};
</script>
keep-alive说明
用于缓存组件实例,避免组件切换时的重复渲染。
场景:适用于需要频繁切换且保留状态的组件,如路由切换、标签页切换等。
常用属性
- include:指定哪些组件需要被缓存(支持字符串、正则表达式或数组)。
- exclude:指定哪些组件不需要被缓存(支持字符串、正则表达式或数组)。
- max:指定缓存的最大组件数,超出时按 LRU 策略淘汰。
生命周期钩子
- activated:组件被激活时调用。
- deactivated:组件被停用时调用。
keep-alive 的主要特点
- 缓存组件:当组件在 keep-alive 中被切换时,Vue 并不会销毁组件实例,而是将其保存在内存中,当组件再次激活时,状态和 DOM 都能恢复原状。
- 保留状态:组件的本地状态、输入框中的内容、滚动位置等在重新切换回来时不会丢失。
- 生命周期钩子:当组件被 keep-alive 缓存时,会触发 deactivated 钩子;再次激活时,会触发 activated 钩子。这两个钩子可以用于处理组件被缓存和重新激活时的操作。
keep-alive 的实现原理
- 缓存机制:
- keep-alive 会在内部维护一个缓存池(一个对象),用来存储已渲染过的组件实例。它通过 VNode(虚拟 DOM 节点)进行组件的缓存。
- 当组件第一次被渲染时,keep-alive 会将组件实例存入缓存池中,而不是销毁该实例。
- 当组件被切换时,keep-alive 会检查缓存池中是否存在该组件实例。如果存在,直接从缓存池中取出并重新渲染。如果不存在,才会重新创建组件实例。
- 组件的 VNode 缓存:
- Vue 的 VNode 是 keep-alive 缓存的核心,它在组件切换时,会将 VNode 存入缓存,并在组件重新激活时,使用已缓存的 VNode 来恢复组件。
- keep-alive 通过 include 和 exclude prop 来决定哪些组件应该被缓存或不缓存。
- 生命周期钩子:
- 在 keep-alive 组件中,通常组件不会在卸载时触发 destroyed 钩子,而是触发 deactivated 钩子,这表明该组件已经被缓存,但不再处于活动状态。
- 当组件被再次激活时,activated 钩子会被调用,这意味着组件恢复为活动状态,但不需要重新创建。
keep-alive 缓存了什么?
- 组件的本地状态:如 data 中定义的属性和数据。
- DOM 状态:组件的 DOM 树,包括输入框中的内容、滚动位置等会被保留。
- 组件实例的所有属性:比如响应式属性、计算属性、方法等都不会丢失。
注意事项
- 缓存策略:keep-alive 采用 LRU(Least Recently Used,最近最少使用)缓存策略,即当缓存满了时,最久未使用的组件会被移除。
- 数据变化:如果组件的数据发生变化,缓存中的组件也会随之更新。
- 生命周期钩子:缓存的组件不会触发 created 和 mounted 钩子,只会触发 activated 钩
使用场景
- 路由切换:在单页面应用(SPA)中,使用 keep-alive 缓存路由组件,避免每次切换时重新加载和初始化组件。
- 标签页切换:在多标签页的界面中,使用 keep-alive 缓存每个标签页的内容,避免用户在切换标签页时重新加载数据。
- 性能优化:在性能敏感的场景中,例如列表滚动、动画等,通过缓存组件来避免不必要的渲染,提高性能。
<template>
<div>
<!-- 使用 keep-alive 缓存路由组件 -->
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<!-- 不需要缓存的路由组件 -->
<router-view v-else />
</div>
</template>
LRU策略
LRU 缓存的基本操作
- get(key):如果缓存中存在该 key,则返回对应的值,并将该 key 变为最近使用的(即更新它的顺序)。
- put(key, value):将 key-value 插入缓存。如果缓存已满,则移除最久未使用的 key-value,然后插入新的数据。
常见的数据结构
- 哈希表(Map):用于快速定位缓存中的数据,时间复杂度是 O(1)。
- 双向链表(Doubly Linked List):用于维护元素的使用顺序,链表头表示最近使用的元素,链表尾表示最久未使用的元素,时间复杂度是 O(1)。
LRU 缓存策略的核心思路
- 每次访问缓存时,将访问的节点移到链表的头部,表示它是最近使用的。
- 每次插入数据时,如果缓存已满,则移除链表尾部的节点,表示淘汰最近最少使用的元素。
使用 JavaScript 实现 LRU 缓存
1、简单实现(使用 Map 和 Set)
class LRUCache {
constructor(capacity) {
this.capacity = capacity; // 缓存的最大容量
this.map = new Map(); // 哈希表,用于存储键值对,支持 O(1) 时间复杂度的查找
this.keys = new Set(); // 存储 key 的访问顺序,利用 Set 的特性保持插入顺序
}
// 获取缓存中的值
get(key) {
if (!this.map.has(key)) {
return -1; // 如果 key 不存在,则返回 -1
}
// 更新访问顺序,将当前 key 变为最近使用的
this.keys.delete(key); // 从 Set 中删除当前 key
this.keys.add(key); // 将当前 key 添加到 Set 的末尾(表示最近使用)
return this.map.get(key); // 返回对应的值
}
// 向缓存中插入值
put(key, value) {
// 如果 key 已存在,更新值并调整访问顺序
if (this.map.has(key)) {
this.map.set(key, value); // 更新值
this.keys.delete(key); // 从 Set 中删除当前 key
this.keys.add(key); // 将当前 key 添加到 Set 的末尾(表示最近使用)
return;
}
// 如果缓存已满,移除最近最少使用的元素(Set 中的第一个元素)
if (this.map.size >= this.capacity) {
const leastUsedKey = this.keys.values().next().value; // 获取 Set 中的第一个元素
this.keys.delete(leastUsedKey); // 从 Set 中删除
this.map.delete(leastUsedKey); // 从 Map 中删除
}
// 插入新元素并更新访问顺序
this.map.set(key, value); // 将新元素添加到 Map 中
this.keys.add(key); // 将新元素添加到 Set 的末尾(表示最近使用)
}
}
// 测试
const lru = new LRUCache(2);
lru.put(1, 1); // 缓存: {1=1}
lru.put(2, 2); // 缓存: {1=1, 2=2}
console.log(lru.get(1)); // 返回 1, 缓存顺序更新: {2=2, 1=1}
lru.put(3, 3); // 缓存容量已满,移除最近最少使用的 2, 缓存: {1=1, 3=3}
console.log(lru.get(2)); // 返回 -1(缓存中不存在)
lru.put(4, 4); // 缓存容量已满,移除最近最少使用的 1, 缓存: {3=3, 4=4}
console.log(lru.get(1)); // 返回 -1(缓存中不存在)
console.log(lru.get(3)); // 返回 3
console.log(lru.get(4)); // 返回 4
2、优化实现(使用双向链表)
class Node {
constructor(key, value) {
this.key = key; // 节点的键
this.value = value; // 节点的值
this.prev = null; // 前驱节点
this.next = null; // 后继节点
}
}
class LRUCache {
constructor(capacity) {
this.capacity = capacity; // 缓存的最大容量
this.map = new Map(); // 哈希表,用于快速查找节点
this.head = new Node(0, 0); // 头节点,表示最近使用的节点
this.tail = new Node(0, 0); // 尾节点,表示最久未使用的节点
this.head.next = this.tail; // 初始化双向链表
this.tail.prev = this.head;
}
// 获取缓存中的值
get(key) {
if (!this.map.has(key)) {
return -1; // 如果 key 不存在,则返回 -1
}
const node = this.map.get(key); // 从哈希表中获取节点
this.remove(node); // 将节点从链表中移除
this.add(node); // 将节点添加到链表头部(表示最近使用)
return node.value; // 返回对应的值
}
// 向缓存中插入值
put(key, value) {
if (this.map.has(key)) {
const node = this.map.get(key); // 获取现有节点
node.value = value; // 更新值
this.remove(node); // 将节点从链表中移除
this.add(node); // 将节点添加到链表头部(表示最近使用)
} else {
// 如果缓存已满,移除最近最少使用的节点(尾部节点的前一个节点)
if (this.map.size === this.capacity) {
this.remove(this.tail.prev); // 移除尾部节点的前一个节点
}
const node = new Node(key, value); // 创建新节点
this.add(node); // 将新节点添加到链表头部
}
}
// 将节点添加到链表头部
add(node) {
node.prev = this.head; // 设置新节点的前驱为头节点
node.next = this.head.next; // 设置新节点的后继为头节点的后继
this.head.next.prev = node; // 更新头节点后继的前驱为新节点
this.head.next = node; // 更新头节点的后继为新节点
this.map.set(node.key, node); // 将新节点添加到哈希表中
}
// 从链表中移除节点
remove(node) {
node.prev.next = node.next; // 更新节点前驱的后继为节点的后继
node.next.prev = node.prev; // 更新节点后继的前驱为节点的前驱
this.map.delete(node.key); // 从哈希表中删除节点
}
}
// 测试
const lru = new LRUCache(2);
lru.put(1, 1); // 缓存: {1=1}
lru.put(2, 2); // 缓存: {1=1, 2=2}
console.log(lru.get(1)); // 返回 1, 缓存顺序更新: {2=2, 1=1}
lru.put(3, 3); // 缓存容量已满,移除最近最少使用的 2, 缓存: {1=1, 3=3}
console.log(lru.get(2)); // 返回 -1(缓存中不存在)
lru.put(4, 4); // 缓存容量已满,移除最近最少使用的 1, 缓存: {3=3, 4=4}
console.log(lru.get(1)); // 返回 -1(缓存中不存在)
console.log(lru.get(3)); // 返回 3
console.log(lru.get(4)); // 返回 4
组件通信
父子组件通信
父组件向子组件传递数据:
- 方式:通过 props。
- 原理:父组件通过 props 将数据传递给子组件,子组件通过 props 选项声明接收的属性。
<!-- ParentComponent.vue -->
<template>
<ChildComponent :message="parentMessage" />
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
data() {
return {
parentMessage: 'Hello from Parent',
}
},
}
</script>
<!-- ChildComponent.vue -->
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
props: ['message'],
}
</script>
子组件向父组件传递数据:
- 方式:通过
$emit
。 - 原理:子组件通过
$emit
触发自定义事件,父组件监听这些事件并处理。
<!-- ChildComponent.vue -->
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script>
export default {
methods: {
sendMessage() {
this.$emit('message-sent', 'Hello from Child')
},
},
}
</script>
<!-- ParentComponent.vue -->
<template>
<ChildComponent @message-sent="handleMessage" />
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
methods: {
handleMessage(message) {
console.log(message) // 输出: Hello from Child
},
},
}
</script>
获取父子组件实例:
- 方式:通过
$parent
和$children
。 - 原理:Vue 实例提供了
parent 和
children 属性,可以用来访问父组件或子组件的实例。
使用 ref 获取实例:
- 方式:通过 ref。
- 原理:通过 ref 属性为子组件指定一个引用名称,父组件可以通过这个引用名称访问子组件的实例。
Provide / Inject:
- 方式:通过 provide 和 inject。
- 原理:父组件通过 provide 提供数据,子组件通过 inject 注入数据。适用于跨层级的依赖注入。
兄弟组件通信
Event Bus:
方式:通过全局事件总线。
原理:
- 创建 Event Bus:通过创建一个 Vue 实例作为事件总线,所有组件都可以通过这个实例进行事件的派发和监听。
- 派发事件:使用 EventBus.$emit(eventName, payload) 派发一个事件,其中 eventName 是事件名称,payload 是传递的数据。
- 监听事件:使用 EventBus.$on(eventName, callback) 监听事件,其中 callback 是事件触发时执行的函数。
- 移除事件监听器:在组件销毁时,使用 EventBus.$off(eventName) 移除事件监听器,以避免内存泄漏。
注意事项:
- 性能问题:Event Bus 会导致组件之间的紧耦合,过多的全局事件可能会导致性能问题。
- 内存泄漏:确保在组件销毁时移除事件监听器,避免内存泄漏。
- 替代方案:对于复杂的全局状态管理,可以考虑使用 Vuex(Vue 2)或 Pinia(Vue 3)。
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
<!-- ComponentA.vue -->
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
methods: {
sendMessage() {
EventBus.$emit('message-sent', 'Hello from Component A')
},
},
}
</script>
<!-- ComponentB.vue -->
<template>
<div>{{ message }}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
data() {
return {
message: '',
}
},
created() {
EventBus.$on('message-sent', (message) => {
this.message = message
})
},
}
</script>
Vuex:
- 方式:通过 Vuex 状态管理。
- 原理:通过 Vuex 的全局状态管理来实现跨组件通信。组件可以通过 this.$store 访问和修改状态。
跨级组件通信
Vuex:
- 方式:通过 Vuex 状态管理。
- 原理:与兄弟组件通信类似,通过 Vuex 的全局状态管理来实现跨级组件通信。
$attrs
和 $listeners
:
- 方式:通过
$attrs
和$listeners
。 - 原理:
$attrs
包含了父组件传递给子组件的非 props 属性,$listeners
包含了父组件传递给子组件的事件监听器。子组件可以通过$attrs
和$listeners
将这些属性和事件传递给更深层次的子组件。
<!-- ParentComponent.vue -->
<template>
<ChildComponent :custom-attr="parentMessage" @custom-event="handleEvent" />
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
data() {
return {
parentMessage: 'Hello from Parent',
}
},
methods: {
handleEvent(message) {
console.log(message) // 输出: Hello from Child
},
},
}
</script>
<!-- ChildComponent.vue -->
<template>
<GrandChildComponent v-bind="$attrs" v-on="$listeners" />
</template>
<script>
import GrandChildComponent from './GrandChildComponent.vue'
export default {
components: { GrandChildComponent },
}
</script>
<!-- GrandChildComponent.vue -->
<template>
<div>{{ customAttr }}</div>
<button @click="sendEvent">Send Event</button>
</template>
<script>
export default {
props: ['customAttr'],
methods: {
sendEvent() {
this.$emit('custom-event', 'Hello from GrandChild')
},
},
}
</script>
provide/inject 和 Props区别
特性 | provide/inject | props |
---|---|---|
适用场景 | 用于跨层级的组件通信,特别是在多层级嵌套的组件中。 | 用于父子组件之间的直接通信。 |
数据流向 | 数据从祖先组件流向后代组件,但不需要直接的父子关系。 | 数据从父组件流向子组件。 |
性能影响 | 会增加 Vue 的响应式系统负担,因为它们会创建响应式引用。 | 性能更高,因为它们是直接的父子通信,没有额外的开销。 |
灵活性 | 提供了更大的灵活性,可以跨层级传递数据。 | 适合简单的父子通信,数据流向清晰。 |
响应式 | 提供的数据是响应式的,但需要确保提供的数据是响应式对象(如 ref 或 reactive )。 | props 本身是响应式的,子组件可以监听 props 的变化。 |
使用场景 | 适合提供全局或全局范围内共享的数据,如主题配置、用户信息等。 | 适合在父子组件之间传递数据,确保数据流向清晰。 |
provide/inject
// 祖先组件
export default {
provide() {
return {
theme: this.theme,
user: this.user,
};
},
data() {
return {
theme: 'dark',
user: { name: 'John Doe', age: 30 },
};
},
};
// 后代组件
export default {
inject: ['theme', 'user'],
created() {
console.log(this.theme); // 'dark'
console.log(this.user); // { name: 'John Doe', age: 30 }
},
};
Props
// 父组件
<template>
<child-component :theme="theme" :user="user"></child-component>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent,
},
data() {
return {
theme: 'dark',
user: { name: 'John Doe', age: 30 },
};
},
};
</script>
// 子组件
export default {
props: ['theme', 'user'],
created() {
console.log(this.theme); // 'dark'
console.log(this.user); // { name: 'John Doe', age: 30 }
},
};
如何自定义组件?
- 搭建模板:定义组件的 HTML 结构和样式。
- 定义逻辑:实现组件的内部逻辑,包括数据、方法和生命周期钩子。
- 定义输入:通过 props 接收外部数据。
- 定义输出:通过 $emit 暴露事件。
- 复用和扩展:通过插槽和事件增强组件的灵活性和复用性。
<template>
<!-- 1. 搭建组件的模板 -->
<div class="my-component">
<h2>{{ title }}</h2>
<p>{{ description }}</p>
<button @click="handleClick" class="btn">点击我</button>
<slot name="content"></slot>
</div>
</template>
<script>
export default {
name: 'MyComponent',
// 2. 定义组件的逻辑
// 2.1 定义组件的数据输入(Props)
props: {
title: {
type: String,
required: true
},
description: {
type: String,
default: '这是一个默认描述'
},
isActive: {
type: Boolean,
default: false
}
},
data() {
return {
internalState: false
};
},
// 2.2 定义组件的数据输出(事件)
methods: {
handleClick() {
this.$emit('click', '按钮被点击了');
},
handleUpdate(value) {
this.$emit('update', value);
}
},
// 可选:定义生命周期钩子
mounted() {
console.log('MyComponent mounted');
}
};
</script>
<style scoped>
/* 3. 封装组件的样式 */
.my-component {
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
.btn {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn:hover {
background-color: #45a049;
}
</style>
在父组件中使用封装好的组件
<template>
<div>
<!-- 使用插槽增强组件的灵活性 -->
<my-component
:title="'组件标题'"
:description="'组件描述'"
:is-active="true"
@click="handleComponentClick"
>
<template #content>
<p>这是父组件提供的内容</p>
</template>
</my-component>
</div>
</template>
<script>
import MyComponent from './MyComponent.vue';
export default {
components: {
MyComponent
},
methods: {
handleComponentClick(message) {
console.log('组件点击事件:', message);
}
}
};
</script>
组件的单例模式
在 Vue 中,可以通过 Vuex 作为状态管理工具来实现组件的单例模式。单例模式确保一个类只有一个实例,并提供一个全局访问点。
工作原理
- Vuex Store:Vuex store 用于存储组件实例的状态,确保所有组件都可以访问到同一个实例。
- 组件创建:在组件的 created 钩子中,检查 Vuex 中是否已经存在该组件的实例。如果存在,则使用已存在的实例;如果不存在,则将当前实例存储到 Vuex 中。
- 组件销毁:在组件的 beforeDestroy 钩子中,从 Vuex 中移除组件实例,避免内存泄漏。
- 状态更新:通过 Vuex 的 mutations 更新组件实例的状态,确保所有实例共享同一个状态。
注意事项
- 性能:单例模式可以减少组件的创建和销毁开销,但需要确保在不需要时及时移除实例。
- 状态管理:使用 Vuex 管理状态可以确保组件之间的状态一致性,但需要合理设计状态结构。
- 适用场景:单例模式适用于需要全局共享状态或行为的组件,如通知中心、用户信息组件等。
// store.js
// 首先,创建一个 Vuex store,用于管理组件实例的状态。
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
componentInstances: {}, // 存储组件实例
},
mutations: {
setComponentInstance(state, { name, instance }) {
state.componentInstance[name] = instance;
},
removeComponentInstance(state, name) {
delete state.componentInstance[name];
},
},
getters: {
getComponentInstance: (state) => (name) => {
return state.componentInstance[name];
},
},
});
{/* 创建一个需要作为单例的组件,并在组件的 created 钩子中检查是否已经存在实例。 */}
<template>
<div>
<h1>单例组件</h1>
<p>{{ message }}</p>
<button @click="updateMessage">更新消息</button>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
export default {
name: 'SingletonComponent',
data() {
return {
message: '这是单例组件的初始消息',
};
},
computed: {
...mapState({
componentInstances: (state) => state.componentInstance,
}),
},
created() {
// 检查是否已经存在实例
if (this.componentInstance[this.name]) {
// 如果存在,使用已存在的实例
Object.assign(this, this.componentInstance[this.name]);
} else {
// 如果不存在,将当前实例存储到 Vuex 中
this.setComponentInstance({
name: this.name,
instance: this,
});
}
},
beforeDestroy() {
// 在组件销毁时,从 Vuex 中移除实例
this.removeComponentInstance(this.name);
},
methods: {
...mapMutations(['setComponentInstance', 'removeComponentInstance']),
updateMessage() {
this.message = '消息已更新';
},
},
};
</script>
{/* 在父组件中,多次使用单例组件,确保它们共享同一个实例。 */}
<template>
<div>
<h1>父组件</h1>
<singleton-component></singleton-component>
<singleton-component></singleton-component>
<singleton-component></singleton-component>
</div>
</template>
<script>
import SingletonComponent from './SingletonComponent.vue';
export default {
components: {
SingletonComponent,
},
};
</script>
Vue中组件复用的方式
组件组合
通过父子组件组合来实现复用,将逻辑划分为更小的组件,以便在不同场景下重复使用。
<template>
<div>
<h1>父组件</h1>
<child-component :message="parentMessage" @child-event="handleChildEvent"></child-component>
</div>
</template>
<script>
// 子组件
const ChildComponent = {
props: ['message'],
template: `
<div>
<h2>子组件</h2>
<p>{{ message }}</p>
<button @click="$emit('child-event', '事件从子组件传递到父组件')">触发事件</button>
</div>
`,
};
export default {
components: {
ChildComponent,
},
data() {
return {
parentMessage: '这是父组件传递给子组件的消息',
};
},
methods: {
handleChildEvent(data) {
console.log('子组件传递的数据:', data);
},
},
};
</script>
插槽(Slots)
插槽允许父组件向子组件传递结构化内容,v-slot 是 Vue 3 中的统一插槽语法。插槽有默认插槽和具名插槽,具名插槽允许传递多个不同位置的内容。
<template>
<div>
<h1>使用插槽的父组件</h1>
<slot-component>
<template v-slot:header>
<h2>这是头部内容</h2>
</template>
<p>这是默认插槽的内容</p>
<template v-slot:footer>
<p>这是底部内容</p>
</template>
</slot-component>
</div>
</template>
<script>
// 子组件
const SlotComponent = {
template: `
<div>
<div class="header">
<slot name="header"></slot>
</div>
<div class="content">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
`,
};
export default {
components: {
SlotComponent,
},
};
</script>
Mixin
Mixin 是一种复用组件逻辑的方式,允许在多个组件之间共享通用的功能。通过定义 Mixin 并在组件中引入,可以复用特定的行为逻辑。不过,Mixin 可能带来命名冲突,使用时要注意。
<template>
<div>
<h1>使用 Mixin 的组件</h1>
<p>{{ mixinData }}</p>
<button @click="mixinMethod">调用 Mixin 中的方法</button>
</div>
</template>
<script>
// 定义 Mixin
const MyMixin = {
data() {
return {
mixinData: '这是 Mixin 中的数据',
};
},
methods: {
mixinMethod() {
console.log('这是 Mixin 中的方法');
},
},
};
export default {
mixins: [MyMixin],
mounted() {
console.log('组件挂载,Mixin 数据:', this.mixinData);
},
};
</script>
自定义指令
通过自定义指令,可以为多个组件或元素复用某些行为逻辑,例如点击外部关闭等功能。
<template>
<div>
<h1>自定义指令示例</h1>
<!-- 使用自定义指令 v-focus -->
<input v-focus placeholder="自动聚焦的输入框" />
<!-- 使用带参数的自定义指令 -->
<div v-highlight:blue="'highlight'">这个文本会被高亮显示</div>
<!-- 使用修饰符 -->
<div v-highlight.once:blue="'highlight'">这个文本只会被高亮一次</div>
</div>
</template>
<script>
export default {
directives: {
// 定义局部自定义指令
highlight: {
// 指令的定义
created(el, binding, vnode) {
console.log('highlight 指令已创建', el);
},
mounted(el, binding) {
// 获取参数和修饰符
const { value, arg, modifiers } = binding;
if (modifiers.once) {
console.log('highlight 指令只会执行一次');
}
el.style.backgroundColor = value;
},
updated(el, binding) {
console.log('highlight 指令已更新', el);
},
},
},
mounted() {
console.log('组件已挂载');
},
};
</script>
带参数和修饰符的自定义指令:
<template>
<div>
<h1>带参数和修饰符的自定义指令</h1>
<!-- 带参数和修饰符 -->
<div v-highlight:blue.once="'highlight'">这个文本会被高亮显示一次</div>
<!-- 动态参数 -->
<div v-highlight:[color]="'highlight'">这个文本的颜色由 data 中的 color 决定</div>
</div>
</template>
<script>
export default {
data() {
return {
color: 'red',
};
},
directives: {
highlight: {
mounted(el, binding) {
const { value, arg, modifiers } = binding;
if (modifiers.once) {
console.log('highlight 指令只会执行一次');
}
el.style.backgroundColor = value;
},
},
},
};
</script>
生命周期钩子:
- created:指令绑定到元素时调用。
- beforeMount:在绑定元素的父组件挂载之前调用。
- mounted:在绑定元素的父组件挂载之后调用。
- beforeUpdate:在绑定元素的父组件更新之前调用。
- updated:在绑定元素的父组件更新之后调用。
- beforeUnmount:在绑定元素的父组件卸载之前调用。
- unmounted:在绑定元素的父组件卸载之后调用。
高阶组件(HOC)
高阶组件是一种工厂函数,接收一个组件作为参数并返回一个增强后的组件,通常用于逻辑复用。
<template>
<div>
<h1>使用高阶组件</h1>
<enhanced-component></enhanced-component>
</div>
</template>
<script>
// 原始组件
const OriginalComponent = {
template: `
<div>
<h2>原始组件</h2>
<p>{{ message }}</p>
</div>
`,
data() {
return {
message: '这是原始组件的内容',
};
},
};
// 高阶组件
const withLogging = (WrappedComponent) => {
return {
components: {
WrappedComponent,
},
template: `
<div>
<WrappedComponent></WrappedComponent>
</div>
`,
mounted() {
console.log('高阶组件已挂载');
},
};
};
// 增强后的组件
const EnhancedComponent = withLogging(OriginalComponent);
export default {
components: {
EnhancedComponent,
},
};
</script>
Vue组件中的this
this 是一个非常重要的关键字,用于在组件的上下文中引用组件实例本身。
this 可以在组件的实例方法、生命周期钩子、计算属性等上下文中使用,以访问组件的 data、methods、props 等属性和方法。
使用场景
- 实例方法:在组件的方法中使用 this 访问组件的属性和方法。
- 生命周期钩子:在生命周期钩子中使用 this 初始化或清理资源。
- 计算属性:在计算属性中使用 this 访问 data 或 props 中的值。
<template>
<div>
<h1>{{ message }}</h1>
<p>计数: {{ count }}</p>
<button @click="incrementCount">增加计数</button>
<p>双倍计数: {{ doubleCount }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue!',
count: 0,
};
},
methods: {
// 实例方法
incrementCount() {
this.count++; // 使用 this 访问 count
console.log('计数已增加到:', this.count);
},
},
created() {
// 生命周期钩子
console.log('组件已创建,message:', this.message);
},
computed: {
// 计算属性
doubleCount() {
return this.count * 2; // 使用 this 访问 count
},
},
beforeDestroy() {
// 生命周期钩子
console.log('组件即将销毁,count:', this.count);
},
};
</script>
箭头函数中的 this
在箭头函数中,this 不会绑定到组件实例,而是继承外层的 this 值。因此,在需要访问组件实例的上下文中,避免使用箭头函数。
methods: {
// 避免使用箭头函数
incrementCount: () => {
console.log(this.count); // this 不会指向组件实例
},
},
在 Vue2 中检测数组的变化?
由于 Object.defineProperty 的限制,Vue 无法直接监听数组的索引或 length 属性的变化,因此 Vue 通过劫持数组的方法来实现对数组变化的响应式处理。
函数劫持
Vue 2.x 通过劫持数组的常用方法(如 push、pop、shift、unshift、splice、sort、reverse 等),重写了这些方法,使得它们能够触发响应式更新。
反过来说:这些方法会直接修改数组,并触发视图更新。
原型链重写
Vue 将 data 中的数组的原型链指向了自己定义的数组原型方法。这样,当调用数组的 API 时,Vue 可以拦截这些操作并通知依赖更新。
递归遍历
如果数组中包含引用类型(如对象或数组),Vue 会对这些引用类型再次递归遍历并进行监控,从而实现深度响应式。
简易实现:
// 定义一个响应式数组的原型方法
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
// 重写数组方法
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
methodsToPatch.forEach((method) => {
const original = arrayProto[method]
arrayMethods[method] = function (...args) {
// 调用原生方法
const result = original.apply(this, args)
// 触发响应式更新
console.log(`Array method called: ${method}`, args)
// 返回原生方法的结果
return result
}
})
// 创建一个响应式数组
function createReactiveArray(arr) {
arr.__proto__ = arrayMethods
arr.forEach((item, index) => {
// 递归遍历数组中的引用类型
if (typeof item === 'object') {
arr[index] = makeReactive(item)
}
})
return arr
}
// 递归遍历对象
function makeReactive(obj) {
if (Array.isArray(obj)) {
return createReactiveArray(obj)
} else {
// 对象响应式处理(简化版)
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
return obj
}
}
// 定义响应式属性
function defineReactive(obj, key, value) {
if (typeof value === 'object') {
value = makeReactive(value)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`Getting ${key}`)
return value
},
set(newValue) {
console.log(`Setting ${key} to ${newValue}`)
value = newValue
},
})
}
// 示例
const data = {
name: 'Vue 2',
list: [1, 2, { nested: 'object' }],
}
makeReactive(data)
data.list.push(3) // 输出: Array method called: push [3]
data.list[2].nested = 'updated' // 输出: Setting nested to updated
defineProperty和proxy区别?
性能问题:初始化遍历对象所有 key
- Vue 2 (Object.defineProperty):
- 必须遍历每一个属性进行 getter/setter 劫持。
- 初始化时需要递归遍历所有嵌套对象的属性,性能开销大。
- Vue 3 (Proxy):
- 直接代理整个对象,而非每个属性。
- 初始化时无需递归遍历所有嵌套对象的属性,只需代理一次对象。
优化策略: 按需响应式:Vue 3 在访问具体属性时才递归生成响应式对象,而非初始化时立即遍历整个对象树。这大大减少了性能开销,尤其在深层嵌套结构时,性能提升更为明显。
Dep 和 Watcher 占用内存较多
- Vue 2 (Object.defineProperty):需要为每个属性维护 dep 实例和 watcher 实例,额外占用较多内存。
- Vue 3 (Proxy):依赖追踪和更新通知机制优化,使用更轻量的 Effect 机制。
优化策略: 使用 Effect 和 Reactive 管理依赖:Vue 3 使用 Effect 机制管理响应式数据依赖,取代了 Vue 2 的 watcher,并通过一个全局的 ReactiveEffect 实例追踪所有响应式的依赖关系,减少了内存占用。
无法监听数组元素变化
- Vue 2 (Object.defineProperty):无法直接监听数组元素的变化,需通过重写数组操作方法(如 push、pop 等)进行拦截。
- Vue 3 (Proxy):可直接监听数组的操作,无需手动劫持数组方法。
优化策略: 全方位监听:Proxy 能直接监听数组长度、索引变动及元素操作(如 push、pop 等),无需额外操作重写,减少代码复杂度和潜在性能损耗。
动态新增/删除属性的监听
- Vue 2 (Object.defineProperty):动态新增或删除属性无法被劫持,需通过 Vue.set 和 Vue.delete 方法显式操作对象。
- Vue 3 (Proxy):可直接捕获动态属性的添加和删除,无需额外的 API 调用。
优化策略: 动态属性响应式:Proxy 天然支持对动态属性(包括数组项)新增和删除的响应式处理,无需使用特定 API,简化了开发体验。
不支持复杂数据结构(如 Map、Set)
- Vue 2 (Object.defineProperty):-无法处理复杂对象如 Map 和 Set,这类数据结构无法被响应式处理。
- Vue 3 (Proxy):支持所有原生 JavaScript 数据结构,包括 Map、Set 等。
优化策略:支持复杂数据结构:Vue 3 的响应式系统能够很好地处理 Map、Set 等复杂数据结构,确保其变化可以被追踪和响应,适用于复杂应用场景(如管理缓存或列表等)。
理解MVVM
MVVM(Model-View-ViewModel) 是一种设计模式,用于将用户界面(View)与业务逻辑(Model)分离,通过一个中间层(ViewModel)来管理数据绑定和交互。
它是 MVC(Model-View-Controller)模式的演变,将 Controller 替换为 ViewModel,以更好地支持数据绑定和响应式编程。
Model(模型层)
代表数据模型,是应用程序的核心数据和业务逻辑。
作用:
- 存储和管理应用程序的数据。
- 提供数据的增删改查等操作。
- 不直接与用户界面交互,而是通过 ViewModel 进行数据同步。
View(视图层)
定义:代表用户界面,是用户与应用程序交互的部分。
作用:
- 显示数据,即 Model 层的数据。
- 捕获用户输入(如点击按钮、输入文本等)。
- 通过数据绑定机制与 ViewModel 层进行交互。
- 视图的变化会通知 ViewModel 层更新数据。
ViewModel(视图模型层)
定义:是 View 和 Model 之间的桥梁,负责管理数据绑定和交互逻辑。
作用:
- 将 Model 层的数据绑定到 View 层。
- 处理 View 层的用户输入,更新 Model 层的数据。
- 监听 Model 层的数据变化,并将变化同步到 View 层。
- 提供数据转换和格式化功能,以满足视图的显示需求。
graph TD
A[用户操作 View] -->|触发事件| B[ViewModel 捕获事件]
B -->|更新 Model 数据| C[Model 数据变化]
C -->|通知 ViewModel| D[ViewModel 检测到数据变化]
D -->|更新 View 数据| E[View 更新显示]
E -->|用户看到更新| A
C -->|数据变化| D
D -->|同步数据到 View| E
Vue生命周期
vue的生命周期有哪些及每个生命周期做了什么?
初始化阶段
beforeCreate
- 触发时机:实例初始化后,data 和 methods 还未初始化。
- 特点:无法访问 data 和 methods,适合初始化全局变量。
created
- 触发时机:实例创建完成后,data 和 methods 已初始化。
- 特点:可以访问 data 和 methods,但无法访问 DOM。适合获取初始数据,更改数据不会触发 updated。适合进行初始数据的获取,例如从后端 API 获取数据。
挂载阶段
beforeMount
- 触发时机:挂载开始之前,模板已编译。
- 特点:虚拟 DOM 已创建,即将渲染。可以更改数据,不会触发 updated。
mounted
- 触发时机:挂载完成后,真实 DOM 已挂载。
- 特点:可以访问真实 DOM,适合操作 DOM 和绑定事件监听器。
更新阶段
beforeUpdate
- 触发时机:响应式数据更新,虚拟 DOM 重新渲染之前。
- 特点:可以更改数据,不会触发额外的重渲染。例如缓存旧数据。
updated
- 触发时机:DOM 更新完成后。
- 特点:DOM 已更新,适合进行 DOM 操作。避免在此更改数据,防止无限循环。
销毁阶段
beforeDestroy
- 触发时机:实例销毁之前。
- 特点:实例仍可使用,适合进行善后工作,如清除计时器。
destroyed
- 触发时机:实例销毁之后。
- 特点:组件已完全销毁,无法再进行操作。
setup
│
├─ onBeforeMount
│ ↓
├─ onMounted
│ ↓
├─ onBeforeUpdate
│ ↓
├─ onUpdated
│ ↓
├─ onBeforeUnmount
│ ↓
├─ onUnmounted
使用场景
- onMounted:用于在组件挂载后执行 DOM 操作。
- onUnmounted:用于在组件销毁前清理资源,如定时器、事件监听器等。
- onBeforeUnmount:用于在组件销毁前执行一些清理逻辑。
- onUpdated:用于在组件更新后执行一些逻辑,如重新计算布局等。
生命周期钩子执行顺序
Vue 的父组件和子组件生命周期钩子函数执行顺序。
加载渲染过程
- 父组件的 beforeCreate、created、beforeMount 钩子依次执行。
- 子组件的 beforeCreate、created、beforeMount、mounted 钩子依次执行。
- 最后父组件的 mounted 钩子执行。
graph TD
A[父 beforeCreate] --> B[父 created]
B --> C[父 beforeMount]
C --> D[子 beforeCreate]
D --> E[子 created]
E --> F[子 beforeMount]
F --> G[子 mounted]
G --> H[父 mounted]
子组件更新过程
- 父组件的 beforeUpdate 钩子执行。
- 子组件的 beforeUpdate 和 updated 钩子依次执行。
- 最后父组件的 updated 钩子执行。
graph TD
A[父 beforeUpdate] --> B[子 beforeUpdate]
B --> C[子 updated]
C --> D[父 updated]
销毁过程
- 父组件的 beforeDestroy 钩子执行。
- 子组件的 beforeDestroy 和 destroyed 钩子依次执行。
- 最后父组件的 destroyed 钩子执行。
graph TD
A[父 beforeDestroy] --> B[子 beforeDestroy]
B --> C[子 destroyed]
C --> D[父 destroyed]
父组件监听子组件生命周期钩子
@hook
语法:适用于需要父组件监听子组件生命周期钩子的场景,简化了代码,减少了子组件中的 $emit 调用。
Parent.vue
<template>
<div>
<Child @hook:mounted="doSomething" />
</div>
</template>
<script>
import Child from './Child.vue';
export default {
components: {
Child
},
methods: {
doSomething() {
console.log('父组件监听到 mounted 钩子函数...');
}
}
};
</script>
Child.vue
<template>
<div>
<p>子组件内容</p>
</div>
</template>
<script>
export default {
mounted() {
console.log('子组件触发 mounted 钩子函数...');
}
};
</script>
Ajax请求放在哪个生命周期?
在 Vue 中,接口请求通常可以放在 created、beforeMount 和 mounted 生命周期钩子中。然而,推荐在 created 钩子中进行接口请求。
原因
- 更快获取数据,减少页面加载时间,提升用户体验。
- 确保代码一致性,在服务端渲染(SSR)场景中,created 钩子可以正常工作。
- 避免页面闪屏问题,在模板渲染之前就准备好数据,避免用户看到空白或加载状态。
Vue的响应式原理
Vue 2 的响应式原理
Object.defineProperty 来实现响应式数据。以下是其核心原理:
数据劫持:
- 在初始化数据时,Vue 使用 Object.defineProperty 重新定义 data 中的所有属性。
- 通过 get 和 set 方法拦截对属性的访问和修改。
依赖收集:
- 当页面使用某个响应式属性时,get 方法会被触发,进行依赖收集(将当前组件的 watcher 收集起来)。
- 如果属性发生变化,set 方法会被触发,通知相关依赖进行更新操作(发布订阅模式)。
Vue 3 的响应式原理
Vue 3 使用 Proxy 代替 Object.defineProperty 来实现响应式数据。以下是其核心原理:
Proxy 代理:
- Proxy 可以直接监听对象和数组的变化,并且提供了多达 13 种拦截方法(如 get、set、deleteProperty 等)。
- Proxy 是 ES6 的新标准,浏览器厂商会对其进行持续的性能优化。
深度代理:
- Proxy 只会代理对象的第一层。为了实现深度代理,Vue 3 在 get 操作中判断返回值是否为对象,如果是,则通过 reactive 方法对返回值进行代理。这样就实现了深度响应式。
性能优化:
- 在处理数组时,可能会触发多次 get 和 set 操作。为了避免不必要的触发,Vue 3 在 set 操作中进行了优化:
- 判断 key 是否为当前被代理对象 target 的自身属性。
- 判断旧值与新值是否相等。
- 只有满足以上两个条件之一时,才执行 trigger 操作。
Vue 的 $destroy
用于手动销毁 Vue 实例。这个方法会清理相关的事件监听器和所有子组件,确保实例被正确地从内存中移除,避免内存泄漏。
$destroy 的作用
- 销毁实例:将 Vue 实例从挂载状态变为未挂载状态。
- 清理事件监听:移除实例上的所有事件监听器。
- 销毁子组件:递归地销毁所有子组件。
- 移除响应式系统:停止数据的响应式系统,不再追踪数据变化。
- 生命周期影响:触发 beforeDestroy 和 destroyed 生命周期钩子。
<template>
<div>
<h1>Vue $destroy 示例</h1>
<button @click="destroyInstance">销毁实例</button>
</div>
</template>
<script>
export default {
data() {
return {
message: '这是一个 Vue 实例',
};
},
methods: {
destroyInstance() {
this.$destroy();
console.log('实例已销毁');
},
},
beforeDestroy() {
console.log('beforeDestroy 钩子已触发');
},
destroyed() {
console.log('destroyed 钩子已触发');
},
};
</script>
工作原理
- 销毁实例:调用 $destroy 方法后,Vue 实例从挂载状态变为未挂载状态。
- 清理事件监听:实例上的所有事件监听器都会被移除。
- 销毁子组件:递归地销毁所有子组件。
- 移除响应式系统:停止数据的响应式系统,不再追踪数据变化。
- 生命周期钩子:beforeDestroy 和 destroyed 钩子会被依次调用。
注意事项
- 手动销毁:通常情况下,Vue 会自动管理实例的生命周期,但在某些情况下(如动态组件或复杂的组件树),可能需要手动销毁实例。
- 性能优化:通过手动销毁不再需要的实例,可以避免不必要的内存占用和性能开销。
- 避免重复销毁:确保不要多次调用 $destroy 方法,避免不必要的操作。
响应式引用
响应式引用是一种机制,它允许 Vue 跟踪数据的变化,并在数据变化时自动更新视图。
Vue 通过 Proxy(在 Vue 3 中)或 Object.defineProperty(在 Vue 2 中)来实现这种响应式机制。
工作原理
- 数据包装:使用 ref 或 reactive 等函数将普通数据包装成响应式数据。
- 依赖收集:当组件首次渲染时,Vue 会收集所有依赖于该数据的视图或计算属性。
- 数据变化检测:当数据发生变化时,Vue 会检测到这些变化。
- 视图更新:Vue 会自动更新所有依赖于该数据的视图或计算属性。
响应式引用的创建
1、ref:用于创建单个值的响应式引用。
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // 0
count.value = 1; // 响应式更新
console.log(count.value); // 1
2、reactive:用于创建深度响应式的对象。
import { reactive } from 'vue';
const state = reactive({
name: 'Alice',
age: 30
});
state.age = 31; // 响应式更新
3、shallowRef:用于创建浅层响应式引用,仅引用本身响应式。
import { shallowRef } from 'vue';
const user = shallowRef({ name: 'Alice', age: 30 });
user.value = { name: 'Bob', age: 25 }; // 响应式更新
user.value.age = 31; // 不会响应式更新
4、shallowReactive:用于创建浅层响应式对象,仅顶层属性响应式。
import { shallowReactive } from 'vue';
const state = shallowReactive({
user: { name: 'Alice', age: 30 },
items: ['apple', 'banana']
});
state.items.push('orange'); // 响应式更新
state.user.age = 31; // 不会响应式更新
Vue 2 和Vue 3的区别
源码组织方式变化
- Vue 2:使用 JavaScript 编写,源码较为复杂。
- Vue 3:使用 TypeScript 重写,源码更加清晰,类型安全,便于维护和扩展。
支持 Composition API
- Vue 2:使用 Options API,组件逻辑通过 data、methods、computed 等选项组织。
- Vue 3:引入 Composition API,基于函数的 API,更加灵活地组织组件逻辑。通过 setup 函数,可以将逻辑拆分为可复用的函数。
响应式系统提升
- Vue 2:使用 Object.defineProperty 实现响应式,只能监听对象的属性,不能监听动态新增或删除的属性,数组变化需要特殊处理。
- 初始化时性能开销较大,尤其是对象嵌套较深时。
- 无法监听动态新增的属性或删除的属性。
- 数组的响应式依赖于对数组方法(如 push、pop 等)的重写,无法直接监听索引或 length 属性的变化。
- Vue 3:使用 Proxy 实现响应式,可以监听动态新增或删除的属性,以及数组的变化。性能优化更好,支持深度响应式。
- 性能优化:Proxy 不需要在初始化时递归遍历所有属性,而是按需拦截属性的访问和修改,减少了初始化时的性能开销。
- 动态监听:Proxy 可以直接监听动态新增的属性、删除的属性,以及数组的索引和 length 属性。
- 更灵活的响应式:通过 reactive 和 ref 两种方式实现响应式,开发者可以根据需求选择适合的 API。
编译优化
- Vue 2:通过标记静态根节点优化 diff,减少不必要的比较。
- Vue 3:标记和提升所有静态节点,diff 时只对比动态节点内容,优化了渲染性能。
- 静态提升:在编译时标记静态节点,跳过对静态节点的比较,只对比动态节点。
- patch flag:为动态节点添加 patch flag,标记节点的更新类型(如 text、class 等),减少不必要的 diff 操作。
- 事件缓存:缓存事件处理函数,避免重复生成。
- Fragments:允许模板中直接使用多个同级节点,而不需要包裹在一个根节点中,减少了 DOM 操作的开销。
打包体积优化
- Vue 2:包含一些不常用的 API(如 inline-template、filter)。
- Vue 3:移除了一些不常用的 API,减小了打包体积。
- Tree-shaking:支持模块化,未使用的功能不会被打包到最终代码中。
- 移除冗余 API:例如移除了 inline-template 和 filter,推荐使用更现代的替代方案(如计算属性)。
生命周期的变化
- Vue 2:生命周期钩子包括 beforeCreate、created、beforeMount、mounted 等。
- Vue 3:引入 setup 函数,替代了 beforeCreate 和 created。setup 是 Composition API 的入口,用于初始化响应式数据和方法。
Template 模板支持
- Vue 2:模板必须有一个根标签。
- Vue 3:模板支持多个根标签,更加灵活。
Vuex 状态管理
- Vue 2:通过
new Vuex.Store
创建 Vuex 实例。 - Vue 3:通过 createStore 创建 Vuex 实例,更加灵活。
路由获取页面实例与路由信息
- Vue 2:通过
this.$router
和this.$route
获取路由实例和路由信息。 - Vue 3:通过 getCurrentInstance 获取当前组件实例,通过 useRoute 和 useRouter 获取路由信息和路由实例。
Props 的使用变化
- Vue 2:通过 this.props 获取 props 的内容。
- Vue 3:直接通过 props 参数获取 props 的内容,更加直观。
父子组件传值
- Vue 2:通过 this.$emit 向父组件传递数据。
- Vue 3:在向父组件传递数据时,如果使用了自定义事件名称(如 backData),需要在 emits 中定义。
Vue 2.x 的 Diff 算法
Vue 2.x 的 Diff 算法主要基于 Snabbdom,它是一种高效的虚拟 DOM 比对库。Vue 2 的 Diff 算法的核心步骤如下:
同级比较:
仅在同一层级比较节点,不跨层级。
双端比较:
通过四个指针(旧前、旧后、新前、新后)进行对比:
- 旧前 vs 新前
- 旧后 vs 新后
- 旧前 vs 新后
- 旧后 vs 新前
Key 的作用:
通过 key 识别节点,优化复用
缺点
- 全量比较:即使只有部分变化,也会重新遍历整个 VNode 树。
- 无法充分利用静态节点优化:静态节点也会被重新创建和比较
Vue 3.x 的 Diff 算法
静态标记与 PatchFlag
- 在编译阶段给 VNode 添加 PatchFlag,用于标记节点的更新类型。
- 静态节点在编译时被标记,运行时直接复用,无需重新创建与对比。
最长递增子序列(LIS)优化
- 通过 getSequence 函数(源码中使用二分法优化)生成 LIS 索引数组。
- 保持顺序的节点(LIS 中的元素)无需移动。
- 非 LIS 元素按序插入对应的正确位置,最小化 DOM 操作次数。
Block Tree
- 将动态节点组织为树结构,减少比较范围。
性能优化
- 时间复杂度从 O(n^2) 降低到接近 O(n)。
- 更智能的节点移动处理,显著减少 DOM 操作。
优点
- 更高效,减少不必要的比较。
- 更好的静态节点优化。
- 更智能的节点移动处理
flowchart TD
A[新旧子节点数组] --> B{头头对比}
B --相同--> C[指针右移]
B --不同--> D{尾尾对比}
D --相同--> E[指针左移]
D --不同--> F{新头旧尾}
F --相同--> G[移动节点]
F --不同--> H{旧头新尾}
H --相同--> I[移动节点]
H --不同--> J[遍历剩余节点]
J --> K[生成最长递增子序列]
K --> L[最小移动操作]
// 核心 Diff 函数(伪代码)
function diff(oldChildren, newChildren, parentEl) {
let oldStartIdx = 0, oldEndIdx = oldChildren.length - 1
let newStartIdx = 0, newEndIdx = newChildren.length - 1
let oldStartVNode = oldChildren, oldEndVNode = oldChildren[oldEndIdx]
let newStartVNode = newChildren, newEndVNode = newChildren[newEndIdx]
// 1. 双端对比
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isSameVNode(oldStartVNode, newStartVNode)) {
// 头头相同:复用节点
patchVNode(oldStartVNode, newStartVNode)
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (isSameVNode(oldEndVNode, newEndVNode)) {
// 尾尾相同:复用节点
patchVNode(oldEndVNode, newEndVNode)
oldEndVNode = oldChildren[--oldEndIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (isSameVNode(oldStartVNode, newEndVNode)) {
// 旧头与新尾相同:移动节点到末尾
parentEl.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
patchVNode(oldStartVNode, newEndVNode)
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (isSameVNode(oldEndVNode, newStartVNode)) {
// 旧尾与新头相同:移动节点到头部
parentEl.insertBefore(oldEndVNode.el, oldStartVNode.el)
patchVNode(oldEndVNode, newStartVNode)
oldEndVNode = oldChildren[--oldEndIdx]
newStartVNode = newChildren[++newStartIdx]
} else {
// 进入复杂 Diff 逻辑(最长递增子序列优化)
break
}
}
// 2. 处理剩余节点
if (oldStartIdx > oldEndIdx) {
// 新增节点
addNewNodes(newChildren, newStartIdx, newEndIdx, parentEl)
} else if (newStartIdx > newEndIdx) {
// 删除旧节点
removeOldNodes(oldChildren, oldStartIdx, oldEndIdx, parentEl)
} else {
// 3. 最长递增子序列优化(核心优化点)
const newIndexMap = createKeyToNewIndexMap(newChildren, newStartIdx, newEndIdx)
const lis = getLongestIncreasingSubsequence(newIndexMap)
let lisIndex = 0
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const oldVNode = oldChildren[i]
if (oldVNode === null) continue
const newIndex = newIndexMap.get(oldVNode.key)
if (newIndex === undefined) {
// 删除不存在节点
parentEl.removeChild(oldVNode.el)
} else {
if (lis[lisIndex] === newIndex) {
// 位置无需移动
lisIndex++
} else {
// 移动节点到正确位置
const anchor = newChildren[newIndex - 1].el.nextSibling
parentEl.insertBefore(oldVNode.el, anchor)
}
// 递归更新子节点
patchVNode(oldVNode, newChildren[newIndex])
}
}
}
}
// 判断是否为相同 VNode
function isSameVNode(a, b) {
return a.key === b.key && a.tag === b.tag
}
// 生成最长递增子序列(贪心 + 二分)
function getLongestIncreasingSubsequence(arr) {
// ... 具体实现省略(Vue 3 使用 O(n log n) 算法)
}
优化项 | Vue 2.x | Vue 3.x |
---|---|---|
时间复杂度 | O(n^2) | 接近 O(n) |
DOM 移动次数 | 较多 | 显著减少 |
静态处理 | 无 | 静态提升跳过 Diff |
动态列表性能 | 较差 | 优异 |
Vue3虚拟Dom
虚拟 DOM 是真实 DOM 的轻量级 JavaScript 对象抽象,通过 { tag, props, children }
结构描述 DOM 节点。
其核心价值在于减少直接操作真实 DOM 的性能损耗,通过 Diff 算法计算最小更新范围,实现高效渲染
VNode 结构
- tag:标签名或组件类型
- props:属性和事件(如 class、id、onClick)
- children:子节点(文本或嵌套 VNode 数组)
const vnode = {
tag: 'div',
props: { class: 'container' },
children: [{ tag: 'h1', children: '标题' }]
}
虚拟 DOM 的工作流程
- 初始化阶段
模板编译:将模板转换为 render() 函数,生成初始 VNode 树18。
挂载:通过 mount() 将 VNode 转换为真实 DOM 并渲染到页面8。
响应式更新阶段
- 数据变化触发 render() 重新执行,生成新 VNode 树16。
- Diff 算法:对比新旧 VNode 树,识别差异节点(Vue 3 采用双端 Diff 优化,减少比对次数)14。
Patch 阶段
- 靶向更新:根据 Diff 结果,仅更新变化的真实 DOM 节点(通过 PatchFlags 标记动态属性,如 TEXT、CLASS)46。
- 异步队列:批量处理 DOM 更新,避免频繁重排重绘
Vue 3 的优化策略
静态提升(Static Hoisting)
- 将静态节点(无动态绑定的内容)提升到渲染函数外,避免重复生成18。
- 示例:模板中的固定标题
<h1>Hello</h1>
仅生成一次 VNode8。
靶向更新(Patch Flags)
- 动态标记(如 TEXT=1、CLASS=2)精准定位需更新的属性,跳过无变化部分36。
- 对比 Vue 2 的全量 Diff,性能提升 30%-50%4。
惰性挂载与 Tree Shaking
- 按需生成组件 VNode,减少内存占用1。
- 配合编译时优化,剔除未使用的代码
简化实现
// 1. 定义 VNode 结构
class VNode {
constructor(tag, props, children) {
this.tag = tag // 标签名(如 'div')
this.props = props || {}// 属性对象(如 { class: 'box' })
this.children = children||[] // 子节点数组
}
}
// 2. 创建虚拟DOM
function createElement(tag, props, children) {
return new VNode(tag, props, children)
}
// 3. 将虚拟DOM渲染为真实DOM
function render(vnode) {
if (typeof vnode === 'string') {
return document.createTextNode(vnode) // 文本节点
}
const el = document.createElement(vnode.tag)
// 添加属性
for (const key in vnode.props) {
el.setAttribute(key, vnode.props[key])
}
// 递归渲染子节点
vnode.children.forEach(child => {
el.appendChild(render(child))
})
return el
}
// 4. Diff算法核心(简化版)
function diff(oldVNode, newVNode) {
// 类型不同直接替换
if (oldVNode.tag !== newVNode.tag) {
return { type: 'REPLACE', node: newVNode }
}
// 属性变化
const propsPatches = {}
const oldProps = oldVNode.props
const newProps = newVNode.props
// 检测新增/修改属性
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
propsPatches[key] = newProps[key]
}
}
// 检测删除属性
for (const key in oldProps) {
if (!(key in newProps)) {
propsPatches[key] = undefined
}
}
// 子节点Diff(仅示例层级比较)
const childPatches = []
const len = Math.max(oldVNode.children.length, newVNode.children.length)
for (let i = 0; i < len; i++) {
childPatches.push(diff(oldVNode.children[i], newVNode.children[i]))
}
return {
type: 'UPDATE',
props: propsPatches,
children: childPatches
}
}
// 5. 应用补丁更新DOM
function patch(parent, patches, index = 0) {
if (!patches) return
const el = parent.childNodes[index]
switch (patches.type) {
case 'REPLACE':
const newNode = render(patches.node)
parent.replaceChild(newNode, el)
break
case 'UPDATE':
// 更新属性
for (const key in patches.props) {
if (patches.props[key] === undefined) {
el.removeAttribute(key)
} else {
el.setAttribute(key, patches.props[key])
}
}
// 更新子节点
patches.children.forEach((childPatch, i) => {
patch(el, childPatch, i)
})
break
}
}
// ------------------- 使用示例 -------------------
// 初始化虚拟DOM
const oldVTree = createElement('div', { class: 'box' }, [
createElement('h1', {}, ['Title']),
createElement('p', { id: 'content' }, ['Hello World'])
])
// 创建新虚拟DOM
const newVTree = createElement('div', { class: 'box active' }, [
createElement('h1', {}, ['New Title']),
createElement('p', { id: 'content' }, ['Updated Content'])
])
// 首次渲染
const app = document.getElementById('app')
const realDOM = render(oldVTree)
app.appendChild(realDOM)
// 计算差异并更新
setTimeout(() => {
const patches = diff(oldVTree, newVTree)
patch(app, patches) // 执行DOM更新
}, 2000)
Vue3响应式原理
reactive reactive 是 Vue 3 中创建响应式对象的核心函数。它的作用是将普通对象转换为响应式对象。
function reactive(target) {
// 如果不是对象,直接返回
if (!isObj(target)) {
return target;
}
// 创建拦截器 handler
const baseHandlers = {
get,
set,
deleteProperty
};
// 使用 Proxy 包装对象
return new Proxy(target, baseHandlers);
}
function isObj(target) {
return target !== null && typeof target === 'object';
}
拦截器 handler 的实现:
- get:拦截属性的访问操作。
- 收集依赖(track)。
- 如果当前属性的值是对象,则递归创建 Proxy。
- set:拦截属性的赋值操作。
- 如果新值与旧值不同,则更新值并触发更新(trigger)。
- deleteProperty:拦截属性的删除操作。
- 如果对象中存在该属性,则删除并触发更新(trigger)。
import { reactive } from 'vue';
const state = reactive({
user: {
name: 'Alice',
age: 30
},
items: ['apple', 'banana']
});
state.user.age = 31; // 响应式更新
state.items.push('orange'); // 响应式更新
特性:
- 深度响应式:对象中的嵌套对象、数组、引用类型都会被递归转换为响应式。
- 自动依赖追踪:当响应式对象中的属性被读取时,Vue 会自动追踪依赖;当这些属性发生变化时,依赖的视图或计算属性会自动更新。
reactive 在实际开发中非常常用,适合需要深度响应的场景。
effect
effect 用于定义一个响应式副作用函数。当访问响应式对象的属性时,会自动收集依赖。
let activeEffect = null;
function effect(fn) {
const _effect = function() {
activeEffect = _effect;
fn();
activeEffect = null;
};
_effect();
}
- activeEffect 是一个全局变量,用于标识当前正在执行的副作用函数。
- 当 effect 被调用时,会将传入的函数包装为一个副作用函数,并立即执行。
track
track 用于收集依赖,将当前的 activeEffect 与响应式对象的某个属性关联起来。
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
// 获取 target 对应的依赖映射
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 获取 key 对应的依赖集合
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
// 将当前的 effect 添加到依赖集合中
dep.add(activeEffect);
}
- 使用 WeakMap 存储响应式对象与依赖映射的关系。
- 每个响应式对象的属性对应一个 Set,存储依赖的 effect。
trigger
trigger 用于触发依赖的更新,当响应式对象的属性发生变化时,会通知所有相关的 effect 重新执行。
function trigger(target, key) {
// 获取 target 对应的依赖映射
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 获取 key 对应的依赖集合
const dep = depsMap.get(key);
// 触发所有依赖的 effect
dep && dep.forEach(effect => {
effect();
});
}
当属性值发生变化时,trigger 会找到所有与该属性相关的 effect,并重新执行它们。
Vue Compiler实现原理
Parse(解析)
- 输入:模板字符串(template)。
- 输出:抽象语法树(AST)。
- 作用:将模板字符串解析为 AST,表示模板的语法结构。
- 实现:使用正则表达式或其他解析技术,将模板分解为节点(如元素、文本、指令等)。
Optimize(优化)
- 输入:AST。
- 输出:优化后的 AST。
- 作用:遍历 AST,标记静态节点,减少渲染时的 diff 开销。
- 实现:标记不会因数据变化而变化的节点为静态节点,优化性能。
Generate(生成)
- 输入:优化后的 AST。
- 输出:渲染函数(render)。
- 作用:将优化后的 AST 转换为渲染函数的字符串表示,通过 new Function 转换为可执行函数。
- 实现:生成的渲染函数返回一个 VNode,用于生成虚拟 DOM。
watch 与 computed 的区别
computed 函数
创建计算属性,基于其他响应式数据计算派生值。计算属性具有缓存特性,只在依赖的数据发生变化时重新计算。
import { ref, computed } from 'vue';
const count = ref(1);
const doubleCount = computed(() => count.value * 2);
console.log(doubleCount.value); // 2
count.value = 2;
console.log(doubleCount.value); // 4
特性:
- 依赖追踪:自动追踪依赖的响应式数据,只在依赖数据变化时重新计算。
- 缓存机制:具有缓存特性,避免重复计算,提高性能。
- 只读:默认情况下,计算属性是只读的。
- 可读写:可以通过传入 getter 和 setter 创建可读写的计算属性。
可写的 computed:
const count = ref(1);
const doubleCount = computed({
get: () => count.value * 2,
set: (val) => {
count.value = val / 2;
}
});
doubleCount.value = 10; // 通过 setter 修改原始数据
console.log(count.value); // 5
watch 函数
用于监听一个或多个响应式数据的变化,并在数据变化时执行特定的副作用。
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
count.value = 1; // 打印:Count changed from 0 to 1
特性:
- 灵活性:允许执行副作用,如异步操作、API 调用等。
- 监听多个数据:可以同时监听多个响应式数据。
- 深度监听:通过
{ deep: true }
选项实现深度监听,监听嵌套对象的属性变化。 - 立即执行:通过
{ immediate: true }
选项在监听器创建时立即执行回调函数。
深度监听和立即执行:
import { reactive, watch } from 'vue';
const state = reactive({
user: { name: 'Alice', age: 30 }
});
watch(
() => state.user,
(newValue, oldValue) => {
console.log('User data changed:', newValue);
},
{ deep: true, immediate: true }
);
监听多个值:
const firstName = ref('John');
const lastName = ref('Doe');
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`);
});
firstName.value = 'Jane'; // 打印:Name changed from John Doe to Jane Doe
特性 | computed | watch |
---|---|---|
用途 | 定义派生数据,基于响应式数据计算结果。 | 监听响应式数据的变化并执行副作用。 |
缓存 | 具有缓存特性,只在依赖数据变化时重新计算。 | 不缓存结果,每次数据变化都会执行回调函数。 |
返回值 | 返回计算结果,通常用于模板或计算逻辑。 | 不返回值,主要用于执行副作用(如 API 调用、状态更新等)。 |
只读 | 默认只读,但可以通过 getter 和 setter 创建可读写的计算属性。 | 不涉及只读性,专注于监听和响应数据变化。 |
灵活性 | 适用于简单的计算逻辑。 | 适用于复杂的副作用逻辑,如异步操作、手动 DOM 操作等。 |
监听多个数据 | 不直接支持,但可以通过组合计算属性实现。 | 支持监听多个响应式数据。 |
深度监听 | 不支持,计算属性的依赖是自动追踪的。 | 支持通过 { deep: true } 实现深度监听。 |
立即执行 | 不支持,计算属性在首次访问时计算。 | 支持通过 { immediate: true } 在监听器创建时立即执行回调。 |
使用场景
- computed:
- 用于计算派生数据,如表单验证、复杂数据的动态计算。
- 当需要基于其他响应式数据计算一个新的值,并希望这个值具有缓存特性时。
- watch:
- 用于处理数据变化的副作用,如保存数据到本地存储、发起 API 请求。
- 当需要在数据变化时执行异步操作或复杂的逻辑时。
watch 和 watchEffect
watch
参考上文
watchEffect
一种自动依赖追踪的监听器,Vue 会在副作用函数执行时自动追踪所有依赖的数据,当这些依赖发生变化时,副作用会重新执行。
特性:
- 自动依赖追踪:无须显式声明依赖,Vue 会自动追踪副作用函数中使用到的响应式数据。
- 立即执行:在创建时立即执行副作用,并在依赖的任何响应式数据发生变化时重新执行。
- 无法获取旧值:不提供新旧值的比较,因为副作用会自动重新运行。
- 适用于简单的副作用:通常用于处理简单的副作用逻辑,如 DOM 操作或响应式数据的快速同步。
import { ref, watchEffect } from 'vue';
const count = ref(0);
watchEffect(() => {
console.log(`Count is now: ${count.value}`);
});
count.value = 1; // 打印:Count is now: 1
count.value = 2; // 打印:Count is now: 2
特性 | watch | watchEffect |
---|---|---|
依赖声明方式 | 显式声明要监听的响应式数据。 | 自动追踪副作用函数中使用到的响应式数据,无需显式声明。 |
副作用执行时机 | 回调函数只在指定的响应式数据发生变化时执行,可以通过配置控制是否立即执行。 | 在副作用创建时立即执行,并在依赖的任何响应式数据发生变化时重新执行。 |
回调函数参数 | 提供新值和旧值,方便进行比较或逻辑处理。 | 不提供新值和旧值,主要侧重于自动重新执行副作用。 |
适用场景 | 适合用于监听具体响应式数据的变化,并执行复杂的逻辑或副作用。 | 适合用于自动追踪响应式依赖、无需手动声明依赖的场景。 |
使用场景
- watch:适合精确控制依赖监听的场景,尤其是需要进行异步操作或新旧值对比时。
- watchEffect:适合简化处理副作用的场景,尤其是在需要自动追踪多个响应式数据依赖时,无需明确声明。
vue 修饰符都有哪些
事件修饰符 事件修饰符用于修改事件的行为,例如阻止默认行为或停止事件冒泡。
- .stop:调用 event.stopPropagation(),阻止事件冒泡。
<button @click.stop="doThis">Click me</button>
- .prevent:调用 event.preventDefault(),阻止默认行为。
<form @submit.prevent="onSubmit">Submit</form>
- .capture:在事件冒泡的捕获阶段触发事件处理器。
<div @click.capture="doThis">Click me</div>
- .self:只有当事件是从该元素本身触发时才触发事件处理器。
<div @click.self="doThis">Click me</div>
- .once:事件只触发一次。
<button @click.once="doThis">Click me</button>
- .passive:以被动模式添加事件监听器,提高滚动性能。
<div @scroll.passive="onScroll">Scroll me</div>
按键修饰符 按键修饰符用于监听特定的按键事件。
- .enter:监听 Enter 键。
<input @keyup.enter="onEnter" />
- .tab:监听 Tab 键。
<input @keyup.tab="onTab" />
- .ctrl:监听 Ctrl 键。
<input @keyup.ctrl.enter="onCtrlEnter" />
- .shift:监听 Shift 键。
<input @keyup.shift.enter="onShiftEnter" />
- .alt:监听 Alt 键。
<input @keyup.alt.enter="onAltEnter" />
- .meta:监听 Meta 键(Mac 上的 Command 键)。
<input @keyup.meta.enter="onMetaEnter" />
表单修饰符
表单修饰符用于修改表单输入的行为,例如自动去除输入值的空白字符。
- .lazy:将 v-model 的更新时机改为 change 事件。
<input v-model.lazy="message" />
- .number:将输入值自动转换为数字。
<input v-model.number="age" />
- .trim:自动去除输入值的空白字符。
<input v-model.trim="message" />
emit、
on、once、
off理解
$emit
用途:触发当前实例上的自定义事件,并将附加参数传递给监听器回调。
实现原理:在 Vue 实例的事件系统中,emit 会查找与事件名对应的回调函数列表,并依次调用这些回调,将传入的参数传递给它们。 `this.
emit('my-event', data);`
$on 用途:监听实例上的自定义事件,并在事件触发时调用回调函数。
实现原理:$on 会将回调函数添加到与事件名对应的回调列表中。当事件被触发时,这些回调会被依次执行。
this.$on('my-event', function(data) {
console.log(data);
});
$once
用途:监听一个自定义事件,但只触发一次。在第一次触发后,监听器会被自动移除。
实现原理:$once 会将回调函数包装在一个只执行一次的函数中,然后将其添加到事件的回调列表。触发后,包装函数会移除自身。
this.$once('my-event', function(data) {
console.log(data);
});
$off
用途:移除自定义事件的监听器。
- 不带参数:移除所有事件监听器。
- 带事件名:移除该事件的所有监听器。
- 带事件名和回调:只移除指定的回调监听器。
实现原理:$off 会从事件的回调列表中移除对应的回调函数。如果未指定事件或回调,则清空所有回调。
// 移除所有监听器
this.$off();
// 移除指定事件的所有监听器
this.$off('my-event');
// 移除指定事件的特定回调
this.$off('my-event', callback);
Vue的Slot
在子组件中使用 <slot>
标签定义插槽,父组件中插入的内容会替换子组件中的 <slot>
标签。
子组件:
<template>
<div class="child-component">
<h2>子组件标题</h2>
<slot></slot>
</div>
</template>
父组件:
<template>
<div>
<child-component>
<p>这是父组件插入的内容,会替换子组件的 <slot> 标签。</p>
</child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
}
};
</script>
具名插槽
当子组件中有多个插槽时,可以通过 name 属性为插槽命名。父组件中使用 slot 属性指定内容插入到哪个插槽。
子组件:
<template>
<div class="child-component">
<h2>子组件标题</h2>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
</template>
父组件:
<template>
<div>
<child-component>
<template v-slot:header>
<h3>这是插入到 header 插槽的内容</h3>
</template>
<p>这是插入到默认插槽的内容。</p>
<template v-slot:footer>
<p>这是插入到 footer 插槽的内容</p>
</template>
</child-component>
</div>
</template>
v-slot作用域插槽
作用域插槽允许父组件访问子组件的局部数据,通过 slot-scope 或 v-slot 语法实现。
原理
- 编译阶段:在编译阶段,v-slot 会将插槽内容转换为一个渲染函数,存储在父组件的 scopedSlots 对象中。
- 运行时:在子组件渲染时,v-slot 的渲染函数会被调用,生成虚拟 DOM 节点(VNode),并绑定相应的插槽作用域数据。
- 响应式更新:当插槽作用域中的数据发生变化时,Vue 的响应式系统会触发 VNode 的更新,重新渲染插槽内容
子组件:
<template>
<div class="child-component">
<h2>子组件标题</h2>
<slot name="items" :items="items"></slot>
</div>
</template>
<script>
export default {
data() {
return {
items: ['Item 1', 'Item 2', 'Item 3']
};
}
};
</script>
父组件:
<template>
<div>
<child-component>
<template v-slot:items="slotProps">
<ul>
<li v-for="item in slotProps.items" :key="item">{{ item }}</li>
</ul>
</template>
</child-component>
</div>
</template>
自定义指令
全局指令 全局指令通过 Vue.directive 方法定义,作用于所有组件。
Vue.directive('my-directive', {
bind(el, binding, vnode) {
// 初始化时调用
},
inserted(el, binding, vnode) {
// 元素插入父级时调用
},
update(el, binding, vnode, oldVnode) {
// 元素更新时调用
},
componentUpdated(el, binding, vnode, oldVnode) {
// 元素更新完成后调用
},
unbind(el, binding, vnode) {
// 元素所在的模板删除时调用
}
});
简写方式: 如果只需要一个钩子函数,可以直接简写为一个函数:
Vue.directive('focus', function(el, binding) {
// 自定义逻辑
});
使用:
<template>
<input v-focus>
</template>
组件指令
组件指令在组件的 directives 选项中定义,仅在该组件中有效。
export default {
directives: {
'my-directive': {
bind(el, binding, vnode) {
// 初始化时调用
},
inserted(el, binding, vnode) {
// 元素插入父级时调用
},
update(el, binding, vnode, oldVnode) {
// 元素更新时调用
},
componentUpdated(el, binding, vnode, oldVnode) {
// 元素更新完成后调用
},
unbind(el, binding, vnode) {
// 元素所在的模板删除时调用
}
}
}
};
钩子函数
自定义指令的钩子函数及其调用时机:
- bind:指令第一次绑定到元素时调用,只调用一次。
- inserted:元素插入父级时调用。
- update:元素更新时调用。
- componentUpdated:元素更新完成后调用。
- unbind:元素所在的模板删除时调用。
钩子函数参数
钩子函数接收以下参数:
- el:指令绑定的 DOM 元素。
- binding:一个对象,包含以下属性:
- name:指令名称(不包括 v-)。
- value:指令的绑定值。
- oldValue:指令的旧值(仅在 update 和 componentUpdated 中有效)。
- expression:指令绑定的原始表达式。
- arg:指令的参数。
- modifiers:指令的修饰符(如 v-focus.show.async 的修饰符为
{ show: true, async: true }
)。
- vnode:Vue 编译生成的虚拟 DOM。
- oldVnode:上一个虚拟 DOM(仅在 update 和 componentUpdated 中有效)。
Vue 性能优化
编码阶段
- 减少 data 中的数据:data 中的数据会增加 getter 和 setter,并收集对应的 watcher,尽量减少不必要的数据。
- 避免 v-if 和 v-for 连用:v-if 和 v-for 连用会导致性能问题,尽量避免。
- 使用事件代理:如果需要使用 v-for 给每项元素绑定事件,使用事件代理可以减少事件监听器的数量。
- 使用 keep-alive 缓存组件:对于 SPA 页面,使用 keep-alive 缓存组件可以减少组件的重复渲染。
- 使用 v-if 替代 v-show:在更多的情况下,使用 v-if 替代 v-show,因为 v-if 不会渲染不需要的 DOM 元素。
- 保证 key 的唯一性:在使用 v-for 时,确保 key 的唯一性,避免 Vue 误判 DOM 的变化。
- 使用路由懒加载和异步组件:通过路由懒加载和异步组件,按需加载资源,减少初始加载时间。
- 防抖和节流:对频繁触发的事件(如输入、滚动等)使用防抖和节流技术,减少不必要的计算。
- 按需导入第三方模块:使用按需导入的方式加载第三方模块,减少打包体积。
- 长列表滚动到可视区域动态加载:对于长列表,只加载可视区域的数据,减少内存占用。
- 图片懒加载:对图片资源使用懒加载技术,减少初始加载时间。
- SEO 优化:使用预渲染或服务端渲染(SSR)提升 SEO 效果。
打包优化
- 压缩代码:通过 Webpack 等工具压缩代码,减少打包体积。
- Tree Shaking/Scope Hoisting:利用 Tree Shaking 和 Scope Hoisting 技术去除未使用的代码。
- 使用 CDN 加载第三方模块:通过 CDN 加载第三方模块,减少服务器负载。
- 多线程打包:使用 HappyPack 等工具实现多线程打包,加快打包速度。
- splitChunks 抽离公共文件:通过 splitChunks 抽离公共文件,减少重复代码。
- sourceMap 优化:合理配置 sourceMap,在开发和生产环境中选择合适的配置。
用户体验优化
- 骨架屏:在页面加载时显示骨架屏,提升用户体验。
- PWA:使用渐进式 Web 应用(PWA)技术,提升应用的离线体验。
- 缓存优化:使用客户端缓存和服务端缓存,减少重复请求。
- 服务端开启 Gzip 压缩:通过服务端开启 Gzip 压缩,减少传输数据量。
Vue组件懒加载
组件懒加载是指在 Vue 应用中,通过动态导入的方式按需加载组件。这种方式不仅限于路由组件,可以应用于任何需要延迟加载的组件。
Vue 异步组件
Vue 提供了异步组件的功能,可以实现按需加载组件。每个组件会生成一个单独的 JS 文件。
{
path: '/home',
name: 'home',
component: resolve => require(['@/components/home'], resolve)
},
{
path: '/index',
name: 'index',
component: resolve => require(['@/components/index'], resolve)
},
{
path: '/about',
name: 'about',
component: resolve => require(['@/components/about'], resolve)
}
ES6 的 import()
使用 ES6 的 import() 语法实现懒加载。可以指定 webpackChunkName 来控制打包文件的名称,多个组件可以合并打包成一个文件。
// 每个组件生成一个单独的 JS 文件
const Home = () => import('@/components/home');
const Index = () => import('@/components/index');
const About = () => import('@/components/about');
// 指定相同的 `webpackChunkName`,合并打包成一个 JS 文件
const Home = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/home');
const Index = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/index');
const About = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/about');
{
path: '/home',
component: Home
},
{
path: '/index',
component: Index
},
{
path: '/about',
component: About
}
Webpack 的 require.ensure()
使用 Webpack 的 require.ensure() 方法实现懒加载。通过指定 chunkName,可以将多个路由打包成一个文件。
{
path: '/home',
name: 'home',
component: r => require.ensure([], () => r(require('@/components/home')), 'demo')
},
{
path: '/index',
name: 'index',
component: r => require.ensure([], () => r(require('@/components/index')), 'demo')
},
{
path: '/about',
name: 'about',
component: r => require.ensure([], () => r(require('@/components/about')), 'demo-01')
}
Vue路由懒加载
路由懒加载是指在 Vue Router 中,通过动态导入(import())的方式按需加载路由组件。
这样可以减少初始加载时的资源消耗,只在用户访问某个路由时才加载对应的组件。
- 按需加载:只有当用户访问某个路由时,对应的组件才会被加载。
- 优化性能:减少初始加载时间,提高应用性能。
- 代码分割:Webpack 会将每个动态导入的组件单独打包。
创建路由配置
使用 Webpack 的动态导入特性来定义路由组件。
// router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{
path: '/',
name: 'home',
component: () => import('./components/Home.vue'), // 懒加载 Home 组件
},
{
path: '/about',
name: 'about',
component: () => import('./components/About.vue'), // 懒加载 About 组件
},
{
path: '/contact',
name: 'contact',
component: () => import('./components/Contact.vue'), // 懒加载 Contact 组件
},
],
});
export default router;
在 Vue 应用中使用路由
在 Vue 应用的入口文件中,引入并使用路由配置。
// main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router';
Vue.config.productionTip = false;
new Vue({
router,
render: (h) => h(App),
}).$mount('#app');
``
**创建组件**
创建需要懒加载的组件。
Home.vue
```vue
<template>
<div>
<h1>首页</h1>
<p>这是首页内容。</p>
</div>
</template>
<script>
export default {
name: 'Home',
created() {
console.log('Home 组件已加载');
},
};
</script>
About.vue
<template>
<div>
<h1>关于我们</h1>
<p>这是关于我们页面的内容。</p>
</div>
</template>
<script>
export default {
name: 'About',
created() {
console.log('About 组件已加载');
},
};
</script>
Contact.vue
<template>
<div>
<h1>联系我们</h1>
<p>这是联系我们页面的内容。</p>
</div>
</template>
<script>
export default {
name: 'Contact',
created() {
console.log('Contact 组件已加载');
},
};
</script>
创建导航链接
在主应用组件中,创建导航链接以便用户可以访问不同的页面。
<template>
<div>
<nav>
<router-link to="/">首页</router-link> |
<router-link to="/about">关于我们</router-link> |
<router-link to="/contact">联系我们</router-link>
</nav>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
};
</script>
工作原理
- 动态导入:使用 () => import('path/to/component') 语法动态导入组件。Webpack 会将这些动态导入的组件单独打包,按需加载。
- 路由配置:在路由配置中,将动态导入的组件分配给对应的路径。
- 按需加载:当用户访问某个路由时,对应的组件才会被加载,从而减少初始加载时间。
注意事项
- 性能优化:懒加载可以显著提高应用的初始加载性能,特别是在路由较多时。
- 代码分割:Webpack 会自动将动态导入的组件进行代码分割,确保每个组件单独打包。
- 加载状态:可以添加加载状态或错误处理逻辑,以提高用户体验。
Vue首页白屏解决
路由懒加载
使用 Vue Router 的懒加载功能,按需加载组件,减少首屏加载的资源量。
const routes = [
{
path: '/home',
component: () => import(/* webpackChunkName: "home" */ './views/Home.vue')
},
{
path: '/about',
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
}
];
优化打包体积
通过 Webpack 的 splitChunks 和 TerserPlugin 来优化打包体积,减少首屏加载的文件大小。
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
},
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
},
},
}),
],
},
};
使用骨架屏
在页面加载时显示骨架屏,提升用户体验,减少白屏时间。
<template>
<div>
<div v-if="loading" class="skeleton">
<!-- 骨架屏内容 -->
</div>
<router-view v-else></router-view>
</div>
</template>
<script>
export default {
data() {
return {
loading: true
};
},
mounted() {
setTimeout(() => {
this.loading = false;
}, 1000);
}
};
</script>
<style>
.skeleton {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
}
</style>
使用 CDN 加速
将静态资源(如 Vue、Vuex 等)托管到 CDN 上,减少主包体积,提高加载速度。
服务端渲染(SSR)
使用 Vue 的服务端渲染技术,提前在服务器端生成 HTML,减少首屏渲染时间。
移除 console.log
在生产环境中删除掉不必要的 console.log。console.log 会占用一定的系统资源,虽然单次调用的影响很小,但如果频繁调用,会对性能产生一定的负面影响。
特别是在生产环境中,console.log 可能会导致页面加载变慢,影响用户体验。
// vue.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
configureWebpack: {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除 console.log
drop_debugger: true // 移除 debugger
}
}
})
]
}
}
};
启用 GZIP 压缩
在 Nginx 配置文件中启用 GZIP 压缩,减少传输数据量。
http {
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 8;
gzip_buffers 16 8k;
gzip_min_length 100;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
}
Vue SPA 首屏速度优化
请求优化
- CDN:将第三方库和静态资源部署到 CDN,减少项目体积,加速资源加载。
- CDN 根据用户位置智能选择最近节点,降低延迟。
缓存策略
- 强缓存:对静态资源设置强缓存,max-age 设为较长值,路径加哈希确保更新后获取新资源。
- 减少重复请求,提升加载速度。
压缩与协议
- Gzip 压缩:开启 Gzip 压缩,减小传输资源体积。
- HTTP/2:利用 HTTP/2 多路复用,突破浏览器 TCP 连接限制,提升加载效率。
按需加载
- 路由懒加载:按需加载路由组件,减少首屏代码量。
const Home = () => import('./views/Home.vue');
- 代码分割:利用 Webpack 的代码分割功能,将非首屏代码异步加载。
预渲染
- 骨架屏:首屏加载时显示骨架屏,减少白屏时间。
- 预渲染:使用服务端渲染(SSR)或静态生成,提前生成 HTML 内容。
合理使用第三方库
- 按需加载:使用按需加载工具(如 babel-plugin-component),仅加载所需组件。
- 体积分析:使用 webpack-bundle-analyzer 分析打包体积,优化大模块。
封装与架构
- 公共封装:封装公共组件、插件、过滤器等,减少重复代码,便于维护。
- 项目架构:合理组织项目结构,提升开发效率和可维护性。
资源优化
- 图片懒加载:延迟加载非首屏图片,减少初始加载时间。
- SVG 图标:使用 SVG 替代图片图标,减少 HTTP 请求,提升质量。
- 图片压缩:使用 image-webpack-loader 压缩图片,减小体积。
Vue 组件中name的好处
- 递归组件:通过名字找到对应的组件,实现递归组件。
- 缓存功能:通过 name 实现
<keep-alive>
的缓存功能。 - 组件识别:通过 name 识别组件,尤其在跨级组件通信时非常重要。
- 调试工具:Vue Devtools 中显示的组件名称由 name 决定,便于调试。
<template>
<div>
<!-- 父组件 -->
<h1>父组件</h1>
<!-- 使用 keep-alive 缓存子组件 -->
<keep-alive include="MyComponent">
<my-component v-if="showComponent"></my-component>
</keep-alive>
<button @click="toggleComponent">切换组件显示</button>
</div>
</template>
<script>
// 子组件
const MyComponent = {
name: 'MyComponent', // 定义组件名称
data() {
return {
count: 0,
};
},
template: `
<div>
<h2>子组件 ({{ count }})</h2>
<button @click="count++">计数 +1</button>
</div>
`,
};
export default {
components: {
MyComponent, // 注册子组件
},
data() {
return {
showComponent: true, // 控制子组件的显示
};
},
methods: {
toggleComponent() {
this.showComponent = !this.showComponent;
},
findComponentByName() {
// 通过 name 查找子组件
const myComponent = this.$children.find(child => child.$options.name === 'MyComponent');
if (myComponent) {
console.log('找到子组件:', myComponent);
} else {
console.log('未找到子组件');
}
},
},
mounted() {
// 在组件挂载后查找子组件
this.findComponentByName();
},
};
</script>
$root
、$refs
、$parent
理解$root
用途:获取 Vue 的根实例。 使用场景:在简单的项目中,可以将公共数据存储在根实例上,作为全局状态管理的替代方案(类似 Vuex)。
// 在根实例上设置公共数据
this.$root.globalData = 'Hello from root';
// 在子组件中访问根实例的数据
console.log(this.$root.globalData); // 输出: Hello from root
$refs
用途:直接访问子组件或 DOM 元素。 使用场景:可以代替事件 emit 和 $on,用于直接操作子组件。
使用方式:
- 在子组件上使用 ref 属性。
- 通过 this.$refs 访问子组件实例。
注意事项:
- $refs 只在组件渲染完成后生效。
- $refs 不是响应式的,应避免在模板或计算属性中使用。
<template>
<div>
<child-component ref="childComponent"></child-component>
<button @click="callChildMethod">调用子组件方法</button>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
methods: {
callChildMethod() {
this.$refs.childComponent.childMethod();
}
}
};
</script>
$parent
用途:从子组件访问父组件的实例。 使用场景:可以替代通过 props 传递数据的方式,但容易增加调试和理解的难度。
<!-- 父组件 -->
<template>
<div>
<child-component></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
parentData: 'Hello from parent'
};
}
};
</script>
<!-- 子组件 -->
<template>
<div>
<p>{{ parentData }}</p>
</div>
</template>
<script>
export default {
mounted() {
this.parentData = this.$parent.parentData;
}
};
</script>
ref 的作用
ref 是 Vue 中的一个属性,用于给元素或子组件注册引用信息。其主要作用是为父组件提供一种方式,能够直接访问子组件或 DOM 元素。
ref 的特点
- 在普通 DOM 元素上使用时:引用指向的是 DOM 元素本身。
- 在子组件上使用时:引用指向的是子组件的实例。
基本用法:获取 DOM 元素
在页面中直接获取 DOM 元素,以便进行操作(如获取元素的尺寸、位置、内容等)。
<template>
<div ref="myDiv">Hello</div>
</template>
<script>
export default {
mounted() {
console.log(this.$refs.myDiv) // 输出 DOM 元素
},
}
</script>
获取子组件中的 data,通过 ref 引用子组件实例,访问子组件的 data 属性。
<template>
<ChildComponent ref="child" />
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
mounted() {
console.log(this.$refs.child.someData) // 访问子组件的 data
},
}
</script>
调用子组件中的方法,通过 ref 引用子组件实例,调用子组件的方法。
<template>
<ChildComponent ref="child" />
<button @click="callChildMethod">Call Child Method</button>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
methods: {
callChildMethod() {
this.$refs.child.someMethod() // 调用子组件的方法
},
},
}
</script>
composition和options API?
结构与语法
Options API: 使用多个选项对象来组织组件的逻辑,例如 data、methods、computed、watch、mounted 等。每个选项都有特定的功能。
export default {
data() {
return {
count: 0,
}
},
methods: {
increment() {
this.count++
},
},
mounted() {
console.log('Component is mounted')
},
}
Composition API: 使用 setup 函数,将逻辑组织在一起,不再依赖于各个选项。可以更灵活地将逻辑划分为可复用的函数或组合函数(composables)。
import { ref, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
onMounted(() => {
console.log('Component is mounted')
})
return {
count,
increment,
}
},
}
逻辑复用与组织
- Options API:逻辑根据功能分散到不同的选项中,复用逻辑较为复杂,通常需要通过 mixins 或继承来实现,但可能导致逻辑分散、难以维护。
- Composition API:可以将逻辑封装为独立的函数或 composables,这些函数可以在不同组件中复用。它提供了更灵活的逻辑组织方式,尤其适合复杂的应用程序。
类型支持
- Options API:对于 TypeScript 的支持相对较弱,类型推断有限,类型安全性较低。
- Composition API:设计时考虑了 TypeScript,类型推断与类型安全性更好,代码编写过程中更友好。
响应式系统
- Options API:响应式系统是通过 data 选项来定义响应式数据。
- Composition API:响应式系统通过 ref、reactive 等更显式的 API 来处理,能够更清晰地控制响应式数据的创建和使用。
代码组织与复用
- Options API:代码根据功能分散在不同的生命周期钩子和选项中,例如数据初始化放在 data,事件监听放在 mounted,逻辑分离较为明确,但难以组织复杂逻辑。
- Composition API:代码组织更灵活,所有逻辑可以集中在 setup 函数内。逻辑复用更简单,通过 composables 和自定义函数可以更加模块化和清晰。
Composition API 与 React Hook 的对比
Vue 的 Composition API 在设计思想上借鉴了 React Hook,都旨在提供一种更灵活的方式来组织和复用组件逻辑。
调用限制
- React Hook:
- 不能在循环、条件或嵌套函数中调用 Hook,否则会导致 Hook 的调用顺序不一致,从而引发错误。
- 必须确保总是在 React 函数组件的顶层调用 Hook。
- Vue Composition API:
- 可以在循环、条件或嵌套函数中调用 Composition API 提供的函数(如 ref、reactive 等)。
- 调用顺序不受限制,灵活性更高。
调用时机
- React Hook:
- 每次组件重渲染时,Hook 都会被重新调用。
- 这意味着每次渲染都会执行 useState、useEffect 等 Hook 的逻辑。
- Vue Composition API:
- setup 函数在组件实例化时只调用一次。
- 响应式数据的更新由 Vue 的响应式系统自动处理,无需在每次渲染时重新调用。
性能影响
- React Hook:
- 每次重渲染都需要调用 Hook,对垃圾回收(GC)压力较大。
- 性能相对较低,尤其是在复杂组件中。
- Vue Composition API:
- 基于 Vue 的响应式系统,性能优化由 Vue 内部完成。
- 对 GC 的压力较小,性能相对更高。
依赖管理
- React Hook:
- 需要手动确定依赖关系,如 useEffect、useMemo 等必须手动传入依赖。
- 必须保证依赖顺序正确,否则会导致组件性能下降或逻辑错误。
- Vue Composition API:
- 响应式系统自动实现依赖收集,无需手动传入依赖。
- Vue 内部自动优化依赖关系,减少性能问题。
响应式机制
- React Hook:
- 基于 JavaScript 的闭包和调用顺序来管理状态和副作用。
- 使用 useState、useEffect 等 Hook 来实现响应式逻辑。
- Vue Composition API:
- 基于 Vue 的响应式系统,通过 ref、reactive 等 API 实现响应式数据管理。
- 响应式数据的更新由 Vue 的响应式系统自动处理。
shallowReactive
在 Vue 3 的 Composition API 中,shallowReactive 是一个用于创建浅层响应式对象的函数。
它的主要作用是将对象的 顶层属性 转换为响应式,但不会递归地将嵌套对象的属性转换为响应式。
这种方式可以优化性能,避免不必要的代理开销。
浅层响应式
- 只有对象的 顶层属性 是响应式的。
- 嵌套对象的属性不会自动响应式更新。
性能优化
- 相比 reactive,shallowReactive 的性能开销更小。
- 适用于嵌套对象无需响应式更新的场景。
适用场景
- 当你明确知道嵌套对象不需要响应式更新时。
- 需要优化性能,避免递归代理嵌套对象。
import { shallowReactive } from 'vue';
const state = shallowReactive({
user: {
name: 'Alice',
age: 30
},
items: ['apple', 'banana']
});
// 顶层属性是响应式的
state.items.push('orange'); // 响应式更新,因为 `items` 是顶层属性
// 嵌套对象的属性不是响应式的
state.user.age = 31; // 不会响应式更新,因为 `user` 是嵌套对象
使用建议
- 如果你需要对嵌套对象的属性也进行响应式更新,可以结合 reactive 和 shallowReactive。
- 如果你明确知道嵌套对象不需要响应式更新,直接使用 shallowReactive 可以避免性能浪费。
const state = shallowReactive({
user: reactive({ name: 'Alice', age: 30 }), // 嵌套对象使用 reactive
items: ['apple', 'banana']
});
state.user.age = 31; // 响应式更新,因为 `user` 是 reactive 对象
特性 | reactive | shallowReactive |
---|---|---|
响应式深度 | 深度响应式(嵌套对象也响应式) | 浅层响应式(仅顶层属性响应式) |
性能开销 | 较高(递归代理嵌套对象) | 较低(仅代理顶层属性) |
适用场景 | 需要全局深度跟踪数据变化 | 嵌套对象无需响应式更新,或性能敏感场景 |
ref shallowRef isRef toRefs函数
ref 函数
创建一个响应式引用,用于处理原始数据类型或单个对象/数组引用。
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // 0
count.value++; // 响应式更新
console.log(count.value); // 1
特性:
- 将任何类型的值包装成响应式数据。
- 访问和修改值时需要通过 .value 属性。
- 适合处理单个原始值或简单对象的响应式场景。
shallowRef 函数
创建一个浅层响应式引用,仅对象或数组的引用本身是响应式的,内部属性不会自动响应式。
import { shallowRef } from 'vue';
const user = shallowRef({
name: 'Alice',
age: 30
});
user.value.age = 31; // 不会触发响应式更新
user.value = { name: 'Bob', age: 25 }; // 更换整个对象,响应式更新
特性:
- 仅对象或数组的引用是响应式的。
- 适合需要频繁替换对象引用而不关心内部属性变化的场景。
isRef 函数
检查一个对象是否是由 ref 或 shallowRef 创建的响应式引用。
import { ref, isRef } from 'vue';
const count = ref(0);
console.log(isRef(count)); // true
const notRef = 42;
console.log(isRef(notRef)); // false
toRefs 函数
将响应式对象的每个属性转换为 ref,通常与 reactive 一起使用。
import { reactive, toRefs } from 'vue';
const state = reactive({
name: 'Alice',
age: 30
});
const { name, age } = toRefs(state);
name.value = 'Bob'; // 响应式更新
age.value++; // 响应式更新
- 将 reactive 对象的属性转换为 ref,保留响应式特性。
- 适用于解构对象时保持响应式。
readonly isReadonly shallowReadonly
readonly
将一个对象转换为深度只读的响应式对象,所有嵌套属性都不可修改。
import { reactive, readonly } from 'vue';
const state = reactive({ name: 'Alice', age: 30 });
const readonlyState = readonly(state);
readonlyState.age = 31; // 无效操作,Vue 会发出警告
console.log(readonlyState.age); // 仍为 30
- 深度只读:递归地将对象及其所有嵌套属性转换为只读状态。
- 保护数据:防止无意中修改数据。
- 开发环境警告:在开发环境中修改只读对象会发出警告。
shallowReadonly
将对象的顶层属性设置为只读,嵌套对象的属性仍可修改。
import { shallowReadonly } from 'vue';
const state = {
user: { name: 'Alice', age: 30 },
items: ['apple', 'banana']
};
const shallowState = shallowReadonly(state);
shallowState.user.age = 31; // 有效操作
shallowState.items.push('orange'); // 有效操作
shallowState.user = { name: 'Bob', age: 25 }; // 无效操作
特性:
- 浅层只读:仅顶层属性只读,嵌套对象的属性可修改。
- 适用场景:保护对象结构,但允许内部数据修改。
isReadonly
检查对象是否由 readonly 或 shallowReadonly 创建的只读对象。
import { readonly, isReadonly } from 'vue';
const state = { name: 'Alice' };
const readonlyState = readonly(state);
console.log(isReadonly(state)); // false
console.log(isReadonly(readonlyState)); // true
Class与Style动态绑定?
动态绑定 class
1、对象语法
通过对象语法,可以动态地绑定一个对象,其中的键是类名,值是布尔值,表示是否应用该类。
<template>
<div v-bind:class="{ active: isActive, 'text-danger': hasError }">
动态绑定类
</div>
</template>
<script>
export default {
data() {
return {
isActive: true,
hasError: false
};
}
}
</script>
- active 类会在 isActive 为 true 时应用。
- text-danger 类会在 hasError 为 true 时应用。
2、数组语法
通过数组语法,可以动态地绑定一个数组,数组中的每个元素可以是字符串、对象或函数返回值。
<template>
<div v-bind:class="[isActive ? activeClass : '', errorClass]">
动态绑定类
</div>
</template>
<script>
export default {
data() {
return {
isActive: true,
activeClass: 'active',
errorClass: 'text-danger'
};
}
}
</script>
- isActive ? activeClass : '':根据 isActive 的值动态选择类名。
- errorClass:静态类名,始终应用。
动态绑定 style
1、对象语法
通过对象语法,可以动态地绑定一个对象,其中的键是样式属性,值是样式值。
<template>
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }">
动态绑定样式
</div>
</template>
<script>
export default {
data() {
return {
activeColor: 'red',
fontSize: 30
};
}
}
</script>
- color 属性会根据 activeColor 的值动态设置。
- fontSize 属性会根据 fontSize 的值动态设置,并拼接单位 px。
2、数组语法
通过数组语法,可以动态地绑定一个数组,数组中的每个元素可以是样式对象。
<template>
<div v-bind:style="[styleColor, styleSize]">
动态绑定样式
</div>
</template>
<script>
export default {
data() {
return {
styleColor: {
color: 'red'
},
styleSize: {
fontSize: '23px'
}
};
}
}
</script>
styleColor 和 styleSize 是两个样式对象,它们会被合并应用到元素上。
注意事项
- 对象语法中的布尔值:在对象语法中,值为 false 的类不会被应用。
- 数组语法中的优先级:在数组语法中,后面的样式对象会覆盖前面的同名样式。
- 样式单位:在动态绑定样式时,需要确保样式值包含正确的单位(如 px、% 等)。
- 驼峰命名法:在对象语法中,样式属性可以使用驼峰命名法(如 fontSize)或引号包裹的 kebab-case(如 'font-size')。
Vue2给数组项赋值数据监听
在 Vue 中,直接给数组项赋值或修改数组长度时,Vue 无法检测到这些变化。这是因为 JavaScript 的限制,Vue 无法追踪这些操作。以下是具体的解释和解决方法:
直接给数组项赋值
当你直接通过索引设置数组项的值时,Vue 无法检测到变化。例如:
vm.items[indexOfItem] = newValue;
这种情况下,Vue 不会触发视图更新,因为 Vue 的响应式系统无法追踪到这种变化。
解决:
1、Vue.set:使用 Vue.set 方法来设置数组项的值。
Vue.set(vm.items, indexOfItem, newValue);
2、vm.set:vm.
set 是 Vue.set 的一个别名,也可以用来设置数组项的值。
vm.$set(vm.items, indexOfItem, newValue);
3、vm.set:vm.
set 是 Vue.set 的一个别名,也可以用来设置数组项的值。
vm.items.splice(indexOfItem, 1, newValue);
修改数组的长度
当你直接修改数组的长度时,Vue 也无法检测到这些变化。例如:
vm.items.length = newLength;
这种情况下,Vue 不会触发视图更新。
解决:
splice:使用 Array.prototype.splice 方法来修改数组的长度。
vm.items.splice(newLength);
Vue 3给数组项赋值数据监听
在 Vue 3 中,直接给数组项赋值或修改数组长度时,Vue 能够检测到这些变化。这是因为 Proxy 可以动态拦截所有属性的访问和修改,包括数组的索引访问和长度修改
// Vue 3 响应式系统示例
const data = new Proxy(
{ message: 'Hello, Vue 3!', items: [1, 2, 3] },
{
get(target, key) {
console.log(`读取 ${key}`);
return target[key];
},
set(target, key, value) {
console.log(`更新 ${key}`);
target[key] = value;
return true;
}
}
);
// 直接给数组项赋值
data.items[0] = 10;
console.log(data.items); // 输出:[10, 2, 3]
// 修改数组长度
data.items.length = 2;
console.log(data.items); // 输出:[10, 2]
delete 和 Vue.delete区别
delete 操作符 delete 是 JavaScript 中的一个操作符,用于删除对象的属性。
当用于数组时,它只会将指定索引处的元素设置为 undefined,而不会改变数组的长度或重新索引数组。
let arr = [1, 2, 3, 4];
delete arr[2];
console.log(arr); // [1, 2, undefined, 4]
console.log(arr.length); // 4
Vue.delete
行为:Vue.delete 是 Vue 提供的一个全局方法,用于安全地删除对象的属性或数组的元素。
它会直接从数组中移除元素,并更新数组的长度,同时通知 Vue 的响应式系统,确保视图能够正确更新。
let arr = [1, 2, 3, 4];
Vue.delete(arr, 2);
console.log(arr); // [1, 2, 4]
console.log(arr.length); // 3
Vue常见问题及解决方案
样式污染
组件间的样式相互影响,导致样式污染。
解决方案:
- 使用 scoped:在组件的
<style scoped>
中定义样式,确保样式只作用于当前组件。 - 唯一标识符:在组件根元素上添加唯一的 class 或 id,并在样式中使用这些标识符。
- CSS Modules:使用 CSS Modules 来模块化样式,避免全局样式冲突。
router-link 在安卓上不起作用
在安卓设备上,router-link 的点击事件可能无法正常触发。
- 使用 .native 修饰符:通过 .native 修饰符直接监听原生的 click 事件。
<router-link to="/path" @click.native="handleClick">Link</router-link>
- 检查 Babel 配置:确保项目中安装了 @babel/polyfill 并正确配置,以解决转码问题。
初始化页面出现闪屏乱码
在 Vue 应用初始化时,可能会看到未编译的模板内容(如 {{ data }}
)。
- 使用 v-cloak 指令:在模板中使用 v-cloak,并在 CSS 中设置
[v-cloak] { display: none; }
。 - 隐藏根元素:在 index.html 中隐藏根元素,直到 Vue 实例挂载完成后再显示。
router-link 上事件无效
router-link 上的事件(如 @click)可能无法正常触发。 解决方案:
- 使用 .native 修饰符:直接监听原生的 click 事件。
<router-link to="/path" @click.native="handleClick">Link</router-link>
。 - 自定义事件处理:通过 @click 监听事件,并在事件处理函数中使用 this.$router.push 进行导航。
<router-link to="/path" @click="handleClick">Link</router-link>
methods: {
handleClick() {
this.$router.push('/path');
}
}
Vue 项目国际化
问题描述:需要支持多语言的国际化应用。
解决方案:
- 使用 Vue I18n:通过 Vue I18n 插件实现多语言支持。
- 动态切换语言:在应用中动态切换语言,支持用户选择。
参考文档:
On this page
- 双向数据绑定原理
- Vue的单向数据流
- Vue 的异步更新队列
- Vue中的合并对象
- setup 语法糖
- nextTick
- Vue单页面和多页面
- Vue路由实现
- route和router的区别?
- 路由跳转和location.href
- Vue Router钩子函数
- Vue Router导航守卫
- Vue组件data是函数的原因
- Vuex 使用
- Vuex的mapState
- Vue 的filter
- Vue的computed实现原理
- v-if 和 v-show
- v-if、v-show、v-html原理
- Vue中的v-pre
- Vue中的v-cloak
- v-for中的key
- Vue的transition
- keep-alive说明
- LRU策略
- 组件通信
- provide/inject 和 Props区别
- 如何自定义组件?
- 组件的单例模式
- Vue中组件复用的方式
- Vue组件中的this
- 在 Vue2 中检测数组的变化?
- defineProperty和proxy区别?
- 理解MVVM
- Vue生命周期
- 生命周期钩子执行顺序
- 父组件监听子组件生命周期钩子
- Ajax请求放在哪个生命周期?
- Vue的响应式原理
- Vue 的 $destroy
- 响应式引用
- Vue 2 和Vue 3的区别
- Vue 2.x 的 Diff 算法
- Vue 3.x 的 Diff 算法
- Vue3虚拟Dom
- Vue3响应式原理
- Vue Compiler实现原理
- watch 与 computed 的区别
- watch 和 watchEffect
- vue 修饰符都有哪些
- $emit、$on、$once、$off理解
- Vue的Slot
- 自定义指令
- Vue 性能优化
- Vue组件懒加载
- Vue路由懒加载
- Vue首页白屏解决
- Vue SPA 首屏速度优化
- Vue 组件中name的好处
- 理解$root、$refs、$parent
- ref 的作用
- composition和options API?
- Composition API 与 React Hook 的对比
- shallowReactive
- ref shallowRef isRef toRefs函数
- readonly isReadonly shallowReadonly
- Class与Style动态绑定?
- Vue2给数组项赋值数据监听
- delete 和 Vue.delete区别
- Vue常见问题及解决方案