引言
经历了几场面试的"狂轰滥炸",这才发现准备不足简直是自寻死路啊!痛定思痛之后,我决定把近几天遇到的"面试怪兽"------从刁钻问题到各种挑战,浓缩成一份精华面经。废话免谈,直接端上干货。希望这份面经能成为你求职路上的秘密武器,助你在面试战场上披荆斩棘,所向无敌!
vue3 指令
在 Vue 3 中,指令(Directives)是一种特殊的标记,用于在 DOM 元素上应用一些特殊的行为。Vue 3 提供了一些内置指令,同时也允许开发者自定义指令。
内置指令
Vue 3 提供了以下常用的内置指令:
-
v-bind
:动态绑定一个或多个属性,或一个组件 prop 到表达式。html<img v-bind:src="imageSrc"> <!-- 简写 --> <img :src="imageSrc">
-
v-model
:在表单控件或组件上创建双向数据绑定。html<input v-model="message">
-
v-if
、v-else-if
、v-else
:根据表达式的值条件性地渲染元素。html<div v-if="isVisible">Visible</div> <div v-else>Not Visible</div>
-
v-for
:基于源数据多次渲染元素或模板块。html<ul> <li v-for="item in items" :key="item.id">{{ item.text }}</li> </ul>
-
v-on
:监听 DOM 事件并在触发时执行一些 JavaScript 代码。html<button v-on:click="handleClick">Click me</button> <!-- 简写 --> <button @click="handleClick">Click me</button>
-
v-show
:根据表达式的值条件性地显示元素(通过 CSS 的display
属性)。html<div v-show="isVisible">Visible</div>
-
v-text
:更新元素的textContent
。html<span v-text="message"></span>
-
v-html
:更新元素的innerHTML
。html<div v-html="rawHtml"></div>
-
v-pre
:跳过这个元素和它的子元素的编译过程。html<div v-pre>{{ this will not be compiled }}</div>
-
v-cloak
:这个指令保持在元素上直到关联组件实例结束编译。通常与 CSS 规则一起使用,以隐藏未编译的 Mustache 标签。html<div v-cloak> {{ message }} </div>
css[v-cloak] { display: none; }
-
v-once
:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。html<span v-once>{{ message }}</span>
-
v-memo
(Vue3.2+):缓存模板,避免不必要更新 。html<div v-memo="[valueA, valueB]"> {{ expensiveCalc}} </div>
自定义指令
(1)** 核心原理**
自定义指令本质是一个 包含生命周期钩子的对象,Vue 会在特定时机自动调用这些钩子,并注入以下关键参数:
el
:指令绑定的 DOM 元素(可直接操作)binding
:一个对象,包含以下属性:value
:指令的绑定值(如v-my-directive="123"
中的123
)arg
:指令的参数(如v-my-directive:foo
中的"foo"
)modifiers
:修饰符对象(如v-my-directive.modifier
中的{ modifier: true }
)
js
// 示例:打印 binding 对象
app.directive('demo', {
mounted(el, binding) {
console.log(binding.value) // 输出指令值
console.log(binding.arg) // 输出参数
console.log(binding.modifiers) // 输出修饰符
}
})
(2)全局注册
javascript
const app = Vue.createApp({});
app.directive('focus', {
mounted(el) {
el.focus(); // 自动聚焦
}
});
app.mount('#app');
- 使用:
html
<input v-focus>
(3)局部注册(<script setup>
)
vue
<script setup>
const vHighlight = {
mounted(el, binding) {
el.style.backgroundColor = binding.value || 'yellow'
}
}
</script>
<template>
<div v-highlight="'#ff0'">高亮背景</div>
</template>
(4)指令生命周期(钩子函数)
钩子 | 触发时机 |
---|---|
created |
元素属性初始化,但未挂载 |
beforeMount |
绑定到 DOM 前 |
mounted |
元素插入父 DOM 后 |
beforeUpdate |
所在组件更新前 |
updated |
所在组件更新后 |
beforeUnmount |
卸载前 |
unmounted |
卸载后 |
示例:自定义指令 v-color
javascript
const app = Vue.createApp({});
app.directive('color', {
mounted(el, binding) {
el.style.color = binding.value;
},
updated(el, binding) {
el.style.color = binding.value;
}
});
app.mount('#app');
html
<p v-color="'red'">This text is red.</p>
在这个例子中,v-color
指令会根据传入的值动态改变文本的颜色。
为何 v-for 需要使用 key
在 Vue.js 中,使用 v-for
渲染列表时,添加 key 属性是一个重要的最佳实践。
- 提高性能 :当 Vue 更新视图时,它会根据
key
来识别哪些元素被修改、添加或移除。如果没有key
,Vue 会依赖其默认的算法(基于元素的位置)来比较元素,这样可能导致不必要的 DOM 操作。使用key
后,Vue 能精确地找到每个项,从而减少不必要的 DOM 重排和重绘,提升性能。 - 保持组件状态 :如果渲染的是一个组件(而不是普通的 DOM 元素),使用
key
可以确保组件在渲染更新时保持正确的状态。例如,如果列表中有表单输入框,每个输入框都有自己的状态,使用key
可以确保输入框状态不会因列表排序或元素移除而丢失。 - 避免渲染错误:key 的存在可以帮助 Vue 确保在列表更新时,元素的顺序和内容保持稳定,避免出现不稳定的渲染或顺序错乱。
路由传参
1. 动态路由参数(Params)
动态路由参数是通过路由路径中的占位符传递的。例如,定义一个动态路由:
定义动态路由
在路由配置中,使用 :
来定义动态参数:
javascript
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/user/:id', // 动态路由参数
component: () => import('@/views/User.vue'),
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
传递参数
在跳转时,可以通过 params
传递参数:
javascript
// 使用 router.push 跳转
this.$router.push({ name: 'user', params: { id: 123 } });
// 或者在模板中使用 <router-link>
<router-link :to="{ name: 'user', params: { id: 123 }}">User</router-link>
获取参数
在目标组件中,可以过 useRoute
或 this.$route
获取参数:
javascript
import { useRoute } from 'vue-router';
export default {
setup() {
const route = useRoute();
const userId = route.params.id; // 获取动态参数
return { userId };
},
};
2. 查询参数(Query)
查询参数是通过 URL 中的 ?
后面的键值对传递的。例如:/user?id=123
。
传递查询参数
在跳转时,可以通过 query
传递参数:
javascript
// 使用 router.push 跳转
this.$router.push({ path: '/user', query: { id: 123 } });
// 或者在模板中使用 <router-link>
<router-link :to="{ path: '/user', query: { id: 123 }}">User</router-link>
获取查询参数
在目标组件中,可以过 useRoute
或 this.$route
获取查询参数:
javascript
import { useRoute } from 'vue-router';
export default {
setup() {
const route = useRoute();
const userId = route.query.id; // 获取查询参数
return { userId };
},
};
3. 通过 props
传递参数
Vue Router 允许将路由参数作为组件的 props
传递,这样可以更方便地在组件中使用参数。
配置路由
在路由配置中,启用 props
:
javascript
const routes = [
{
path: '/user/:id',
component: () => import('@/views/User.vue'),
props: true, // 将 params 作为 props 传递
},
{
path: '/profile',
component: () => import('@/views/Profile.vue'),
props: (route) => ({ query: route.query }), // 自定义 props
},
];
在组件中使用 props
在目标组件中,可以直接通过 props
接收参数:
javascript
export default {
props: ['id'], // 接收动态路由参数
setup(props) {
console.log(props.id); // 使用 props
},
};
4. 通过 state
传递参数
Vue Router 还支持通过 state
传递参数,这种方式不会在 URL 中暴露参数。
传递 state
在跳转时,可以通过 state
传递参数:
javascript
this.$router.push({
path: '/user',
state: { id: 123 }, // 通过 state 传递参数
});
获取 state
在目标组件中,可以通过 history.state
获取参数:
javascript
export default {
setup() {
const userId = history.state.id; // 获取 state 参数
return { userId };
},
};
总结
方式 | 特点 | 示例 URL |
---|---|---|
动态路由参数(Params) | 参数是 URL 的一部分,适合传递必要参数 | /user/123 |
查询参数(Query) | 参数通过 ? 传递,适合传递可选参数 |
/user?id=123 |
props 传递参数 |
将参数作为组件的 props 传递,代码更清晰 |
/user/123 |
state 传递参数 |
参数不会暴露在 URL 中,适合传递敏感或不希望暴露的数据 | /user |
使用场景
- 动态路由参数:适合传递必要参数,如用户 ID、文章 ID 等。
- 查询参数:适合传递可选参数,如分页、排序等。
props
传递参数:适合将路由参数与组件解耦,使代码更清晰。state
传递参数:适合传递敏感数据或不希望暴露在 URL 中的数据。
template标签 会编译成什么
在 Vue 3 中,<template>
标签是一个特殊的标签,用于包裹模板内容,但它本身不会被渲染到最终的 DOM 中。Vue 的编译器会将 <template>
标签中的内容编译成 渲染函数(Render Function) ,最终生成虚拟 DOM(Virtual DOM),然后由 Vue 的运行时将其渲染为真实的 DOM。
1. 模板解析
Vue 的编译器会解析 <template>
中的内容,生成一个抽象语法树(AST,Abstract Syntax Tree)。
例如:
html
<template>
<div>{{ message }}</div>
</template>
生成的 AST 可能类似于:
javascript
{
type: 'element',
tag: 'div',
children: [
{
type: 'expression',
content: 'message'
}
]
}
2. 生成渲染函数
Vue 将 AST 转换为渲染函数。渲染函数是一个 JavaScript 函数,用于生成虚拟 DOM。
例如,上述模板会被编译为:
javascript
function render() {
return h('div', this.message);
}
3. 生成虚拟 DOM
渲染函数执行后,会生成虚拟 DOM 节点。虚拟 DOM 是一个轻量级的 JavaScript 对象,描述了真实 DOM 的结构。
例如:
javascript
{
tag: 'div',
children: 'Hello, Vue!'
}
4. 渲染为真实 DOM
Vue 的运行时将虚拟 DOM 渲染为真实 DOM,并插入到页面中。
总结
- Vue 的
<template>
是一个 编译时的占位符,最终会被转换为虚拟 DOM 的渲染逻辑。 - 它不会生成真实的 DOM 节点,主要用于组织模板结构或配合指令控制渲染流程。
- 与 HTML 标准的
<template>
标签不同,前者是 Vue 模板语法的一部分,后者是浏览器原生支持的惰性模板。
监听键盘事件
1. 使用 v-on
指令(@
简写)
Vue 提供了 v-on
指令(或 @
简写)来监听 DOM 事件,包括键盘事件。常用的键盘事件有:
keydown
:按下键盘时触发。keyup
:松开键盘时触发。keypress
:按下并松开键盘时触发(不推荐使用,已被废弃)。
示例:监听 keydown
事件
html
<template>
<input type="text" @keydown="handleKeyDown" />
</template>
<script>
export default {
methods: {
handleKeyDown(event) {
console.log('按下的键:', event.key);
console.log('键码:', event.keyCode);
},
},
};
</script>
2. 监听特定按键
可以通过事件对象的 key
或 keyCode
属性来判断按下的具体按键。
示例:监听回车键(Enter)
html
<template>
<input type="text" @keyup.enter="handleEnter" />
</template>
<script>
export default {
methods: {
handleEnter() {
console.log('按下了回车键');
},
},
};
</script>
示例:监听组合键(如 Ctrl + S
)
html
<template>
<input type="text" @keydown.ctrl.s="handleSave" />
</template>
<script>
export default {
methods: {
handleSave(event) {
event.preventDefault(); // 阻止默认行为
console.log('按下了 Ctrl + S');
},
},
};
</script>
3. 使用修饰符
Vue 提供了一些键盘事件的修饰符,可以更方便地监听特定按键:
.enter
:回车键。.tab
:Tab 键。.delete
:删除键或退格键。.esc
:Esc 键。.space
:空格键。.up
:上箭头键。.down
:下箭头键。.left
:左箭头键。.right
:右箭头键。.ctrl
:Ctrl 键。.alt
:Alt 键。.shift
:Shift 键。.meta
:Windows 键或 Command 键。
示例:使用修饰符监听特定按键
html
<template>
<input type="text" @keyup.esc="handleEscape" />
</template>
<script>
export default {
methods: {
handleEscape() {
console.log('按下了 Esc 键');
},
},
};
</script>
4. 全局监听键盘事件
如果需要在组件外部或全局监听键盘事件,可以直接在 mounted
钩子中添加事件监听器,在 unmounted
钩子中移除。
示例:全局监听 keydown
事件
html
<template>
<div>
<p>按下任意键查看键码</p>
</div>
</template>
<script>
export default {
mounted() {
window.addEventListener('keydown', this.handleGlobalKeyDown);
},
unmounted() {
window.removeEventListener('keydown', this.handleGlobalKeyDown);
},
methods: {
handleGlobalKeyDown(event) {
console.log('全局按下的键:', event.key);
},
},
};
</script>
5. 使用第三方库
如果需要更复杂的键盘事件处理(例如监听组合键、长按等),可以使用第三方库,如:
示例:使用 Vueuse 监听键盘事件
bash
npm install @vueuse/core
html
<template>
<div>
<p>按下 Enter 键查看效果</p>
</div>
</template>
<script>
import { useKeyPress } from '@vueuse/core';
export default {
setup() {
useKeyPress('Enter', () => {
console.log('按下了 Enter 键');
});
},
};
</script>
总结
方法 | 适用场景 | 示例 |
---|---|---|
v-on 指令 |
监听单个元素的键盘事件 | @keydown="handleKeyDown" |
修饰符 | 监听特定按键 | @keyup.enter="handleEnter" |
全局监听 | 监听全局键盘事件 | window.addEventListener |
第三方库(如 Vueuse) | 复杂键盘事件处理(组合键、长按等) | useKeyPress('Enter', callback) |
null 和 undefined
特性 | undefined |
null |
---|---|---|
含义 | 表示未定义的值 | 表示空值或无对象引用 |
类型 | undefined |
object (历史遗留问题) |
使用场景 | 变量未初始化、函数无返回值、属性不存在 | 显式表示"无"或"空" |
相等性 | null == undefined 为 true |
null === undefined 为 false |
类型检查 | typeof undefined 为 "undefined" |
typeof null 为 "object" |
为什么 typeof null
返回 "object"
?
1. JavaScript 的类型标签机制
在 JavaScript 的早期实现中,值的类型信息是通过一个 类型标签 来存储的。这个标签通常是一个低位的二进制值,用于标识值的类型。
- 对象的类型标签是
000
。 null
的二进制表示是全0
(即00000000
)。- 由于
null
的二进制表示与对象的类型标签相同,typeof
错误地将null
识别为object
。
2. 历史遗留问题
这个问题在 JavaScript 的早期版本中就已经存在,但由于修复这个问题可能会导致大量现有代码无法正常运行,因此 JavaScript 的标准化组织(ECMA)决定保留这一行为,以避免破坏现有的 Web 应用。
vue 的响应式api
一、Vue 2 响应式原理(基于 Object.defineProperty
)
javascript
// 简化版实现原理
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`读取 ${key}: ${val}`);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log(`设置 ${key}: ${newVal}`);
val = newVal;
}
}
});
}
const data = { count: 0 };
defineReactive(data, 'count', data.count);
data.count++; // 触发 setter
局限性:
- 无法检测对象属性的添加/删除
- 数组变异方法需要特殊处理(如
push
,pop
)
二、Vue 3 响应式 API(基于 Proxy
)
Vue 3 通过 Proxy
实现更强大的响应式系统:
1. reactive
reactive
用于创建一个响应式的对象。它返回一个对象的代理(Proxy),当对象的属性被访问或修改时,Vue 可以自动追踪依赖并触发更新。
用法
javascript
import { reactive } from 'vue';
const state = reactive({
count: 0,
message: 'Hello, Vue 3!',
});
console.log(state.count); // 0
state.count++; // 修改属性,触发响应式更新
特点
- 只适用于对象(包括数组)。
- 嵌套对象也会被递归地转换为响应式对象。
2. ref
ref
用于创建一个响应式的基本类型值 (如 number
、string
、boolean
等)。它返回一个包含 value
属性的对象,通过 value
属性访问或修改值。
用法
javascript
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // 0
count.value++; // 修改值,触发响应式更新
特点
- 适用于基本类型值。
- 在模板中使用时,Vue 会自动解包
ref
,无需通过.value
访问。
3. computed
computed
用于创建一个计算属性。它接收一个 getter 函数,并返回一个只读的响应式引用(ref
)。当依赖的响应式数据发生变化时,计算属性会自动重新计算。
用法
javascript
import { reactive, computed } from 'vue';
const state = reactive({
count: 0,
});
const doubleCount = computed(() => state.count * 2);
console.log(doubleCount.value); // 0
state.count++;
console.log(doubleCount.value); // 2
特点
- 计算属性是惰性的,只有在依赖的响应式数据变化时才会重新计算。
- 可以设置
setter
来支持双向绑定。
4. watch
watch
用于监听响应式数据的变化,并在变化时执行回调函数。
用法
javascript
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`);
});
count.value++; // 触发 watch 回调
特点
- 可以监听
ref
、reactive
对象或计算属性。 - 支持深度监听(
deep: true
)。
5. watchEffect
watchEffect
用于立即执行一个函数,并自动追踪函数中使用的响应式数据。当依赖的响应式数据变化时,函数会重新执行。
用法
javascript
import { ref, watchEffect } from 'vue';
const count = ref(0);
watchEffect(() => {
console.log(`count is ${count.value}`);
});
count.value++; // 触发 watchEffect
特点
- 不需要显式指定依赖,自动追踪。
- 适合处理副作用逻辑(如异步请求、DOM 操作等)。
6. toRef
和 toRefs
toRef
:将响应式对象的某个属性转换为ref
。toRefs
:将响应式对象的所有属性转换为ref
。
用法
javascript
import { reactive, toRef, toRefs } from 'vue';
const state = reactive({
count: 0,
message: 'Hello',
});
const countRef = toRef(state, 'count'); // 将 count 转换为 ref
const { count, message } = toRefs(state); // 将所有属性转换为 ref
console.log(countRef.value); // 0
console.log(count.value); // 0
console.log(message.value); // Hello
特点
- 用于解构响应式对象时保持响应性。
- 常用于组合式 API 中返回响应式数据。
7. readonly
readonly
用于创建一个只读的响应式对象。任何尝试修改只读对象的操作都会失败。
用法
javascript
import { reactive, readonly } from 'vue';
const state = reactive({ count: 0 });
const readonlyState = readonly(state);
console.log(readonlyState.count); // 0
readonlyState.count++; // 报错:无法修改只读对象
特点
- 适用于保护数据不被意外修改。
8. shallowReactive
和 shallowRef
shallowReactive
:创建一个浅层响应式对象,只有顶层属性是响应式的。shallowRef
:创建一个浅层ref
,只有.value
是响应式的。
用法
javascript
import { shallowReactive, shallowRef } from 'vue';
const state = shallowReactive({
count: 0,
nested: { value: 1 }, // nested 不是响应式的
});
const countRef = shallowRef(0);
countRef.value++; // 响应式
特点
- 适用于性能优化,避免不必要的深度响应式转换。
9. triggerRef
triggerRef
用于手动触发 shallowRef
的更新。
用法
javascript
import { shallowRef, triggerRef } from 'vue';
const countRef = shallowRef(0);
countRef.value = 1; // 不会触发更新
triggerRef(countRef); // 手动触发更新
10. markRaw
markRaw
用于标记一个对象,使其不会被转换为响应式对象。
用法
javascript
import { reactive, markRaw } from 'vue';
const rawObject = markRaw({ value: 1 });
const state = reactive({
rawObject, // rawObject 不会被转换为响应式
});
三、最佳实践建议
-
优先选择:
- 对象/数组用
reactive
- 基本类型用
ref
- 组合逻辑时用
toRefs
解构
- 对象/数组用
-
避免陷阱:
javascript// 错误:直接替换 reactive 对象 state = { ...state, newProp: 1 }; // ❌ 失去响应式 // 正确:修改属性 Object.assign(state, { newProp: 1 }); // ✅ // 正确:使用 ref 包装对象 const objRef = ref({ foo: 'bar' }); objRef.value.foo = 'new'; // ✅
-
性能优化:
- 大数据量时用
shallowRef
/shallowReactive
- 频繁更新的数据用
ref
而非reactive
- 使用
markRaw
跳过不需要响应式的数据
- 大数据量时用
四、与 Vue 2 的对比
特性 | Vue 2 | Vue 3 |
---|---|---|
响应式核心 | Object.defineProperty |
Proxy |
数组监听 | 需要特殊处理 | 原生支持 |
新增属性 | 需要 Vue.set |
直接添加即可响应 |
性能 | 中等 | 提升 1.3~2 倍 |
五、常见问题
Q:为什么需要 ref
?
- 用于包装基本类型值(
string
,number
,boolean
) - 统一响应式对象的访问方式(通过
.value
)
Q:reactive
和 ref
如何选择?
- 对象/数组 →
reactive
- 基本类型/需要灵活替换 →
ref
Q:如何避免深层响应式?
javascript
const shallow = shallowReactive({ nested: { data: 1 } });
shallow.nested.data = 2; // 不会触发响应
用过ts的哪些特性
一、核心类型系统
1. 类型注解与类型推断
typescript
// 显式类型注解
let count: number = 0;
// 类型推断自动推导类型(推导为 string)
const message = "Hello TS";
// 函数参数与返回值类型
function add(x: number, y: number): number {
return x + y;
}
应用场景:
- 强制接口参数类型安全
- 避免因隐式类型转换导致的 bug
- 结合 Vue 的 Prop 类型验证 (
defineProps<{id: number}>()
)
2. 接口(Interface)与类型别名(Type Alias)
typescript
// 接口定义对象结构
interface User {
id: number;
name: string;
email?: string; // 可选属性
}
// 类型别名定义复杂类型
type ApiResponse<T> = {
code: number;
data: T;
error?: string;
};
// Vue 组件 Props 类型
defineProps<{
user: User;
onSuccess: (result: ApiResponse<boolean>) => void;
}>();
应用场景:
- 定义 API 响应格式
- 约束组件 Props/Emits 类型
- 规范 Redux/Vuex 的 State 结构
3. 泛型(Generics)
typescript
// 泛型函数
function identity<T>(arg: T): T {
return arg;
}
// 泛型接口
interface Pagination<T> {
list: T[];
total: number;
}
// Vue 组合式函数中的泛型
function useFetch<T>(url: string): Promise<T> {
return fetch(url).then(res => res.json());
}
应用场景:
- 封装可复用的数据请求 Hook
- 处理响应式数据 (
Ref<T>
,Reactive<T>
) - 构建通用工具函数库
二、高级类型操作
1. 联合类型与类型守卫
typescript
type Status = 'loading' | 'success' | 'error';
function handleStatus(status: Status) {
if (status === 'loading') {
// 类型守卫下自动推导为特定字面量类型
console.log('加载中...');
}
}
应用场景:
- Redux/Vuex 的 Action 类型区分
- 处理 API 状态机逻辑
- 联合类型 + 类型收窄代替枚举
2. 工具类型(Utility Types)
typescript
interface User {
id: number;
name: string;
role: 'admin' | 'user';
}
// 生成新类型
type UserPreview = Pick<User, 'id' | 'name'>;
type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;
// Vue 中的 emits 类型约束
const emit = defineEmits<{
(e: 'update:name', value: string): void;
}>();
应用场景:
- 快速生成表单的 Partial 类型
- 限制组件事件触发的参数类型
- 避免重复定义相似类型
3. 类型断言与守卫
typescript
// 类型断言(慎用)
const element = document.getElementById('root') as HTMLElement;
// 自定义类型守卫
function isErrorResponse(res: any): res is ApiResponse<never> {
return res.code >= 400;
}
应用场景:
- 处理第三方库未提供类型的对象
- 复杂运行时类型校验(如 API 响应格式验证)
三、工程化特性
1. 声明文件(.d.ts)
typescript
// 扩展第三方库类型
declare module 'untyped-lib' {
export function parse(input: string): Record<string, any>;
}
// 全局类型声明
declare interface Window {
__DEV__: boolean;
}
应用场景:
- 为无类型声明的老旧库补充类型
- 定义全局变量类型(如注入的环境变量)
2. 装饰器(Decorators)
typescript
// 类装饰器(Vue Class Component)
@Component
class MyComp extends Vue {
@Prop({ type: Number, required: true })
readonly id!: number;
}
应用场景:
- 旧版 Vue Class Component 开发
- NestJS 等后端框架的控制器定义
四、最新特性实践
1. 模板字面量类型(Template Literal Types)
typescript
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiPath = `/api/v1/${string}`;
function request(method: HttpMethod, url: ApiPath) { /* ... */ }
request('POST', '/api/v1/users'); // ✅
request('GET', '/other/path'); // ❌ 类型错误
应用场景:
- 强约束 API 路径格式
- 生成动态 CSS 类名类型
2. satisfies 运算符(TS 4.9+)
typescript
const config = {
theme: 'dark',
layout: 'grid'
} satisfies Record<string, string>;
// 自动推导出 theme 和 layout 的字符串类型
高频使用总结
特性分类 | 使用频率 | 典型场景 |
---|---|---|
接口 & 类型别名 | ⭐⭐⭐⭐⭐ | 组件 Props、API 数据结构定义 |
泛型 | ⭐⭐⭐⭐ | 工具函数、Hooks 封装 |
联合类型 | ⭐⭐⭐⭐ | 状态机、Action 类型区分 |
工具类型 | ⭐⭐⭐ | 快速生成衍生类型 |
类型断言 | ⭐⭐ | 兼容无类型库、DOM 操作 |
装饰器 | ⭐ | 旧版类式组件开发 |
避坑经验
-
避免过度使用
any
优先使用
unknown
+ 类型守卫,或通过泛型传递类型信息 -
区分
interface
和type
- 接口:扩展性强(
extends
),适合描述对象形状 - 类型别名:支持联合/交叉类型,适合复杂类型组合
- 接口:扩展性强(
-
Vue 3 TS 最佳实践
typescript// 推荐使用 defineProps 泛型 const props = defineProps<{ id: number; list: Array<{ title: string }>; }>(); // 精确的 emit 类型 const emit = defineEmits<{ (e: 'update', value: number): void; }>();
浏览器沙箱机制
浏览器沙箱机制 是一种安全机制,用于隔离 Web 应用程序的运行环境,防止恶意代码对用户系统造成损害。
特性 | 说明 |
---|---|
核心思想 | 隔离 Web 应用程序的运行环境,限制其访问系统资源。 |
作用 | 防止恶意代码、保护用户隐私、隔离运行环境。 |
实现方式 | 进程隔离、同源策略、CSP、权限控制、沙箱化的 iframe。 |
局限性 | 浏览器漏洞、用户操作、XSS 攻击。 |
增强安全性 | 使用最新浏览器、实施 CSP、遵循最小权限原则、用户教育。 |
虚拟列表
虚拟列表(Virtual List) 是一种优化长列表渲染性能的技术,通过只渲染当前可见区域的内容,减少 DOM 元素的数量,从而提升页面的渲染效率和用户体验。虚拟列表通常用于处理包含大量数据的列表(如聊天记录、表格数据、社交媒体动态等)。
1. 为什么需要虚拟列表?
在传统的列表渲染中,所有数据都会被一次性渲染到 DOM 中。当数据量很大时,会导致以下问题:
- DOM 元素过多:大量 DOM 元素会占用大量内存,导致页面卡顿。
- 渲染性能低下:浏览器需要处理大量的布局和绘制操作,影响页面性能。
- 用户体验差:页面加载慢,滚动卡顿,交互不流畅。
虚拟列表通过只渲染可见区域的内容,解决了这些问题。
2. 虚拟列表的核心思想
虚拟列表的核心思想是:
- 只渲染可见区域的内容:根据滚动位置计算当前可见的数据项,只渲染这些数据项。
- 动态更新内容:当用户滚动时,动态更新可见区域的内容。
实现
虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域
内需要的列表项,当滚动发生时,动态通过计算获得可视区域
内的列表项,并将非可视区域
内存在的列表项删除。
- 计算当前
可视区域
起始数据索引(startIndex
) - 计算当前
可视区域
结束数据索引(endIndex
) - 计算当前
可视区域的
数据,并渲染到页面中 - 计算
startIndex
对应的数据在整个列表中的偏移位置startOffset
并设置到列表上
websocket断开了该如何解决
1. 检测 WebSocket 断开
WebSocket 提供了以下事件来检测连接状态:
onclose
:当连接关闭时触发。onerror
:当发生错误时触发。
示例
javascript
const socket = new WebSocket('wss://example.com');
socket.onclose = (event) => {
console.log('WebSocket 断开:', event);
// 在这里处理断开逻辑
};
socket.onerror = (error) => {
console.error('WebSocket 错误:', error);
// 在这里处理错误逻辑
};
2. 自动重连机制
当 WebSocket 断开时,可以通过以下方式实现自动重连:
(1) 简单重连
在 onclose
事件中设置重连逻辑。
javascript
let socket;
const url = 'wss://example.com';
function connect() {
socket = new WebSocket(url);
socket.onopen = () => {
console.log('WebSocket 连接成功');
};
socket.onclose = (event) => {
console.log('WebSocket 断开,尝试重连...');
setTimeout(connect, 3000); // 3 秒后重连
};
socket.onerror = (error) => {
console.error('WebSocket 错误:', error);
socket.close(); // 关闭连接,触发 onclose
};
}
connect();
(2) 指数退避重连
为了避免频繁重连,可以使用指数退避策略(Exponential Backoff)。
javascript
let socket;
const url = 'wss://example.com';
let reconnectDelay = 1000; // 初始重连延迟
function connect() {
socket = new WebSocket(url);
socket.onopen = () => {
console.log('WebSocket 连接成功');
reconnectDelay = 1000; // 重置重连延迟
};
socket.onclose = (event) => {
console.log('WebSocket 断开,尝试重连...');
setTimeout(connect, reconnectDelay);
reconnectDelay *= 2; // 延迟时间翻倍
if (reconnectDelay > 30000) {
reconnectDelay = 30000; // 最大延迟 30 秒
}
};
socket.onerror = (error) => {
console.error('WebSocket 错误:', error);
socket.close(); // 关闭连接,触发 onclose
};
}
connect();
3. 处理网络问题
(1) 检测网络状态
使用 navigator.onLine
检测网络连接状态。
javascript
window.addEventListener('online', () => {
console.log('网络已恢复');
connect(); // 重新连接 WebSocket
});
window.addEventListener('offline', () => {
console.log('网络已断开');
socket.close(); // 关闭 WebSocket
});
(2) 心跳机制
通过定时发送心跳包(Ping/Pong)检测连接状态。
javascript
let heartbeatInterval;
socket.onopen = () => {
console.log('WebSocket 连接成功');
heartbeatInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send('ping'); // 发送心跳包
}
}, 30000); // 每 30 秒发送一次
};
socket.onclose = () => {
clearInterval(heartbeatInterval); // 清除心跳定时器
};
4. 服务器端处理
(1) 服务器重连
确保服务器支持客户端重连,并在客户端重连时恢复会话状态。
(2) 负载均衡
如果使用负载均衡,确保 WebSocket 连接被正确路由到同一台服务器。
(3) 连接超时
设置合理的连接超时时间,避免长时间占用资源。
5. 调试和日志
(1) 记录日志
记录 WebSocket 的连接、断开和错误信息,便于排查问题。
javascript
socket.onclose = (event) => {
console.log('WebSocket 断开:', event.code, event.reason);
};
socket.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
(2) 调试工具
使用浏览器开发者工具(如 Chrome DevTools)查看 WebSocket 的连接状态和数据传输。
6. 总结
方法 | 说明 |
---|---|
检测断开 | 使用 onclose 和 onerror 事件检测连接状态。 |
自动重连 | 在 onclose 中实现重连逻辑,支持简单重连和指数退避重连。 |
处理网络问题 | 检测网络状态,使用心跳机制保持连接。 |
服务器端处理 | 确保服务器支持重连,设置合理的超时时间。 |
调试和日志 | 记录日志,使用开发者工具调试 WebSocket 连接。 |
组件销毁
在 Vue.js 中,组件销毁是指组件实例从 DOM 中移除并释放资源的过程。组件销毁时,Vue 会触发一系列生命周期钩子,开发者可以在这些钩子中执行清理操作(如取消定时器、解绑事件、释放内存等)。
1. 组件销毁的生命周期钩子
Vue 提供了以下生命周期钩子,用于处理组件销毁时的逻辑:
(1) beforeDestroy
- 在组件销毁之前调用。
- 适合执行一些清理操作,如取消定时器、解绑事件等。
(2) destroyed
- 在组件销毁之后调用。
- 此时,组件已经从 DOM 中移除,所有的事件监听器和子组件也已被销毁。
2. 组件销毁的常见场景
(1) 路由切换
当使用 Vue Router 切换路由时,当前组件会被销毁。
(2) 条件渲染
当使用 v-if
或 v-show
控制组件显示时,条件为 false
时组件会被销毁。
(3) 动态组件
当使用 <component :is="...">
切换动态组件时,旧组件会被销毁。
(4) 手动销毁
通过 $destroy
方法手动销毁组件。
3. 组件销毁时的清理操作
(1) 取消定时器
如果组件中设置了定时器(如 setInterval
),需要在组件销毁时清除,否则会导致内存泄漏。
javascript
export default {
data() {
return {
timer: null,
};
},
mounted() {
this.timer = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
},
beforeDestroy() {
clearInterval(this.timer); // 清除定时器
},
};
(2) 解绑事件
如果组件中绑定了全局事件(如 window.addEventListener
),需要在组件销毁时解绑。
javascript
export default {
mounted() {
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize); // 解绑事件
},
methods: {
handleResize() {
console.log('窗口大小改变');
},
},
};
(3) 取消异步请求
如果组件中发起了异步请求(如 Axios),可以在组件销毁时取消请求。
javascript
import axios from 'axios';
export default {
data() {
return {
cancelToken: null,
};
},
methods: {
fetchData() {
this.cancelToken = axios.CancelToken.source();
axios.get('/api/data', {
cancelToken: this.cancelToken.token,
});
},
},
beforeDestroy() {
if (this.cancelToken) {
this.cancelToken.cancel('组件销毁,取消请求'); // 取消请求
}
},
};
(4) 释放资源
如果组件中使用了第三方库或创建了对象,需要在组件销毁时释放资源。
javascript
export default {
mounted() {
this.chart = new Chart(...); // 初始化图表
},
beforeDestroy() {
this.chart.destroy(); // 销毁图表
},
};
4. 手动销毁组件
可以通过 $destroy
方法手动销毁组件。
javascript
export default {
methods: {
destroyComponent() {
this.$destroy(); // 手动销毁组件
},
},
};
5. 总结
场景 | 说明 |
---|---|
生命周期钩子 | beforeDestroy 和 destroyed 用于处理组件销毁逻辑。 |
常见场景 | 路由切换、条件渲染、动态组件、手动销毁。 |
清理操作 | 取消定时器、解绑事件、取消异步请求、释放资源。 |
手动销毁 | 使用 $destroy 方法手动销毁组件。 |
路由守卫
路由守卫的使用
(1) 全局前置守卫(beforeEach
)
在路由导航之前进行权限验证。
javascript
const router = new VueRouter({ ... });
router.beforeEach((to, from, next) => {
const isAuthenticated = checkAuth(); // 检查用户是否登录
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login'); // 跳转到登录页
} else {
next(); // 继续导航
}
});
(2) 全局解析守卫(beforeResolve
)
确保所有数据已加载后再进入路由。
javascript
router.beforeResolve((to, from, next) => {
if (to.meta.requiresData) {
fetchData().then(() => next()); // 加载数据
} else {
next();
}
});
(3) 全局后置钩子(afterEach
)
用于页面访问统计或日志记录。
javascript
router.afterEach((to, from) => {
logPageView(to.path); // 记录页面访问
});
(4) 路由独享守卫(beforeEnter
)
在进入特定路由之前触发。
javascript
const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
const isAdmin = checkAdmin(); // 检查用户是否是管理员
if (isAdmin) {
next();
} else {
next('/403'); // 跳转到无权限页面
}
},
},
];
(5) 组件内守卫
在组件内部定义路由守卫。
javascript
export default {
beforeRouteEnter(to, from, next) {
// 在进入组件之前触发
next((vm) => {
// 可以访问组件实例 `vm`
});
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变但组件复用时触发
next();
},
beforeRouteLeave(to, from, next) {
// 在离开组件之前触发
const confirmLeave = confirm('确定离开吗?');
if (confirmLeave) {
next();
} else {
next(false); // 取消导航
}
},
};
路由守卫的参数
路由守卫的回调函数接收以下参数:
-
to
:目标路由对象。 -
from
:当前路由对象。 -
next
:控制导航行为的函数。next()
:继续导航。next(false)
:取消导航。next('/path')
:跳转到指定路径。next(error)
:导航失败,触发错误。
路由守卫的应用场景
(1) 登录验证
在全局前置守卫中检查用户是否登录。
javascript
router.beforeEach((to, from, next) => {
const isAuthenticated = checkAuth();
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login');
} else {
next();
}
});
(2) 权限控制
在路由独享守卫中检查用户权限。
javascript
const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
const isAdmin = checkAdmin();
if (isAdmin) {
next();
} else {
next('/403');
}
},
},
];
(3) 数据预加载
在组件内守卫中加载数据。
javascript
export default {
beforeRouteEnter(to, from, next) {
fetchData().then((data) => {
next((vm) => {
vm.data = data; // 将数据传递给组件实例
});
});
},
};
(4) 页面访问统计
在全局后置钩子中记录页面访问。
javascript
router.afterEach((to, from) => {
logPageView(to.path);
});
实现三栏布局
1. 使用浮动(Float)
通过 float
属性实现三栏布局,左右两栏固定宽度,中间栏自适应。
HTML 结构
html
<div class="container">
<div class="left">左侧栏</div>
<div class="right">右侧栏</div>
<div class="center">中间内容</div>
</div>
CSS 样式
css
.container {
overflow: hidden; /* 清除浮动 */
}
.left {
float: left;
width: 200px;
background-color: #f0f0f0;
}
.right {
float: right;
width: 200px;
background-color: #f0f0f0;
}
.center {
margin-left: 200px; /* 左侧栏宽度 */
margin-right: 200px; /* 右侧栏宽度 */
background-color: #ccc;
}
2. 使用 Flexbox
通过 flex
布局实现三栏布局,中间栏自适应宽度。
HTML 结构
html
<div class="container">
<div class="left">左侧栏</div>
<div class="center">中间内容</div>
<div class="right">右侧栏</div>
</div>
CSS 样式
css
.container {
display: flex;
}
.left {
width: 200px;
background-color: #f0f0f0;
}
.right {
width: 200px;
background-color: #f0f0f0;
}
.center {
flex: 1; /* 中间栏自适应 */
background-color: #ccc;
}
3. 使用 Grid
通过 grid
布局实现三栏布局,代码简洁且功能强大。
HTML 结构
html
<div class="container">
<div class="left">左侧栏</div>
<div class="center">中间内容</div>
<div class="right">右侧栏</div>
</div>
CSS 样式
css
.container {
display: grid;
grid-template-columns: 200px 1fr 200px; /* 三列布局 */
}
.left {
background-color: #f0f0f0;
}
.right {
background-color: #f0f0f0;
}
.center {
background-color: #ccc;
}
4. 使用绝对定位
通过 position: absolute
实现三栏布局,左右两栏固定宽度,中间栏自适应。
HTML 结构
html
<div class="container">
<div class="left">左侧栏</div>
<div class="center">中间内容</div>
<div class="right">右侧栏</div>
</div>
CSS 样式
css
.container {
position: relative;
}
.left {
position: absolute;
left: 0;
width: 200px;
background-color: #f0f0f0;
}
.right {
position: absolute;
right: 0;
width: 200px;
background-color: #f0f0f0;
}
.center {
margin-left: 200px; /* 左侧栏宽度 */
margin-right: 200px; /* 右侧栏宽度 */
background-color: #ccc;
}
5. 使用表格布局
通过 display: table
实现三栏布局,兼容性好但灵活性较差。
HTML 结构
html
<div class="container">
<div class="left">左侧栏</div>
<div class="center">中间内容</div>
<div class="right">右侧栏</div>
</div>
CSS 样式
css
.container {
display: table;
width: 100%;
}
.left, .center, .right {
display: table-cell;
}
.left {
width: 200px;
background-color: #f0f0f0;
}
.right {
width: 200px;
background-color: #f0f0f0;
}
.center {
background-color: #ccc;
}
6. 总结
方法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
浮动(Float) | 兼容性好 | 需要清除浮动,代码较复杂 | 传统布局,兼容旧浏览器 |
Flexbox | 代码简洁,布局灵活 | 兼容性较差(IE9 以下不支持) | 现代浏览器,响应式布局 |
Grid | 功能强大,代码简洁 | 兼容性较差(IE11 部分支持) | 现代浏览器,复杂布局 |
绝对定位 | 布局精确 | 需要手动计算间距,灵活性差 | 固定宽度布局 |
表格布局 | 兼容性好 | 语义化差,灵活性差 | 传统布局,兼容旧浏览器 |