春招面试拷打实录(三):面试血泪史,面经干货分享!!!

引言

经历了几场面试的"狂轰滥炸",这才发现准备不足简直是自寻死路啊!痛定思痛之后,我决定把近几天遇到的"面试怪兽"------从刁钻问题到各种挑战,浓缩成一份精华面经。废话免谈,直接端上干货。希望这份面经能成为你求职路上的秘密武器,助你在面试战场上披荆斩棘,所向无敌!

vue3 指令

在 Vue 3 中,指令(Directives)是一种特殊的标记,用于在 DOM 元素上应用一些特殊的行为。Vue 3 提供了一些内置指令,同时也允许开发者自定义指令。

内置指令

Vue 3 提供了以下常用的内置指令:

  1. v-bind:动态绑定一个或多个属性,或一个组件 prop 到表达式。

    html 复制代码
    <img v-bind:src="imageSrc">
    <!-- 简写 -->
    <img :src="imageSrc">
  2. v-model:在表单控件或组件上创建双向数据绑定。

    html 复制代码
    <input v-model="message">
  3. v-ifv-else-ifv-else:根据表达式的值条件性地渲染元素。

    html 复制代码
    <div v-if="isVisible">Visible</div>
    <div v-else>Not Visible</div>
  4. v-for:基于源数据多次渲染元素或模板块。

    html 复制代码
    <ul>
      <li v-for="item in items" :key="item.id">{{ item.text }}</li>
    </ul>
  5. v-on:监听 DOM 事件并在触发时执行一些 JavaScript 代码。

    html 复制代码
    <button v-on:click="handleClick">Click me</button>
    <!-- 简写 -->
    <button @click="handleClick">Click me</button>
  6. v-show :根据表达式的值条件性地显示元素(通过 CSS 的 display 属性)。

    html 复制代码
    <div v-show="isVisible">Visible</div>
  7. v-text :更新元素的 textContent

    html 复制代码
    <span v-text="message"></span>
  8. v-html :更新元素的 innerHTML

    html 复制代码
    <div v-html="rawHtml"></div>
  9. v-pre:跳过这个元素和它的子元素的编译过程。

    html 复制代码
    <div v-pre>{{ this will not be compiled }}</div>
  10. v-cloak:这个指令保持在元素上直到关联组件实例结束编译。通常与 CSS 规则一起使用,以隐藏未编译的 Mustache 标签。

    html 复制代码
    <div v-cloak>
      {{ message }}
    </div>
    css 复制代码
    [v-cloak] {
      display: none;
    }
  11. v-once:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。

    html 复制代码
    <span v-once>{{ message }}</span>
  12. 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>
获取参数

在目标组件中,可以过 useRoutethis.$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>
获取查询参数

在目标组件中,可以过 useRoutethis.$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. 监听特定按键

可以通过事件对象的 keykeyCode 属性来判断按下的具体按键。

示例:监听回车键(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. 使用第三方库

如果需要更复杂的键盘事件处理(例如监听组合键、长按等),可以使用第三方库,如:

  • Mousetrap:轻量级的键盘事件库。
  • Vueuse :Vue 组合式 API 工具库,提供了 useKeyModifieruseKeyPress 等工具。
示例:使用 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 == undefinedtrue null === undefinedfalse
类型检查 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 用于创建一个响应式的基本类型值 (如 numberstringboolean 等)。它返回一个包含 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 回调
特点
  • 可以监听 refreactive 对象或计算属性。
  • 支持深度监听(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. toReftoRefs
  • 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. shallowReactiveshallowRef
  • 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 不会被转换为响应式
});

三、最佳实践建议

  1. 优先选择

    • 对象/数组用 reactive
    • 基本类型用 ref
    • 组合逻辑时用 toRefs 解构
  2. 避免陷阱

    javascript 复制代码
    // 错误:直接替换 reactive 对象
    state = { ...state, newProp: 1 }; // ❌ 失去响应式
    
    // 正确:修改属性
    Object.assign(state, { newProp: 1 }); // ✅
    
    // 正确:使用 ref 包装对象
    const objRef = ref({ foo: 'bar' });
    objRef.value.foo = 'new'; // ✅
  3. 性能优化

    • 大数据量时用 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:reactiveref 如何选择?

  • 对象/数组 → 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 操作
装饰器 旧版类式组件开发

避坑经验

  1. 避免过度使用 any

    优先使用 unknown + 类型守卫,或通过泛型传递类型信息

  2. 区分 interfacetype

    • 接口:扩展性强(extends),适合描述对象形状
    • 类型别名:支持联合/交叉类型,适合复杂类型组合
  3. 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. 总结

方法 说明
检测断开 使用 oncloseonerror 事件检测连接状态。
自动重连 onclose 中实现重连逻辑,支持简单重连和指数退避重连。
处理网络问题 检测网络状态,使用心跳机制保持连接。
服务器端处理 确保服务器支持重连,设置合理的超时时间。
调试和日志 记录日志,使用开发者工具调试 WebSocket 连接。

组件销毁

在 Vue.js 中,组件销毁是指组件实例从 DOM 中移除并释放资源的过程。组件销毁时,Vue 会触发一系列生命周期钩子,开发者可以在这些钩子中执行清理操作(如取消定时器、解绑事件、释放内存等)。

1. 组件销毁的生命周期钩子

Vue 提供了以下生命周期钩子,用于处理组件销毁时的逻辑:

(1) beforeDestroy
  • 在组件销毁之前调用。
  • 适合执行一些清理操作,如取消定时器、解绑事件等。
(2) destroyed
  • 在组件销毁之后调用。
  • 此时,组件已经从 DOM 中移除,所有的事件监听器和子组件也已被销毁。

2. 组件销毁的常见场景

(1) 路由切换

当使用 Vue Router 切换路由时,当前组件会被销毁。

(2) 条件渲染

当使用 v-ifv-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. 总结

场景 说明
生命周期钩子 beforeDestroydestroyed 用于处理组件销毁逻辑。
常见场景 路由切换、条件渲染、动态组件、手动销毁。
清理操作 取消定时器、解绑事件、取消异步请求、释放资源。
手动销毁 使用 $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 部分支持) 现代浏览器,复杂布局
绝对定位 布局精确 需要手动计算间距,灵活性差 固定宽度布局
表格布局 兼容性好 语义化差,灵活性差 传统布局,兼容旧浏览器
相关推荐
在下千玦2 分钟前
#前端js发异步请求的几种方式
开发语言·前端·javascript
知否技术3 分钟前
面试官最爱问的Vue3响应式原理:我给你讲明白了!
前端·vue.js
ylfhpy26 分钟前
Java面试黄金宝典19
java·开发语言·数据结构·算法·面试·面经
小周同学:1 小时前
vue将页面导出成word
前端·vue.js·word
阿杰在学习1 小时前
基于OpenGL ES实现的Android人体热力图可视化库
android·前端·opengl
xfq1 小时前
[ai] cline使用总结(包括mcp)
前端·后端·ai编程
weiran19991 小时前
手把手的建站思路和dev-ops方案
前端·后端·架构
小刀飘逸1 小时前
子元素 margin-top 导致父元素下移问题的分析与解决方案
前端
Evrytos1 小时前
告别石器时代#2:ES6新数据类型
前端·javascript
Ody1 小时前
大屏开发适配方案全面解析
前端