精选面试题
1、js中set和map的作用和区别?
在 JavaScript 中,Set 和 Map 是两种非常重要的集合类型
1、Set 是一种集合数据结构,用于存储唯一值。它类似于数组,但成员的值都是唯一的,没有重复的值。Set 中的值只能是唯一的,任何重复的值都会被自动忽略。Set 中的值可以是任何数据类型(原始值或对象引用)。Set 提供了操作集合的方法,比如添加、删除、检查成员等。add(value): 添加一个值到 Set 中,如果该值已存在,则不会改变 Set。delete(value): 从 Set 中删除一个值,如果删除成功则返回 true,否则返回 false。has(value): 检查 Set 中是否包含一个值,如果包含则返回 true,否则返回 false。clear(): 清除 Set 中的所有值。size: 获取 Set 中元素的数量let mySet = new Set();mySet.add(1);mySet.add(1);console.log(mySet); // 输出: Set {1}2、Map 是一种键值对集合,类似于对象,但“键”的范围不限于字符串和符号,可以是任何数据类型(原始值或对象引用)。Map 中的键和值都是唯一的,键和值可以是任意类型。Map 保持插入顺序,即迭代 Map 对象时,元素的顺序与它们被插入的顺序一致。Map 提供了操作键值对的方法,比如设置、获取、删除、检查键等。set(key, value): 设置 Map 对象中指定元素的值。get(key): 返回指定键的值,如果不存在则返回 undefined。delete(key): 移除 Map 对象中指定的元素,如果删除成功则返回 true,否则返回 false。has(key): 判断 Map 对象中是否存在指定的键,如果存在则返回 true,否则返回 false。clear(): 移除 Map 对象中的所有键/值对。size: 获取 Map 中键值对的数量。let myMap = new Map();myMap.set('name', 'Alice');myMap.set(1, 'first');myMap.set(1, 'second'); // 后面的值会覆盖前面的值,但键还是1console.log(myMap); // 输出: Map(2) { 'name' => 'Alice', 1 => 'second' }3、区别存储内容:Set 只存储唯一值,没有键和值的区分。Map 存储键值对,键和值都可以是任意类型,且键是唯一的。键的类型:Set 中的元素作为值,没有键。Map 中的元素作为键,每个键都映射到一个值。顺序:Set 和 Map 都保持插入顺序。方法:Set 提供了与集合操作相关的方法,如 add、delete、has 等。Map 提供了与键值对操作相关的方法,如 set、get、delete、has 等。
2、flex 1是哪几个的缩写?
flex: 1 是 flex-grow、flex-shrink 和 flex-basis 的简写形式。具体来说:flex-grow: 1:表示弹性盒子在主轴方向上的增长比例为 1,即它可以占用剩余空间。flex-shrink: 1:表示弹性盒子在主轴方向上的收缩比例为 1,即在空间不足时可以缩小。flex-basis: 0%(或简写为 0,在某些情况下等同于 auto 的初始效果,但在此上下文中通常理解为 0 以确保空间分配):表示弹性盒子的初始大小为 0,这样可以确保所有设置了 flex: 1 的弹性盒子平分剩余空间。
flex: 1 是 CSS Flexbox 布局中的一个简写属性,用于设置弹性容器(flex container)内子项(flex item)的伸缩比例,使子项能够根据容器的可用空间自动调整大小。
3、flex布局有哪几种属性?
display: flex;
flex-direction;//决定主轴的方向(即项目的排列方向)
flex-wrap;//定义如果一条轴线排不下项目,如何换行。
justify-content;//定义项目在主轴上的对齐方式 space-between,space-around,space-evenly
align-items;//定义项目在交叉轴上如何对齐
align-content;//定义了多根轴线(多行)在交叉轴上的对齐方式
4、vuex里变各属性介绍?
Vuex 包含五个核心概念,分别是 state、getters、mutations、actions 和 modules。1、state定义:state是Vuex中的基本数据,用于存储变量,相当于Vue 组件中的 data。特性:state中的数据是响应式的,当数据发生变化时,依赖该数据的组件会自动更新。使用:在组件中可以通过this.$store.state.xxx 或使用辅助函数mapState将数据映射到组件的computed 计算属性中来访问state中的数据。
2、getters定义:getters 是从 state 中派生的数据,相当于 state 的计算属性。特性:getters 的返回值会根据其依赖的 state 值的变化而重新计算,并且具有缓存功能,只有当依赖值发生变 化时才会重新计算。使用:在组件中可以通过 this.$store.getters.xxx 或使用辅助函数 mapGetters 将 getters 映射到组 件的 computed 计算属性中来访问 getters 的返回值。
3、mutations定义:mutations 是 Vuex 中唯一允许更新 state 的方法,它必须是同步的。特性:每个 mutation 都有一个字符串的事件类型(type)和一个回调函数(handler),回调函数用于执行状 态更新操作,并且会接受 state 作为第一个参数。使用:在组件中通过 this.$store.commit('xxx', payload) 来触发 mutation,其中 'xxx' 是 mutation 的事件类型,payload 是传递给 mutation 的额外参数。
4、actions定义:actions 类似于 mutations,但不同的是 actions 提交的是 mutations,而不是直接变更状态,并且 actions 可以包含任意异步操作。特性:actions 的回调函数接收一个上下文对象(context),该对象包含了 commit 方法用于触发mutation, 以及 state 和 getters 等属性用于访问状态和 getters。使用:在组件中通过 this.$store.dispatch('xxx', payload) 来触发 action,其中 'xxx' 是 action 的事件类型,payload 是传递给 action 的额外参数。或使用辅助函数 mapActions
5、modules定义:modules 是 Vuex 的模块化机制,允许将 store 分割成模块(module),每个模块拥有自己的 state、mutation、action、getter,甚至是嵌套子模块。特性:模块化使得 Vuex 的结构更加清晰,方便管理。每个模块都是独立的,可以单独进行状态管理,同时也可以通 过 namespaced: true 来启用命名空间,避免不同模块之间的命名冲突。使用:在创建 Vuex store 时,可以通过 modules 选项来定义模块。在组件中访问模块的状态和方法时,需要 使用模块的命名空间(如果启用了命名空间)来区分不同模块的状态和方法。
//一:定义store
//引入Vue和Vuex库
import Vue from 'vue';
import Vuex from 'vuex';Vue.use(Vuex);// 使用Vuex插件//定义一个名为cartModule的模块,用于管理购物车状态
const cartModule = {// state对象包含模块的状态state: {items: [],// 购物车中的商品列表totalQuantity: 0,// 购物车中商品的总数量totalPrice: 0.0,// 购物车中商品的总价格checkoutStatus: false,// 结账状态,默认为未结账lastAddedItemId: 0// 最后添加商品的ID,用于唯一标识商品},// getters对象包含基于state的派生状态(计算属性)getters: {getItems: state => state.items,// 获取购物车中的商品列表getTotalQuantity: state => state.totalQuantity,// 获取购物车中商品的总数量getTotalPrice: state => state.totalPrice,// 获取购物车中商品的总价格getCheckoutStatus: state => state.checkoutStatus,// 获取结账状态getLastAddedItemId: state => state.lastAddedItemId// 获取最后添加商品的ID},// mutations对象包含改变状态的同步方法mutations: {ADD_ITEM(state, item) {//向购物车中添加商品,并更新总数量和总价格state.items.push({ ...item, id: ++state.lastAddedItemId });state.totalQuantity += item.quantity;//总数量state.totalPrice += item.price * item.quantity;//价格*总数量},REMOVE_ITEM(state, itemId) {// 从购物车中移除指定ID的商品,并更新总数量和总价格const item = state.items.find(i => i.id === itemId);if (item) {state.totalQuantity -= item.quantity;state.totalPrice -= item.price * item.quantity;state.items = state.items.filter(i => i.id !== itemId);}},SET_CHECKOUT_STATUS(state, status) {// 设置结账状态state.checkoutStatus = status;}},// actions对象包含改变状态的异步方法或批量更新actions: {addItem({ commit }, item) {// 调用mutation方法向购物车中添加商品commit('ADD_ITEM', item);},removeItem({ commit }, itemId) {// 调用mutation方法从购物车中移除商品commit('REMOVE_ITEM', itemId);},checkout({ commit }) {// 调用mutation方法设置结账状态为已结账commit('SET_CHECKOUT_STATUS', true);}}
};// 创建Vuex store实例,并将cartModule作为模块传入
export default new Vuex.Store({modules: {cart: cartModule//namespaced: true 启用了命名空间,可以定义多个不同的store}
});
<template><div><h1>购物车</h1><!-- 使用v-for指令遍历购物车中的商品列表 --><ul><li v-for="item in items" :key="item.id"><!-- 显示商品名称、数量和价格 -->{{ item.name }} - {{ item.quantity }} x ${{ item.price }}<!-- 提供一个按钮用于移除当前商品 --><button @click="removeItem(item.id)">移除</button></li></ul><!-- 显示购物车中商品的总数量和总价格 --><p>总数量: {{ totalQuantity }}</p><p>总价: ${{ totalPrice }}</p><!-- 提供一个按钮用于添加新商品 --><button @click="addItem">添加商品</button><!-- 提供一个按钮用于结账 --><button @click="checkout">结账</button><!-- 如果结账状态为已结账,则显示已结账提示 --><p v-if="checkoutStatus">已结账</p></div>
</template><script>
export default {// 使用computed属性从Vuex store中获取购物车状态computed: {items() {// 使用getters获取购物车中的商品列表return this.$store.getters['cart/getItems'];},totalQuantity() {// 使用getters获取购物车中商品的总数量return this.$store.getters['cart/getTotalQuantity'];},totalPrice() {// 使用getters获取购物车中商品的总价格return this.$store.getters['cart/getTotalPrice'];},checkoutStatus() {// 使用getters获取结账状态return this.$store.getters['cart/getCheckoutStatus'];}},// 定义方法用于与Vuex store交互methods: {addItem() {// 创建一个新商品对象,并通过dispatch调用action方法将其添加到购物车中const newItem = { name: '商品', quantity: 1, price: 10.0 };this.$store.dispatch('cart/addItem', newItem);},removeItem(itemId) {// 通过dispatch调用action方法从购物车中移除指定ID的商品this.$store.dispatch('cart/removeItem', itemId);},checkout() {// 通过dispatch调用action方法设置结账状态为已结账this.$store.dispatch('cart/checkout');}}
};
</script>
5、ref()和 reactive() 的作用以及区别
ref() 和 reactive() 都是 Vue.js 3 中用于创建响应式数据的方法
一、ref() 的作用及特点1、ref() 主要用于包装 JavaScript 的基本类型数据(如字符串、数字、布尔值等),使其具有响应性。2、它返回一个响应式引用对象,该对象有一个 .value 属性来存储传入的值3、在html模板中,Vue 会自动解包 ref,使得可以直接使用 ref 变量而无需 .value 属性。4、在JavaScript代码中访问或修改 ref 包装的数据时,需要通过 .value 属性来获取或设置其实际值
二、reactive() 的作用及特点1、reactive() 主要用于包装 JavaScript 对象和数组等复杂类型的数据,使其具有响应性2、它返回一个响应式代理对象,该对象可以拦截对原始对象的各种操作(如属性读取、赋值等),并在数据变化时触 发更新。3、可以直接访问和修改对象或数组的属性或元素,而无需使用 .value 属性。4、内部使用 Proxy 对象来实现数据代理和响应式机制。
三、使用场景:当需要处理简单的、单一的响应式数据时,优先选择 ref()。当需要处理复杂对象或数组时,考虑使用 reactive()。
四、实现原理:ref() 通过 Object.defineProperty() 的 get 和 set 方法实现数据代理。reactive() 使用 ES6 的 Proxy 对象来实现数据代理,可以更细粒度地控制对象数据的访问和修改。import { reactive,ref } from 'vue';const count = ref(0); // 使用 ref() 包装一个数字 function increment() {count.value++; // 通过 .value 属性修改值}const user = reactive({name: 'John Doe',age: 30}); // 使用 reactive() 包装一个对象function updateUser() {user.name = 'Jane Doe'; // 直接修改对象属性user.age = 28;}
6、箭头函数和普通函数的区别?
1、箭头函数:使用箭头(=>)来定义,语法更加简洁;普通函数:使用function关键字来定义,语法相对传统;
2、箭头函数:如果只有一个参数,可以省略参数周围的括号;如果函数体只有一行代码且不需要返回值(即隐式返回),可以省略大括号和return关键字。
普通函数:参数和函数体都需要使用括号明确包围;
3、箭头函数:没有自己的this,它会捕获其所在上下文的this值作为自己的this值。这意味着在箭头函数内部,this始终指向定义该函数时的上下文。普通函数:this的指向是可变的,它通常指向调用它的对象。在严格模式下('use strict'),如果未指定上下文(即非方法调用),this将默认为undefined;在非严格模式下,将默认为全局对象(在浏览器中通常是window)
4、箭头函数:不绑定arguments对象。如果需要访问传递给函数的参数列表,可以使用剩余参数(...args)语法。普通函数:每次调用都会创建一个新的arguments对象,该对象包含传递给函数的所有参数。
5、箭头函数:不能用作构造函数,因此没有prototype属性。普通函数:可以用作构造函数来创建对象实例
6、箭头函数:不能使用super关键字普通函数:在类的方法中,可以使用super关键字来调用父类的方法或访问父类的属性。
7、css中定位有哪些,有什么区别,哪些会影响性能
一、静态定位(Static)特点:这是所有元素的默认定位方式。元素按照正常的文档流进行排列,不会受到top、bottom、left、right属性的影响。
二、相对定位(Relative)特点:元素相对于其正常位置进行定位。即使移动了元素,它原本在文档流中的空间仍然会被保留。
三、绝对定位(Absolute)特点:元素脱离正常的文档流,相对于最近的已定位(即position属性不为static)的祖先元素进行定位。如果没 有已定位的祖先元素,则相对于文档的初始包含块(通常是<html>元素或浏览器窗口)进行定位。(父相子绝)
四、固定定位(Fixed)特点:元素相对于浏览器窗口进行定位,无论页面如何滚动,元素始终保持在指定的位置。元素脱离文档流,不占据 原来的空间。
五、粘性定位(Sticky)特点:元素在滚动到特定位置之前表现为相对定位,滚动到特定位置后表现为固定定位。这允许元素在滚动过程中固 定在某个位置,直到滚动超出其父容器的边界。
六、对性能的影响*静态定位和相对定位:由于它们不改变元素在文档流中的位置或只进行微小的偏移,因此对性能的影响较小。*绝对定位和固定定位:元素脱离文档流,可能导致其他元素重新排列以填补空白。这可能会增加浏览器的重排和重绘工作,从而在某些情况下影响性能。然而,对于少量元素或简单布局,这种影响通常是微不足道的。*粘性定位:粘性定位的实现可能涉及复杂的计算和状态切换,因此在某些情况下可能对性能产生一定影响。然而,现代浏览器已经对粘性定位进行了优化,以提供流畅的用户体验。
8、重排和重绘的区别?
1、重排(Reflow):*当页面元素的尺寸、结构或某些属性(如边距、内边距、边框宽度、字体大小等)发生改变时,会触发重排。*添加、删除DOM元素,或者改变DOM元素的位置也会触发重排。*浏览器窗口大小的变化(如调整窗口大小或旋转设备)同样会导致重排。
2、重绘(Repaint):*当页面元素的样式发生改变,但这些改变不影响元素在文档流中的位置和大小时,会触发重绘。*常见的触发重绘的样式变化包括颜色、背景、文本颜色、边框颜色等的改变。
3、性能开销重排:*重排是一种比较昂贵的操作,因为它需要浏览器重新计算元素的几何属性并重新布局页面。*这会消耗较多的计算资源和时间,尤其是在页面包含大量元素或复杂布局的情况下。重绘:*相比重排,重绘是一种比较轻量级的操作。*它只需要浏览器重新渲染元素的外观,而不需要重新计算元素的位置和大小。
4、优化建议*合并样式改变:尽量将多次样式改变合并成一次,以减少重排和重绘的次数。*使用CSS动画:利用CSS动画代替JavaScript操作来更新样式,因为CSS动画通常在浏览器内部进行了优化,可以减少性能开销。*避免频繁操作DOM:减少不必要的DOM操作,尤其是在循环或频繁触发的事件处理程序中。*使用绝对定位或固定定位:对于不需要参与文档流布局的元素,可以使用绝对定位或固定定位来减少对其他元素的影响。*利用文档片段:在大量添加或删除DOM元素时,可以使用文档片段(DocumentFragment)来减少重排次数。文档片段是一个轻量级的文档对象,可以在其中构建DOM结构,然后一次性将其添加到页面中。
9、vue2中nexttick的作用和用法,以及场景
1、$nextTick的主要作用是确保在DOM更新完成后执行一些操作。在Vue中,数据的更新是异步的,即Vue会异步执行更新队列,而不是立即操作DOM。因此,如果需要在数据更新后立即获取更新后的DOM元素或执行某些依赖于最新DOM状态的操作,就需要使用$nextTick。
// 使用回调函数
this.$nextTick(function() {// 在DOM更新后执行的代码console.log('DOM已更新');
});// 使用Promise对象
this.$nextTick().then(function() {// 在DOM更新后执行的代码console.log('DOM已更新');
});2、常见使用场景(1)在数据变化后立即获取更新后的DOM:当数据发生变化后,如果需要立即获取更新后的DOM元素的状态(如尺寸、位置、属性等),可以使用$nextTick。(2)确保在DOM更新后执行某些操作:有时需要在DOM更新后执行一些操作,比如添加或删除元素、更新元素的样式、触发动画效果等。使用$nextTick可以确保这些操作在DOM更新后进行,避免操作无效或报错。(3)在组件的生命周期钩子中使用:在Vue组件的mounted和updated生命周期钩子中,可以使用$nextTick来确保在DOM更新后执行某些逻辑。这特别适用于需要在组件挂载或更新后立即操作DOM的场景。(4)在动态渲染组件时使用:当动态渲染组件时,可以使用$nextTick来确保组件已经渲染完成。这对于需要在组件渲染后立即执行某些操作的场景非常有用。(5)在集成第三方库时使用:在Vue中集成第三方库时,有时需要确保第三方库在正确的DOM状态下初始化。使用$nextTick可以确保在DOM更新完成后初始化第三方库。
9.5、vue3中nexttick的作用和用法,以及场景?
在 Vue 3 中,nextTick 是一个用于在下次 DOM 更新循环结束之后执行延迟回调的函数。当你修改了组件的数据并希望立即基于更新后的 DOM 状态执行某些操作时,nextTick 非常有用。
import { nextTick } from 'vue';
import { ref, nextTick } from 'vue';export default {setup() {const message = ref('Hello Vue 3!');const updateMessageAndFocus = async () => {message.value = 'Updated Message!';// 使用 nextTick 确保 DOM 更新完成后再执行代码await nextTick();// 假设这里有一个对 DOM 元素的操作,比如聚焦到某个输入框console.log('DOM updated, now you can safely interact with it.');// 例如,通过 ref 引用 DOM 元素进行操作// inputElementRef.value.focus();};return {message,updateMessageAndFocus};}
};
详细说明
数据更新: 当你在 Vue 组件中修改响应式数据(如 ref 或 reactive 对象)时,Vue 会将这些更改放入一个队列中,并在下一个“tick”中批量更新 DOM。
nextTick: 如果你需要在数据更改后立即执行依赖于最新 DOM 状态的操作,你可以使用 nextTick。它返回一个 Promise,你可以通过 await 来等待 DOM 更新完成。
异步操作: nextTick 通常用于需要确保在 DOM 更新后再执行的异步操作,比如获取更新后的 DOM 尺寸、滚动到某个元素、或者聚焦到某个输入框。
10、computed和watch的区别?
computed和watch是两个用于响应数据变化的特性。
一、computed(计算属性)计算属性是基于其他数据计算得出的属性,它的值会根据依赖的数据自动更新。计算属性会被缓存,只有当依赖的数据发生变化时,才会重新计算计算属性更适合处理数据的计算和衍生,它提供了一种简洁和高效的方式来处理数据的计算和格式化。计算属性返回一个新的计算后的值,通常用于模板中。当需要根据其他数据进行计算或格式化时,例如根据输入框的值计算出其他相关数据、根据列表数据计算出统计信息等。computed在依赖未变化的情况下避免了多次计算,适用于频繁读取的数据。
二、watch(侦听器)watch用于监视数据的变化,并在数据变化时执行相应的操作。watch可以监听单个数据、对象的属性或数组的变化,并且可以进行深度监听。watch适用于需要在数据变化时执行异步或开销较大的操作,例如发送网络请求、处理复杂的计算逻辑等。watch提供了更灵活的方式来响应数据的变化,并且可以处理更复杂的业务逻辑。watch的回调函数没有返回值,它的重点在于执行的副作用,例如更新DOM、调用方法等。当需要监听某个数据的变化并执行异步操作时,例如API请求或复杂的逻辑处理。当需要在数据变化后触发某些副作用,例如重置表单、清空数据等操作时。watch更多地用于处理复杂的逻辑和异步操作,可能在性能方面考虑较少。
11、v-if和v-for在vue2和vue3中的区别?
1、在Vue2中当v-if和v-for同时出现在一个元素上时,v-for的优先级高于v-if。这意味着v-for会先遍历数据,然后再根据v-if的条件决定是否渲染每个元素。
2、在Vue3中在Vue3中,v-if的优先级高于v-for。这意味着Vue会先根据v-if的条件过滤数据,然后再对过滤后的数据进行v-for遍历。
12、vue中操作dom的方法有哪些
1、使用 ref 引用 DOM 元素
<template><div><input ref="myInput" type="text" />//引用信息将会注册到父组件的 $refs 对象上。<button @click="focusInput">Focus Input</button></div>
</template><script>
export default {methods: {focusInput() {this.$refs.myInput.focus();//引用信息将会注册到父组件的 $refs 对象上。}}
}
</script>
2、使用生命周期钩子
//在 mounted 钩子中,组件的 DOM 已经被渲染和插入到文档中。
<template><div ref="myDiv">Hello, Vue!</div>
</template><script>
export default {mounted() {this.$refs.myDiv.style.color = 'red';}
}
</script>3、使用第三方库(如 jQuery)
<template><div ref="myDiv">Hello, Vue with jQuery!</div>
</template><script>
import $ from 'jquery';export default {mounted() {$(this.$refs.myDiv).css('color', 'blue');}
}
</script>
13、vue中父子组件间传参的方式有哪些?
一、使用Props传递参数父组件通过属性向子组件传递数据。子组件通过props选项接收父组件传递的数据,并在模板中使用这些数据。*数据传递是单向的,即父组件向子组件传递
二、使用事件传递参数子组件通过触发事件向父组件传递数据子组件使用$emit方法触发事件,并传递数据作为参数,父组件通过监听子组件触发的事件来接收数据。
三、使用Provide和Inject(依赖注入)父组件通过provide提供数据,子组件通过inject注入这些数据可以实现跨多个层级的组件通信,适用于祖孙组件或更深层次的组件间通信
四、使用parent和children父组件通过$children属性访问子组件实例,子组件通过$parent属性访问父组件实例。
五、使用Vuex或事件总线(Bus)
//一、使用Props传递参数
//父组件
<template><div><ChildComponent :message="parentMessage"></ChildComponent></div>
</template><script>
import ChildComponent from './ChildComponent.vue';export default {components: {ChildComponent},data() {return {parentMessage: 'Hello from Parent!'};}
}
</script>
//子组件
<template><div>{{ message }}</div>
</template><script>
export default {props: {message: {type: String,required: true}}
}
</script>
//二、使用事件传递参数
//父组件
<template><div>//和子组件$emit的第一个参数保持一直<ChildComponent @message-from-child="handleMessageFromChild"></ChildComponent><p>{{ childMessage }}</p></div>
</template><script>
import ChildComponent from './ChildComponent.vue';export default {components: {ChildComponent},data() {return {childMessage: ''};},methods: {handleMessageFromChild(message) {//监听的事件this.childMessage = message;}}
}
</script>
//子组件
<template><button @click="sendMessageToParent">Send Message to Parent</button>
</template><script>
export default {methods: {sendMessageToParent() {//第一个参数要个父组件@方法名保持一致,第二个参数是要传递的内容this.$emit('message-from-child', 'Hello from Child!');}}
}
</script>
14、vue3中路由守卫?
Vue Router中的路由守卫主要分为全局守卫、路由独享守卫和组件内守卫三种类型。
一、全局守卫beforeEach:在路由即将改变前调用,参数包括即将进入的目标路由对象to、即将离开的当前路由对象from以及一 个用于阻止导航的next函数。beforeResolve:在路由解析之后但在导航确认之前调用,通常用于数据预取。afterEach:在路由改变之后调用,不接收next函数,因此不能改变导航结果。二、路由独享守卫路由独享守卫是在单个路由配置对象中定义的,它们只会在该路由匹配时生效。常见的路由独享守卫有:beforeEnter:在路由即将进入前调用,参数包括即将进入的目标路由对象to、即将离开的当前路由对象from以及一个用于阻止导航的next函数。三、组件内守卫组件内守卫是在Vue组件内部定义的,它们会在组件的生命周期钩子中调用。beforeRouteEnter:在路由进入组件之前调用,此时组件实例还未创建,不能访问this。参数包括即将进入的目标路由对象to、即将离开的当前路由对象from以及一个用于继续导航的next函数(在next中传递的参数会作为组件的初始props)。beforeRouteUpdate:在路由更新组件时调用(例如,从一个/user/1路由跳转到/user/2)。此时组件实例已经存在,可以访问this。参数同样包括to、from和next。beforeRouteLeave:在导航离开组件时调用。参数包括即将离开的当前路由对象from、即将进入的目标路由对象to以及一个用于阻止导航的next函数。// 全局守卫
router.beforeEach((to, from, next) => {// 权限验证逻辑if (to.meta.requiresAuth && !isAuthenticated) {// 未登录则重定向到登录页面next({ path: '/login' });} else {// 已登录或不需要验证则继续导航next();}
});// 路由独享守卫
const route = {path: '/profile',component: Profile,beforeEnter: (to, from, next) => {// 特定路由的验证逻辑if (to.meta.requiresProfile) {// 验证逻辑...next();} else {next({ path: '/' });}}
};// 组件内守卫
export default {name: 'UserProfile',beforeRouteEnter(to, from, next) {// 组件即将进入前的逻辑next(vm => {// 可以通过vm访问组件实例});},beforeRouteUpdate(to, from, next) {// 路由更新时的逻辑next();},beforeRouteLeave(to, from, next) {// 离开组件前的逻辑next();}
};
15、js中原型和原型链?
一、原型(Prototype)*在JavaScript中,对象有一个特殊的隐藏属性[[Prototype]],它要么为null,要么就是对另一个对象的引用,该对象被称为“原型”*通过原型,可以定义对象的共享属性和方法。这意味着所有对象实例都可以访问和修改这些属性和方法,这在创建大量具有相同属性和方法的对象时非常有用,因为它可以避免在每个对象实例中重复定义这些属性和方法。*原型还可以用于动态修改和扩展对象,允许我们在不修改原始构造函数的情况下,为所有对象实例添加新的属性和方法。
二、原型链(Prototype Chain)原型链是JavaScript中实现对象继承的主要机制。当一个对象试图访问一个属性时,如果它自身没有这个属性,JavaScript会在它的原型链上查找这个属性,直到找到这个属性或者到达链的尽头(null)。原型是 JavaScript 中每个对象上都有的一个特殊属性,它指向另一个对象,这个对象即原型对象。原型对象也可以有自己的原型,这样就形成了一条链,即原型链。原型链是由一系列原型对象连接而成的链,每个对象都有一个 __proto__ 属性指向它的原型对象,直到 Object.prototype,这个原型对象是最终的原型对象,也就是说它没有自己的原型。function Person(name, age) {this.name = name;this.age = age;
}Person.prototype.sayHello = function() {console.log("Hello, my name is " + this.name);
};const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);alice.sayHello(); // 输出: Hello, my name is Alice
bob.sayHello(); // 输出: Hello, my name is Bob
16、js中对数组和对象的操作方法有哪些?
一、数组的操作方法
1、添加/删除元素push():向数组的末尾添加一个或多个元素,并返回新的长度。pop():删除并返回数组的最后一个元素。shift():删除并返回数组的第一个元素。unshift():向数组的开头添加一个或多个元素,并返回新的长度。splice():通过删除或替换现有元素或者添加新元素来修改数组,并以数组的形式返回被修改的内容。此方法会改变原数组。
2、截取/复制数组slice():返回一个新的数组对象,这个对象是一个由原数组的指定开始到结束(不包括结束)的浅拷贝。原始数组不会被改变。concat():用于连接两个或更多的数组。该方法不会改变现有的数组,而是返回一个新数组。3、排序/反转数组sort():对数组的元素进行排序,并返回数组。默认情况下,sort()方法将元素转换为字符串,然后比较它们的UTF-16代码单元值序列,以进行排序。如果需要对数字进行排序,需要提供一个比较函数。reverse():反转数组中元素的顺序。4、遍历/映射数组forEach():对数组的每个元素执行一次给定的函数。map():创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。for循环:通过索引来遍历数组元素。for...of循环:直接遍历数组(或任何可迭代对象)的值,无需关心索引。5、搜索/查找数组find():返回数组中满足提供的测试函数的第一个元素的值。否则返回undefined。findIndex():返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。includes():判断一个数组是否包含一个指定的值,根据情况可从左往右或从右往左遍历。indexOf():在数组中从左到右搜索一个值,并返回其索引(从0开始)。lastIndexOf():在数组中从右到左搜索一个值,并返回其索引(从0开始)。
6、归约/累加数组reduce():对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。reduceRight():与reduce()类似,但是从右到左。7、其他方法fill():使用给定的值填充数组的从起始索引到结束索引的所有元素。不会改变原数组的大小。copyWithin():在当前数组中,将指定位置的元素复制到其他位置,并返回该数组。不会改变数组的长度。flat()和flatMap():flat()方法会按照一个可指定的深度递归遍历数组,并将所有元素合并为一个新数组。 flatMap()方法首先使用映射函数映射每个元素,然后将结果展平成一个新数组。toString():把数组转换为字符串,并返回结果。数组中的元素之间用逗号分隔。values():返回一个新的数组迭代器对象,该对象包含数组中的每个索引的键/值对。entries():返回一个新的数组迭代器对象,该对象包含数组中的每个索引的键/值对(索引作为键)。keys():返回一个新的数组迭代器对象,该对象包含数组中的每个索引的键。二、对象的操作方法1、创建对象Object.create():创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。字面量语法:使用花括号{}直接创建一个对象。
2、添加/删除属性点(.)或方括号([])语法:向对象添加或访问属性。delete操作符:删除对象的属性。
3、属性描述符Object.defineProperty():在对象上定义一个新属性,或修改一个对象的现有属性,并返回该对象。Object.defineProperties():在对象上定义多个新属性或修改现有属性,并返回该对象。
4、获取对象信息Object.keys():返回一个给定对象自身可枚举属性组成的数组,数组中属性名的排列顺序和使用for...in循环遍历该对象时返回的顺序一致(两者的主要区别是for-in循环还会枚举其原型链上的属性)。Object.values():返回一个给定对象自身的所有可枚举属性值的数组,其排列与Object.keys()返回的数组中的属性名排列相同。Object.entries():返回一个给定对象自身可枚举属性的键值对数组,其排列与通过手动遍历该对象属性返回的顺序一致。Object.getOwnPropertyNames():返回一个数组,该数组对对象的所有自身属性(不包括不可枚举属性,但包括符号属性)的键进行排序。Object.getOwnPropertySymbols():返回一个给定对象自身的所有符号属性的数组。
5、合并对象Object.assign():将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
6、冻结/密封对象Object.freeze():可以冻结一个对象。冻结对象后,不能向对象添加新的属性,不能删除现有属性,不能修改现有属性的值,不能修改现有属性的可枚举性、可配置性,也不能修改现有属性的[[Writable]]特性。Object.seal():可以封闭一个对象,阻止添加新的属性并将所有现有属性标记为不可配置。当前属性的值只要可写,依然可以被修改。
7、其他方法Object.is():确定两个值是否相同,与严格相等运算符(===)的行为存在一些细微差别。Object.hasOwnProperty():返回一个布尔值,表示对象自身属性中是否存在指定的属性(不包括继承的属性)。Object.prototype.toString.call():可以用来获取对象的内部[[Class]]属性,返回表示该对象的字符串。
17、get请求和post请求的区别?
1、数据传输方式GET请求:将数据附加在URL的查询字符串中;POST请求:将数据放在HTTP请求体中。这意味着,请求的参数不会直接暴露在URL上,而是包含在请求的正文部分。
2、缓存处理GET请求:通常会被浏览器缓存;POST请求:通常不会被浏览器缓存;
3、安全性:GET请求:参数直接暴露在URL上,容易泄露敏感信息。因此,GET请求不适合传输敏感数据。POST请求:参数不会暴露在URL上,相对更安全。
4、幂等性:GET请求:是幂等的。这意味着,多次执行同一个GET请求不会产生副作用。例如,多次请求同一个资源,返回的结果 应该是相同的。POST请求:不是幂等的。多次执行同一个POST请求可能会改变服务器的状态。例如,多次提交表单数据,可能会导 致数据库中插入多条记录。
5、数据长度限制GET请求:参数长度受URL长度限制。POST请求:没有长度限制。
6、书签保存与历史记录GET请求:URL可以保存为书签,并且参数会保留在浏览器历史记录中。这使得用户可以通过书签或历史记录方便地重新访问之前请求过的资源。POST请求:URL不能直接保存为书签,并且参数不会保留在浏览器历史记录中
7、数据类型限制GET请求:通常只接受ASCII字符POST请求:没有数据类型限制import axios from 'axios';// 封装一个GET请求函数
export const getRequest = (url, params) => {return axios.get(url, {params: params, // 将参数作为查询字符串附加到URL上}).then(response => {// 如果请求成功,返回响应数据return response.data;}).catch(error => {// 如果请求失败,抛出错误console.error('GET request error:', error);throw error;});
};// 封装一个POST请求函数
export const postRequest = (url, data) => {return axios.post(url, data).then(response => {// 如果请求成功,返回响应数据return response.data;}).catch(error => {// 如果请求失败,抛出错误console.error('POST request error:', error);throw error;});
};
//调用
getRequest('https://api.example.com/data', { id: 123 })
postRequest('https://api.example.com/data', { name: 'John Doe', age: 30 })
18、js中,请求拦截器和响应拦截器都能分别做什么,给出代码案例
一、请求拦截器请求拦截器可以在请求被发送到服务器之前对其进行修改或添加一些额外的处理逻辑。例如,你可以在每个请求中添加认证令牌、修改请求头或处理错误。
const axios = require('axios');// 添加请求拦截器
axios.interceptors.request.use(config => {// 在发送请求之前做些什么// 例如,添加认证令牌到请求头const token = 'your-auth-token';if (token) {config.headers['Authorization'] = `Bearer ${token}`;}return config;
}, error => {// 对请求错误做些什么return Promise.reject(error);
});// 发送请求
axios.get('/some/endpoint').then(response => {console.log(response.data);}).catch(error => {console.error('Error:', error);});二、响应拦截器响应拦截器可以在服务器响应到达客户端之前对其进行处理。例如,你可以统一处理错误响应、转换响应数据格式或根据响应状态码执行不同的逻辑。const axios = require('axios');// 添加响应拦截器
axios.interceptors.response.use(response => {// 对响应数据做点什么// 例如,检查响应状态码并处理错误if (response.status === 200) {// 请求成功,返回响应数据return response.data;} else {// 请求失败,抛出错误return Promise.reject(new Error(`Error ${response.status}: ${response.statusText}`));}
}, error => {// 对响应错误做点什么// 例如,统一处理401未授权错误并重定向到登录页面if (error.response && error.response.status === 401) {// 执行重定向或其他逻辑window.location.href = '/login';}return Promise.reject(error);
});// 发送请求
axios.get('/some/endpoint').then(data => {console.log('Data:', data);}).catch(error => {console.error('Error:', error.message);});
19、vue3中缓存组件怎么处理,里边用到了那个钩子函数?
在Vue 3中,缓存组件通常使用<keep-alive>组件来实现。<keep-alive>是Vue内置的一个组件,它能够缓存不活动的组件实例,而不是销毁它们,从而保留组件的状态并避免重复渲染,进而提升性能和用户体验。<keep-alive>还支持include和exclude属性,允许你精确控制哪些组件需要被缓存,哪些需要被排除。你还可以使用max属性来限制缓存组件的最大数量。在Vue 3中,当缓存的组件被激活或停用时,会触发特定的生命周期钩子函数(1)onActivated:当组件被插入DOM时触发。这通常发生在用户导航回到该组件所在的页面或视图时。在这个钩子函数中,你可以执行一些需要在组件激活时进行的操作,比如更新数据或重新获取焦点等。(2)onDeactivated:当组件从DOM中移除时触发。这通常发生在用户导航离开该组件所在的页面或视图时。在这个钩子函数中,你可以执行一些清理工作,比如取消订阅事件或停止某些后台操作等。
20、vue2和vue3的区别?
一、响应式数据绑定原理Vue 2:使用ES5的Object.defineProperty方法,通过发布/订阅模式实现双向数据绑定。这种方式存在一些局限性,例如它只能监听某个属性,不能对全对象进行监听,且需要额外的操作来监听数组的变化。Vue 3:引入ES6的Proxy对象,对数据进行代理,从而实现了更强大和灵活的响应式系统。Proxy可以监听对象和数组的变化,无需进行额外的操作,并且可以直接绑定整个对象,提高了效率和易用性。二、API设计Vue 2:使用选项式API(Options API),组件的逻辑被分散在data、methods、computed等选项中。这种方式在组件较复杂时,可能会导致代码组织不够清晰。Vue 3:引入了组合式API(Composition API),通过setup函数来组织组件的逻辑。这种方式使得代码更加模块化和可复用,逻辑更加清晰,易于理解和维护。
三、生命周期钩子函数Vue 2:提供了如beforeCreate、created、beforeMount、mounted等生命周期钩子函数。Vue 3:同样提供了生命周期钩子函数,但命名和触发时机有所调整。例如,setup函数在beforeCreate和created之前执行,而onBeforeMount和onMounted分别对应Vue 2中的beforeMount和mounted。此外,Vue 3还增加了onRenderTracked和onRenderTriggered两个调试钩子。四、TypeScript支持Vue 2:虽然可以使用TypeScript,但支持不够完善,类型推断和类型检查较弱。Vue 3:从设计之初就考虑了TypeScript的支持,提供了更好的类型推断,允许更安全的开发体验。这使得在Vue 3项目中使用TypeScript变得更加自然和高效。
五、组件结构Vue 2:在<template>中必须只有一个根标签。Vue 3:支持碎片化(Fragments),允许组件有多个根标签。Vue 3会默认把这些标签包裹在一个虚拟标签中,以减少内存占用。
六、创建Vue实例Vue 2:通过new Vue()构造函数来创建Vue实例,通常是在main.js文件中直接创建应用实例,并将路由和状态管理作为选项传入。Vue 3:使用createApp函数来创建应用实例,这使得创建过程更加清晰。路由和状态管理通过use方法进行插件注册。
七、性能优化Vue 2:性能较好,但在大型应用中,当数据变化频繁时可能出现性能瓶颈。Vue 3:引入了虚拟DOM的优化,减少了不必要的渲染;使用编译时优化,生成更小的代码,提高了运行时性能。此外,Vue 3的响应式系统也更加高效,进一步提升了性能。
21、css中像素有那些?
一、、CSS像素(px)定义:CSS像素是Web编程中的概念,指的是CSS样式代码中使用的逻辑像素。它是浏览器内一切长度的基本单位。特性:*CSS像素是一个相对单位,它相对于设备像素(device pixel)而言。在同样一个设备上,每1个CSS像素所 代表的物理像素是可以变化的。*在不同的设备之间,每1个CSS像素所代表的物理像素也是可以变化的。*CSS像素的值是固定的,不会随屏幕尺寸或分辨率变化。二、、物理像素(pt)定义:物理像素是显示屏上能控制显示的最小物理单位,即显示器上一个个的点。从屏幕生产出来的那天起,它上面的物理像素点就固定不变了。单位换算:1pt = 1/72英寸,而1英寸等于2.54厘米。因此,pt是一个真正的绝对单位。三、设备像素比(DPR)与设备独立像素(DIP设备像素比(DPR):设备像素比描述的是未缩放状态下,物理像素和CSS像素的初始比例关系。它可以通过设备的物理像素除以CSS像素来计算。例如,在Retina屏的iPhone上,DPR的值为2,意味着1个CSS像素相当于2个物理像素。(物理像素/CSS像素)=2【【假设有一个元素的CSS宽度为100px。在DPR = 2的设备上,这个元素实际上会占据200x设备物理像素的宽度(因为每个CSS像素由2x2个物理像素组成,所以100px * 2 = 200个物理像素宽度)。】】设备独立像素(DIP):设备独立像素也称为逻辑像素,它可以是系统中的一个点,这个点代表一个可以由程序使用的虚拟像素。在移动端浏览器中以及某些桌面浏览器中,window对象有一个devicePixelRatio属性,它的定义为设备物理像素和设备独立像素的比例。CSS像素就可以看做是设备的独立像素。
四、视窗相关像素单位(vh、vw等)vh:视窗高度(viewpoint height),1vh等于视窗高度的1%。vw:视窗宽度(viewpoint width),类似地,1vw等于视窗宽度的1%。
五、其他相对单位(em、rem)em:相对单位,基准点为父节点字体的大小。如果自身定义了font-size,则按自身来计算。整个页面内1em的值不是固定的。rem:相对单位,可理解为“root em”,即相对根节点html的字体大小来计算。这是CSS3新加的属性,被Chrome、Firefox、IE9+等浏览器支持。
六、百分比像素单位(%)定义:百分比是一个相对单位,它表示某个值相对于另一个值(通常是父元素的某个属性)的百分比。在CSS中,百分比单位常用于定义元素的宽度、高度、边距、内边距等属性。为了在不同的DPR屏幕下让图片看起来都不失真,开发者需要为不同DPR的屏幕提供不同大小的图片资源。例如:
对于DPR = 1的设备,提供标准大小的图片。
对于DPR = 2的设备,提供两倍大小的图片(即图片宽度和高度都是原来的两倍)。
对于DPR = 3的设备,提供三倍大小的图片。
<img src="image-small.png" srcset="image-small.png 1x, image-medium.png 2x, image-large.png 3x">
22、promise有哪些状态和方法?
一、Promise的三种状态*Pending(进行中):这是Promise的初始状态,表示异步操作尚未完成,处于等待状态。*Fulfilled(已完成):表示异步操作已成功完成,并返回了结果。此时,Promise的状态从Pending变为Fulfilled。*Rejected(已拒绝):表示异步操作失败,并返回了错误原因。此时,Promise的状态从Pending变为Rejected。
二、Promise的实例方法then(onFulfilled, onRejected):catch(onRejected)finally(onFinally)三、Promise的静态方法Promise.resolve(value)返回一个状态为fulfilled的Promise对象,并将传入的值作为该Promise对象的结果。Promise.reject(reason)返回一个状态为rejected的Promise对象,并将传入的错误作为该Promise对象的结果.Promise.all(iterable)接收一个包含多个Promise对象的可迭代对象(如Promise数组),并返回一个新的PromisePromise.allSettled(iterable)只有当所有传入的Promise对象都变为settled(即fulfilled或rejected)时,返回的Promise才变为fulfilled。Promise.race(iterable)返回的Promise的状态和结果由第一个变为settled(fulfilled或rejected)的Promise决定Promise.any(iterable)只要存在一个Promise变为fulfilled,返回的Promise就变为fulfilled,其结果为第一个fulfilled的Promise的结果。
23、v-if和v-show的区别?
**相同点:
在 Vue.js 中,v-if 和 v-show 都用于条件渲染元素
**不同点:
1、实现原理v-if直接通过 添加/移除 DOM 元素 控制显示。若条件为 false,元素会被完全销毁,相关组件实例和事件监听也会被销毁。v-show通过 切换 CSS 的 display 属性(如 display: none)控制显示。元素始终存在于 DOM 中,只是视觉不可见。
2、性能差异v-if适合低频切换:条件变化时触发 DOM 增删,初始渲染成本较高,但条件稳定后无额外开销v-show适合高频切换:仅修改 CSS,性能开销小。
3、支持的指令v-if支持 v-else、v-else-if 实现多条件分支v-show不支持 v-else,只能控制单个元素的显示状态。
4、生命周期影响v-if条件变化时触发组件的 创建/销毁 生命周期(如 mounted/unmounted)。v-show不触发生命周期,仅改变样式,组件状态保持。
5、典型场景v-if需要完全移除元素(如权限控制、动态组件)。条件切换频率低。v-show需要频繁切换显示状态(如折叠菜单、模态框);需要保留元素状态(如表单输入值)。
24、apply、call、bind的相同点和不同点,以及如何使用,给出代码示例
一、相同点*改变 this 指向:这三个方法都可以用来改变函数调用时 this 的指向。*与函数相关:它们都是函数对象的方法
二、不同点
1、参数传递call 方法接受一个参数列表,第一个参数是 this 的值,后续参数按顺序传递给函数。apply 方法也接受 this 的值作为第一个参数,但后续参数是以数组(或类数组对象)的形式传递的。bind 方法返回一个新的函数,这个新函数在被调用时会将其 this 关键字设置为 bind 方法的第一个参数,同时可以接受一个参数列表(可选),这些参数会预先传递给原函数。
2、执行时机call 和 apply 会立即调用函数。bind 不会立即调用函数,而是返回一个新的函数,这个新函数在被调用时才会执行原函数。三、使用示例//call的使用function greet() {console.log(`Hello, ${this.name}!`);}const person = { name: 'Alice' };greet.call(person); // 输出: Hello, Alice!//apply的使用function sum(a, b) {return a + b;}const numbers = [3, 5];console.log(sum.apply(null, numbers)); // 输出: 8,这里 null 因为 sum 函数不依赖 this//bind的使用function describe(city, country) {console.log(`I'm visiting ${city} in ${country}`);}const visit = describe.bind(null, 'Paris'); // 绑定 this 为 null,并预先传递 'Paris'visit('France'); // 输出: I'm visiting Paris in France
25、js中计算一个数组的数组里面数据的和该用哪些方法?
计算一个二维数组(数组的数组)中所有数据的和
//方法一:使用嵌套的 for 循环
function sumNestedArray(arr) {let sum = 0;for (let i = 0; i < arr.length; i++) {for (let j = 0; j < arr[i].length; j++) {sum += arr[i][j];}}return sum;
}const nestedArray = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
console.log(sumNestedArray(nestedArray)); // 输出 45//方法二:使用 reduce 方法
//reduce 方法可以用来对数组中的元素进行累积操
function sumNestedArray(arr) {return arr.reduce((acc, subArray) => acc + subArray.reduce((sum, num) => sum + num, 0), 0);
}const nestedArray = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
console.log(sumNestedArray(nestedArray)); // 输出 45//方法三:使用 flat 方法
//flat 方法可以将多维数组展平为一维数组,然后使用 reduce 或其他方法求和
function sumNestedArray(arr) {const flattenedArray = arr.flat();return flattenedArray.reduce((sum, num) => sum + num, 0);
}const nestedArray = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
console.log(sumNestedArray(nestedArray)); // 输出 45//方法四:使用 forEach 方法
function sumNestedArray(arr) {let sum = 0;arr.forEach(subArray => {subArray.forEach(num => {sum += num;});});return sum;
}const nestedArray = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
console.log(sumNestedArray(nestedArray)); // 输出 45//方法五:使用递归(适用于不规则嵌套数组)
//如果数组的嵌套层次不固定,可以使用递归方法来求和。
function sumNestedArray(arr) {let sum = 0;arr.forEach(item => {if (Array.isArray(item)) {sum += sumNestedArray(item);} else {sum += item;}});return sum;
}const nestedArray = [1, [2, [3, 4], 5], 6, [7, 8, 9]];
console.log(sumNestedArray(nestedArray)); // 输出 45//方法六:使用 for...of 循环
function sumNestedArray(arr) {let sum = 0;for (const subArray of arr) {for (const num of subArray) {sum += num;}}return sum;
}const nestedArray = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
console.log(sumNestedArray(nestedArray)); // 输出 45
26、vue3中如何封装一个公共组件,要从那些方面考虑,并给出代码案例?
在 Vue 3 中封装公共组件时,需要从多个方面考虑,以确保组件的可复用性、灵活性和可维护性。以下是一些关键点和一个简单的代码示例:
1. 组件的可配置性Props:通过 props 提供组件的配置项,允许使用者自定义组件的行为和外观。默认值:为 props 提供默认值,以减少使用者的配置负担。类型检查:使用 TypeScript 或 Vue 的 PropType 来确保传入的值符合预期。
2. 事件交互自定义事件:通过 $emit发出事件,允许父组件监听和响应子组件的行为。事件命名:使用清晰的事件命名,避免与原生事件冲突。
3. 样式封装局部样式:使用 scoped 样式或 CSS Modules,避免样式污染。可覆盖样式:提供一些可覆盖的样式类或样式变量,方便使用者自定义样式。
4. 逻辑复用组合式 API:使用 Vue 3 的组合式 API (setup 函数) 来封装逻辑,便于复用。计算属性和侦听器:合理使用计算属性和侦听器来处理数据。
5. 文档和示例文档:为组件提供清晰的文档,说明 props、事件、插槽等的使用方法。示例:提供使用示例,帮助使用者快速上手。
//封装的组件
<template><div class="counter"><button @click="decrement">-</button><span>{{ count }}</span><button @click="increment">+</button></div>
</template><script setup>
import { ref, watch, defineProps, defineEmits } from 'vue';const props = defineProps({modelValue: {type: Number,default: 0,},min: {type: Number,default: 0,},max: {type: Number,default: 100,},
});
//emit 函数被定义为一个可以触发 'update:modelValue' 和 'change' 两个事件的对象。
//'update:modelValue' 通常用于V-Model的双向绑定更新。当组件内部需要更新父组件通过v-model绑定的值时,可以触发这个事件。
//'change' 事件则是一个自定义事件,可以在组件内部逻辑需要时触发,通知父组件或其他监听者某些状态或数据已经改变。
const emit = defineEmits(['update:modelValue', 'change']);const count = ref(props.modelValue);//watch 函数侦听的是一个箭头函数 () => props.modelValue 的返回值。
//通常是通过父组件传递给子组件的数据,使用v-model绑定时,modelValue 就是绑定的值。
//当 props.modelValue 发生变化时,watch 函数的回调函数会被调用,接收新的值 newValue 作为参数。
watch(() => props.modelValue, (newValue) => {count.value = newValue;
});function increment() {if (count.value < props.max) {count.value++;emit('update:modelValue', count.value);emit('change', count.value);}
}function decrement() {if (count.value > props.min) {count.value--;emit('update:modelValue', count.value);emit('change', count.value);}
}
</script><style scoped>
.counter {display: flex;align-items: center;gap: 8px;
}button {padding: 4px 8px;cursor: pointer;
}span {font-size: 16px;font-weight: bold;
}
</style>
//使用案例
<template><div><Counter v-model="counterValue" :min="0" :max="10" @change="handleChange" /><p>Counter Value: {{ counterValue }}</p></div>
</template><script setup>
import { ref } from 'vue';
import Counter from './Counter.vue';const counterValue = ref(5);function handleChange(value) {console.log('Counter changed:', value);
}
</script>
27、场景题:假设目前已经封装了一个公共组件,但在用时要根据不同的需求,再里边添加输入框(根据不同需求添加一个或多个),如何处理这种情况,给出代码示例
在这种场景下,可以通过 插槽(Slots) 来实现组件的灵活性。插槽允许父组件在子组件的特定位置插入自定义内容,从而满足不同需求。
父组件:根据需求在子组件中插入不同数量的输入框。
子组件:使用插槽来支持动态插入内容。
//公共组件CustomComponent.vue
<template><div class="custom-component"><div class="header"><slot name="header">默认头部内容</slot></div><div class="content">//补写name就是 默认插槽<slot>默认内容区域</slot></div><div class="footer"><slot name="footer">默认底部内容</slot></div></div>
</template><script setup>
import { defineProps, defineEmits } from 'vue';const props = defineProps({// 可以根据需要定义 props
});const emit = defineEmits([]);
</script><style scoped>
.custom-component {border: 1px solid #ccc;padding: 16px;border-radius: 8px;
}.header, .content, .footer {margin-bottom: 16px;
}
</style>
//父组件代码ParentComponent.vue
<template><div><h2>使用 CustomComponent 并插入输入框</h2><CustomComponent><!-- 插入一个输入框 --><template #content><div><label for="input1">输入框 1:</label><input id="input1" type="text" v-model="input1" /></div></template></CustomComponent><h2>使用 CustomComponent 并插入多个输入框</h2><CustomComponent><!-- 插入多个输入框 --><template #content><div><label for="input2">输入框 2:</label><input id="input2" type="text" v-model="input2" /></div><div><label for="input3">输入框 3:</label><input id="input3" type="text" v-model="input3" /></div></template></CustomComponent></div>
</template><script setup>
import { ref } from 'vue';
import CustomComponent from './CustomComponent.vue';const input1 = ref('');
const input2 = ref('');
const input3 = ref('');
</script>tips:
在 Vue 3 中,#default 是默认插槽的别名,而 # 是 v-slot 的简写。因此,#content 实际上是默认插槽的一种自定义命名方式。如果子组件中没有显式定义名为 content 的具名插槽,Vue 会将 #content 的内容插入到默认插槽中。
28、具名插槽,有什么作用和如何使用,给出代码示例
它允许父组件在子组件的特定位置插入内容。具名插槽的作用是让父组件能够更精确地控制子组件的内部结构,同时保持子组件的通用性和灵活性。具名插槽通常用于需要在子组件的多个不同位置插入内容的场景。一、如何使用具名插槽1、定义具名插槽在子组件中,使用 <slot name="slotName"> 定义具名插槽。name 属性用于标识插槽的名称。2. 使用具名插槽在父组件中,使用 <template #slotName> 或 <template v-slot:slotName> 将内容插入到具名插槽中。
29、vue2h和vue3中父子传值方式有何不同,给出代码案例
一、从父组件向子组件传值
在 Vue 2 中,父组件通过 props 向子组件传值。
在 Vue 3 中,父组件同样通过 props 向子组件传值,但可以使用 defineProps 来更简洁地定义 props。//vue2子组件
<template><div><p>从父组件接收到的值:{{ message }}</p></div>
</template><script>
export default {props: {message: {type: String,default: ''}}
};
</script>//vue2父组件
<template><div><ChildComponent :message="parentMessage" /></div>
</template><script>
import ChildComponent from './ChildComponent.vue';export default {components: {ChildComponent},data() {return {parentMessage: 'Hello from Parent!'};}
};
</script>//vue3子组件
<template><div><p>从父组件接收到的值:{{ message }}</p></div>
</template><script setup>
import { defineProps } from 'vue';const props = defineProps({message: {type: String,default: ''}
});
</script>//vue3父组件
<template><div><ChildComponent :message="parentMessage" /></div>
</template><script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';const parentMessage = ref('Hello from Parent!');
</script>二、从子组件向父组件传值在 Vue 2 中,子组件通过 $emit 向父组件发送事件,父组件通过监听这些事件来接收数据在 Vue 3 中,子组件同样通过 emit 向父组件发送事件,父组件通过监听这些事件来接收数据。但可以使用 defineEmits 来更简洁地定义事件。defineEmits 的主要作用是提供类型安全,确保发出的事件名称和参数类型正确。tips:在 Vue 3 中,推荐使用 emit 而不是 $emit。这是因为在 Vue 3 的组合式 API (setup 函数) 中,emit 是通过 defineEmits 定义的,它是一个更简洁和类型安全的方式。而 $emit 是 Vue 2 中的用法,虽然在 Vue 3 中仍然可以使用,但不推荐在组合式 API 中使用。
虽然在 Vue 3 中仍然可以使用 $emit,但通常只在选项式 API 中使用。在组合式 API 中,推荐使用 emit。//vue2子组件
<template><div><button @click="sendMessage">发送消息到父组件</button></div>
</template><script>
export default {methods: {sendMessage() {this.$emit('message-标识-sent', '传递的值');}}
};
</script>//vue2父组件
<template><div><ChildComponent @message-标识-sent="handleMessage" /><p>从子组件接收到的消息:{{ childMessage }}</p></div>
</template><script>
import ChildComponent from './ChildComponent.vue';export default {components: {ChildComponent},data() {return {childMessage: ''};},methods: {handleMessage(message) {this.childMessage = message;}}
};
</script>//vue3子组件
<template><div><button @click="sendMessage">发送消息到父组件</button></div>
</template><script setup>
import { defineEmits } from 'vue';const emit = defineEmits(['message-标识-sent']);const sendMessage = () => {emit('message-标识-sent', '传递的值');
};
</script>//vue3父组件
<template><div><ChildComponent @message-标识-sent="handleMessage" /><p>从子组件接收到的消息:{{ childMessage }}</p></div>
</template><script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';const childMessage = ref('');const handleMessage = (message) => {childMessage.value = message;
};
</script>三、双向绑定(v-model)在 Vue 2 中,v-model 默认绑定到子组件的 value 属性和 input 事件。如果需要自定义 v-model,可以通过 model 选项来实现。在 Vue 3 中,v-model 的使用更加灵活,可以通过 defineProps 和 defineEmits 来实现自定义 v-model。//vue2子组件
<template><div><input :value="value" @input="$emit('input', $event.target.value)" /></div>
</template><script>
export default {props: ['value']
};
</script>//vue2父组件
<template><div><ChildComponent v-model="parentValue" /><p>父组件的值:{{ parentValue }}</p></div>
</template><script>
import ChildComponent from './ChildComponent.vue';export default {components: {ChildComponent},data() {return {parentValue: 'Initial Value'};}
};
</script>//vue3子组件
<template><div><input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /></div>
</template><script setup>
import { defineProps, defineEmits } from 'vue';const props = defineProps({modelValue: String
});const emit = defineEmits(['update:modelValue']);
</script>//vue3父组件
<template><div><ChildComponent v-model="parentValue" /><p>父组件的值:{{ parentValue }}</p></div>
</template><script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';const parentValue = ref('Initial Value');
</script>
30、let、 var 、const的区别?
1、作用域(1)var 声明的变量在函数内有效,如果在全局作用域中声明,则在全局范围内有效,会变量提升(2)let 声明的变量在块级作用域内有效(如 if、for 等),不会变量提升(3)const 声明的变量在块级作用域内有效(如 if、for 等),不会变量提升
2、可变性 (1)var 声明的变量可以被重新赋值。(2)let 声明的变量可以被重新赋值(3)const 声明的变量不能被重新赋值,但对象或数组的属性或元素可以被修改。3、重复声明(1)var 允许重复声明变量,后面的声明会覆盖前面的声明。(2)在同一个块级作用域内,let 不允许重复声明变量(3)在同一个块级作用域内,const 不允许重复声明变量
tips:优先使用 const:默认情况下,使用 const 声明变量,除非需要重新赋值,才使用 let。避免使用 var:var 由于其提升行为和作用域问题,容易导致错误,应尽量避免使用。
31、在vue3组合式中,hooks是什么,有什么作用,如何封装一个hooks
在 Vue 3 的组合式 API 中,Hooks(通常称为组合式函数或 Composables)是一种用于封装和复用有状态逻辑的函数。这些函数可以包含响应式数据、计算属性、生命周期钩子等,从而实现代码的高内聚和低耦合*Hooks 的作用:(1)代码复用:允许在多个组件中共享逻辑,避免重复代码。(2)逻辑分离:将特定功能的逻辑封装到一个函数中,使组件的 setup 函数更加清晰。(3)响应式连接:返回的响应式数据与 Vue 的响应式系统相连,自动触发组件更新。*封装一个 Hook 的步骤如下:(1)定义函数:创建一个函数,通常以 use 开头,例如 useCounter。(2)使用组合式 API:在函数内部使用 ref、reactive、computed、watch 等组合式 API 来创建响应式数据和逻辑。(3)返回对象:返回一个包含响应式数据和方法的对象。//封装一个简单的 useCounter Hook
// useCounter.js
import { ref } from 'vue';export function useCounter(initialValue = 0) {const count = ref(initialValue);const increment = () => {count.value++;};const decrement = () => {count.value--;};return {count,increment,decrement,};
}//在组件中使用封装的 Hook
<template><div><p>Count: {{ count }}</p><button @click="increment">Increment</button><button @click="decrement">Decrement</button></div>
</template><script setup lang="ts">
import { useCounter } from './useCounter';const { count, increment, decrement } = useCounter(5);
</script>
//封装一个更复杂的 Hook:useMouse
// useMouse.ts
import { onMounted, onUnmounted, ref } from 'vue';export function useMouse() {const x = ref(0);const y = ref(0);function update(event: MouseEvent) {x.value = event.pageX;y.value = event.pageY;}onMounted(() => window.addEventListener('mousemove', update));onUnmounted(() => window.removeEventListener('mousemove', update));return { x, y };
}//在组件中使用 useMouse
<template><div>Mouse position: {{ x }}, {{ y }}</div>
</template><script setup lang="ts">
import { useMouse } from './useMouse';const { x, y } = useMouse();
</script>
//封装异步逻辑的 Hook:useFetch
// useFetch.ts
import { ref, onMounted } from 'vue';export function useFetch(url: string) {const data = ref(null);const loading = ref(true);const error = ref(null);const fetchData = async () => {try {loading.value = true;const response = await fetch(url);data.value = await response.json();} catch (err) {error.value = err;} finally {loading.value = false;}};onMounted(fetchData);return { data, loading, error, fetchData };
}//在组件中使用 useFetch
<template><div><p v-if="loading">Loading...</p><p v-else-if="error">Error: {{ error }}</p><div v-else><pre>{{ JSON.stringify(data, null, 2) }}</pre></div></div>
</template><script setup lang="ts">
import { useFetch } from './useFetch';const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');
</script>
32、es6新增了哪些?
1、变量声明
(1)let 和 const:这两个关键字用于声明变量。let 声明的变量具有块级作用域,避免了变量提升的问题;而const 用于声明常量,一旦赋值后不可更改2、箭头函数
3、模板字符串 使用反引号(`)包围,并通过 ${} 插入变量。
4、解构赋值
5、Promise提供了三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)
6、扩展运算符
//合并对象 .assign
//把lisi 的所有的属性 给了 zhangsan , lisi没有任何的变化var zhangsan = {name:'zhangsan'};var lisi = {age:18};Object.assign(zhangsan,lisi)let res = Object.assign(zhangsan,lisi)//把lisi合并到zhangsan console.log(zhangsan)//{name: "zhangsan", age: 18}console.log(res)//{name: "zhangsan", age: 18}console.log(lisi)//{age: 18}
7、SymbolSymbol 是一种新的原始数据类型,常用于创建唯一的键名,以解决对象属性名冲突的问题
8、Map 和 SetMap 和 Set 是新的数据结构,分别用于存储键值对和集合,它们提供了更高效的数据操作方式
9、迭代器和生成器 迭代器接口和生成器函数(使用 yield 关键字)使得异步编程更加高效和优雅
10、默认参数 函数可以设置默认参数值,当调用函数时未传递参数时,将使用默认值
11、模块化 ES6 引入了 import 和 export 语句,支持模块化编程,使代码组织更加清晰和可维护
12、其他特性剩余参数(Rest Parameters):用于将多余的参数收集到一个数组中。Proxy 和 Reflect:提供了更强大的元编程能力,Proxy 用于拦截对象操作,Reflect 提供了操作对象的方法
33、es6中的Proxy如何用?
一、作用Proxy 是 ES6 中引入的一个强大的特性,它允许你创建一个代理对象,从而拦截并自定义对目标对象的操作。通过 Proxy,你可以对对象的读取、设置、删除等操作进行拦截和控制。二、基本语法const proxy = new Proxy(target, handler);target:要代理的目标对象(可以是任何类型的对象,包括数组、函数等)。handler:一个对象,其属性是当执行各种操作时定义代理的行为的函数。常见的拦截操作:(1)get(target, prop, receiver):拦截对象属性的读取操作。(2)set(target, prop, value, receiver):拦截对象属性的设置操作。(3)has(target, prop):拦截 in 操作符。(4)deleteProperty(target, prop):拦截 delete 操作符。(5)ownKeys(target):拦截Object.getOwnPropertyNames和Object.getOwnPropertySymbols。(6)apply(target, thisArg, argumentsList):拦截函数调用。(7)construct(target, argumentsList, newTarget):拦截 new 操作符。三、示例代码1、拦截属性的读取和设置const target = {name: 'Kimi',age: 25};const handler = {get(target, prop, receiver) {console.log(`Getting property: ${prop}`);return Reflect.get(target, prop, receiver);},set(target, prop, value, receiver) {console.log(`Setting property: ${prop} to ${value}`);return Reflect.set(target, prop, value, receiver);}};const proxy = new Proxy(target, handler);console.log(proxy.name); // 输出: Getting property: nameproxy.age = 26; // 输出: Setting property: age to 26console.log(proxy.age); // 输出: Getting property: age2、拦截in操作符const target = {name: 'Kimi',age: 25};const handler = {has(target, prop) {console.log(`Checking if property exists: ${prop}`);return Reflect.has(target, prop);}};const proxy = new Proxy(target, handler);console.log('name' in proxy); // 输出: Checking if property exists: nameconsole.log('gender' in proxy); // 输出: Checking if property exists: gender3、拦截delete 操作符const target = {name: 'Kimi',age: 25};const handler = {deleteProperty(target, prop) {console.log(`Deleting property: ${prop}`);return Reflect.deleteProperty(target, prop);}};const proxy = new Proxy(target, handler);delete proxy.name; // 输出: Deleting property: nameconsole.log(target); // 输出: { age: 25 }4、拦截函数调用const target = function (x, y) {return x + y;};const handler = {apply(target, thisArg, argumentsList) {console.log(`Calling function with arguments: ${argumentsList}`);return Reflect.apply(target, thisArg, argumentsList);}};const proxy = new Proxy(target, handler);console.log(proxy(2, 3)); // 输出: Calling function with arguments: 2,3
34、事件循环
一、浏览器有哪些进程和线程?浏览器是⼀个多进程多线程的应⽤程序1. 浏览器进程主要负责界⾯显示、⽤户交互、⼦进程管理等。浏览器进程内部会启动多个线程处理不同的任务。2. ⽹络进程负责加载⽹络资源。⽹络进程内部会启动多个线程来处理不同的⽹络任务。3. 渲染进程(本节课重点讲解的进程)渲染进程启动后,会开启⼀个渲染主线程,主线程负责执⾏ HTML、CSS、JS 代码。默认情况下,浏览器会为每个标签⻚开启⼀个新的渲染进程,以保证不同的标签⻚之间不相互影响。
二、渲染主线程是如何⼯作的?渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:解析 HTML解析 CSS计算样式布局处理图层每秒把⻚⾯画 60 次执⾏全局 JS 代码执⾏事件处理函数执⾏计时器的回调函数
35、如何理解 JS 的异步?
JS是⼀⻔单线程的语⾔,这是因为它运⾏在浏览器的渲染主线程中,⽽渲染主线程只有⼀个。
⽽渲染主线程承担着诸多的⼯作,渲染⻚⾯、执⾏ JS 都在其中运⾏。如果使⽤同步的⽅式,就极有可能导致主线程产⽣阻塞,从⽽导致消息队列中的很多其他任务⽆法得到执⾏。这样⼀来,⼀⽅⾯会导致繁忙的主线程⽩
⽩的消耗时间,另⼀⽅⾯导致⻚⾯⽆法及时更新,给⽤户造成卡死现象。所以浏览器采⽤异步的⽅式来避免。具体做法是当某些任务发⽣时,⽐如计时器、⽹络、事件监听,主线程将任务交给其他线程去处理,⾃身⽴即结束任务的执⾏,转⽽执⾏后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加⼊到消息队列的末尾排队,等待主线程调度执⾏。
在这种异步模式下,浏览器永不阻塞,从⽽最⼤限度的保证了单线程的流畅运⾏
36、阐述⼀下 JS 的事件循环?
事件循环⼜叫做消息循环,是浏览器渲染主线程的⼯作⽅式。在 Chrome 的源码中,它开启⼀个不会结束的 for 循环,每次循环从消息队列中取出第⼀个任务执⾏,⽽其他线程只需要在合适的时候将任务加⼊到队列末尾即可。过去把消息队列简单分为宏队列和微队列,这种说法⽬前已⽆法满⾜复杂的浏览器环境,取⽽代之的是⼀种更加灵活多变的处理⽅式。
根据 W3C 官⽅的解释,每个任务有不同的类型,同类型的任务必须在同⼀个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在⼀次事件循环中,由浏览器⾃⾏决定取哪⼀个队列的任务。但浏览器必须有⼀个微队列,微队列的任务⼀定具有最⾼的优先级,必须优先调度执⾏。
37、Cookie、sessionStorage、localStorage 的区别?
相同点:
• 存储在客⼾端
不同点:• cookie数据⼤⼩不能超过4k;sessionStorage和localStorage的存储⽐cookie⼤得多,可以达到 5M+ • cookie设置的过期时间之前⼀直有效;localStorage永久存储,浏览器关闭后数据不丢失除⾮主动 删除数据;sessionStorage数据在当前浏览器窗⼝关闭后⾃动删除• cookie的数据会⾃动的传递到服务器;sessionStorage和localStorage数据保存在本地
38、粘包问题分析与对策?
TCP粘包是指发送⽅发送的若⼲包数据到接收⽅接收时粘成⼀包,从接收缓冲区看,后⼀包数据的头紧 接着前⼀包数据的尾。
粘包出现原因
简单得说,在流传输中出现,UDP不会出现粘包,因为它有消息边界
粘包情况有两种,⼀种是粘在⼀起的包都是完整的数据包 ,另⼀种情况是 粘在⼀起的包有不完整的 包 。
为了避免粘包现象,可采取以下⼏种措施:
(1)对于发送⽅引起的粘包现象,⽤⼾可通过编程设置来避免, TCP提供了强制数据⽴即传送的操作 指令push ,TCP软件收到该操作指令后,就⽴即将本段数据发送出去,⽽不必等待发送缓冲区满;
(2)对于接收⽅引起的粘包,则可通过优化程序设计、精简接收进程⼯作量、 提⾼接收进程优先级 等措施 ,使其及时接收数据,从⽽尽量避免出现粘包现象;
(3)由接收⽅控制,将⼀包数据按结构字段,⼈为控制分多次接收,然后合并,通过这种⼿段来避免 粘包。 分包多发 。 以上提到的三种措施,都有其不⾜之处。 * 第⼀种编程设置⽅法虽然可以避免发送⽅引起的粘包,但它关闭了优化算法,降低了⽹络发送 效率,影响应⽤程序的性能,⼀般不建议使⽤。 * 第⼆种⽅法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较⾼时,或由于 ⽹络突发可能使某个时间段数据包到达接收⽅较快,接收⽅还是有可能来不及接收,从⽽导致粘包。 * 第三种⽅法虽然避免了粘包,但应⽤程序的效率较低,对实时应⽤的场合不适合。 ⼀种⽐较周全的对策是:接收⽅创建⼀预处理线程,对接收到的数据包进⾏预处理,将粘连的包分 开。实验证明这种⽅法是⾼效可⾏的。
39、HTTP 请求跨域问题
1. 跨域的原理
2. 跨域,是指浏览器不能执⾏其他⽹站的脚本。它是由浏览器的 同源策略 造成的。 同源策略,是浏览器对 JavaScript 实施的安全限制,只要 协议、域名、端⼝ 有任何⼀个不同,都 被当作是不同的域。 跨域原理,即是通过各种⽅式, 避开浏览器的安全限制 。
3. 解决⽅案
4. 最初做项⽬的时候,使⽤的是jsonp,但存在⼀些问题,使⽤get请求不安全,携带数据较⼩,后来 也⽤过iframe,但只有主域相同才⾏,也是存在些问题,后来通过了解和学习发现使⽤代理和 proxy代理配合起来使⽤⽐较⽅便,就引导后台按这种⽅式做下服务器配置,在开发中使⽤proxy, 在服务器上使⽤nginx代理,这样开发过程中彼此都⽅便,效率也⾼;现在h5新特性还有 windows.postMessage() ◦
5. JSONP:ajax 请求受同源策略影响,不允许进⾏跨域请求,⽽ script 标签 src 属性中的链 接却可以访问 跨域的 js 脚本,利⽤这个特性,服务端不再返回 JSON 格式的数据,⽽是 返回⼀段调⽤某个函 数的 js 代码,在 src 中进⾏了调⽤,这样实现了跨域。
6.步骤:i. 去创建⼀个script标签ii. script的src属性设置接⼝地址 iii. 接⼝参数,必须要带⼀个⾃定义函数名,要不然后台⽆法返回数据iv. 通过定义函数名去接受返回的数据 //动态创建 scriptvar script = document.createElement('script');// 设置回调函数function getData(data) { console.log(data);}//设置 script 的 src 属性,并设 置请求地址script.src = 'http://localhost:3000/?callback=getData';// 让 script ⽣效document.body.appendChild(script); JSONP 的缺点: JSON 只⽀持 get,因为 script 标签只能使⽤ get 请求; JSONP 需要后端配合返回指定格式的 数据。 document.domain 基础域名相同 ⼦域名不同 ◦ window.name 利⽤在⼀个浏览器窗⼝内,载⼊所有的域名都是共享⼀个window.name ◦CORS CORS(Cross-origin resource sharing)跨域资源共享 服务器设置对CORS的⽀持原理:服 务器设置Access-Control-Allow-Origin HTTP响应头之后,浏览器将会允许跨域请求 ◦proxy代理 ⽬前常⽤⽅式,通过服务器设置代理 ◦window.postMessage() 利⽤h5新特性window.postMessage()
40、http 和 https 的区别及优缺点?
• http 是超⽂本传输协议,信息是明⽂传输,HTTPS 协议要⽐ http 协议 安全 ,https 是具有安全性 的 ssl 加密传输协议,可防⽌数据在传输过程中被窃取、改变,确保数据的完整性(当然这种安全性 并⾮绝对的,对于更深⼊的 Web 安全问题,此处暂且不表)。
• http 协议的 默认端⼝ 为 80,https 的默认端⼝为 443。
• http 的连接很简单,是⽆状态的。https 握⼿阶段⽐较 费时 ,会使⻚⾯加载时间延⻓ 50%,增加 10%~20%的耗电。
• https 缓存 不如 http ⾼效,会增加数据开销。
• Https 协议需要 ca 证书,费⽤较⾼,功能越强⼤的 证书费 ⽤越⾼。
• SSL 证书需要绑定 域名 。
41、https 协议的⼯作原理
客⼾端在使⽤ HTTPS ⽅式与 Web 服务器通信时有以下⼏个步骤:
1.客⼾端使⽤ https url 访问服务器,则要求 web 服务器 建⽴ ssl 链接 。
2.web 服务器接收到客⼾端的请求之后,会 将⽹站的证书(证书中包含了公钥),传输给客⼾端 。
3.客⼾端和 web 服务器端开始 协商 SSL 链接的安全等级 ,也就是加密等级。
4.客⼾端浏览器通过双⽅协商⼀致的安全等级, 建⽴会话密钥 ,然后通过⽹站的公钥来加密会话密 钥,并传送给⽹站。
5.web 服务器 通过⾃⼰的私钥解密出会话密钥 。
6.web 服务器 通过会话密钥加密与客⼾端之间的通信 。
42、TCP三次握⼿
1. 第⼀次握⼿: 建⽴连接时,客⼾端发送syn包(syn=j)到服务器,并进⼊SYN_SENT状态,等待 服务器确认 ;SYN:同步序列编号(Synchronize Sequence Numbers)。
2. 第⼆次握⼿: 服务器收到syn包并确认客⼾的SYN (ack=j+1), 同时也发送⼀个⾃⼰的SYN包 (syn=k),即SYN+ACK包,此时服务器进⼊SYN_RECV状态;
3. 第三次握⼿: 客⼾端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1) ,此包发 送完毕,客⼾端和服务器进⼊ESTABLISHED(TCP连接成功)状态,完成三次握⼿。
4. 握⼿过程中传送的包⾥不包含数据,三次握⼿完毕后,客⼾端与服务器才正式开始传送数据。
43、TCP 四次挥⼿
1.客⼾端进程发出连接释放报⽂ ,并且停⽌发送数据。释放数据报⽂⾸部,FIN=1,其序列号为 seq=u(等于前⾯已经传送过来的数据的最后⼀个字节的序号加1),此时, 客⼾端进⼊FIN- WAIT-1(终⽌等待1)状态 。 TCP规定,FIN报⽂段即使不携带数据,也要消耗⼀个序号。
2.服务器收到连接释放报⽂,发出确认报⽂ ,ACK=1,ack=u+1,并且带上⾃⼰的序列号seq=v, 此时, 服务端就进⼊了CLOSE-WAIT(关闭等待)状态 。TCP服务器通知⾼层的应⽤进程,客⼾端向 服务器的⽅向就释放了,这时候处于半关闭状态,即客⼾端已经没有数据要发送了,但是服务器若发 送数据,客⼾端依然要接受。这个状态还要持续⼀段时间,也就是整个CLOSE-WAIT状态持续的时间。
3.客⼾端收到服务器的确认请求后,此时, 客⼾端就进⼊FIN-WAIT-2(终⽌等待2)状态 ,等待 服务器发送连接释放报⽂(在这之前还需要接受服务器发送的最 后的数据)。
4.服务器将最后的数据发送完毕后,就向客⼾端发送连接释放报⽂ ,FIN=1,ack=u+1,由于在半 关闭状态,服务器很可能⼜发送了⼀些数据,假定此时的序列号为seq=w,此时, 服务器就进⼊了 LAST-ACK(最后确认)状态 ,等待客⼾端的确认。
5.客⼾端收到服务器的连接释放报⽂后,必须发出确认 ,ACK=1,ack=w+1,⽽⾃⼰的序列号是 seq=u+1,此时, 客⼾端就进⼊了TIME-WAIT(时间等待)状态 。注意此时TCP连接还没有释放, 必须经过2X区MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状
6.服务器只要收到了客户端发出的确认,立即进入CLOSED状态 。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
44、TCP/IP / 如何保证数据包传输的有序可靠?
对字节流分段并进⾏编号然后 通过 ACK 回复 和 超时重发 这两个机制来保证。
(1)为了保证数据包的可靠传递,发送⽅必须把已发送的数据包保留在缓冲区;
(2)并为每个已发送的数据包启动⼀个超时定时器;
(3)如在定时器超时之前收到了对⽅发来的应答信息(可能是对本包的应答,也可以是对本包后续包 的应答),则释放该数据包占⽤的缓冲区;
(4)否则,重传该数据包,直到收到应答或重传次数超过规定的最⼤次数为⽌。
(5)接收⽅收到数据包后,先进⾏CRC校验,如果正确则把数据交给上层协议,然后给发送⽅发送⼀ 个累计应答包,表明该数据已收到,如果接收⽅正好也有数据要发给发送⽅,应答包也可⽅在数据包 中捎带过去。
45、TCP和UDP的区别
1. TCP是⾯向 链接 的,⽽UDP是⾯向⽆连接的。
2. TCP仅⽀持 单播传输 ,UDP 提供了单播,多播,⼴播的功能。
3. TCP的三次握⼿保证了连接的 可靠性 ; UDP是⽆连接的、不可靠的⼀种数据传输协议,⾸先不可靠 性体现在⽆连接上,通信都不需要建⽴连接,对接收到的数据也不发送确认信号,发送端不知道数 据是否会正确接收。
4. UDP的 头部开销 ⽐TCP的更⼩,数据 传输速率更⾼ , 实时性更好 。
46、vue3中组件懒加载如何做?
//一:使用 defineAsyncComponent 定义异步组件
<template><div><AsyncComponent /></div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
//组件懒加载
const AsyncComponent = defineAsyncComponent(() =>import('./components/MyComponent.vue')
);//普通加载
//import AsyncComponent from '@/components/MyComponent.vue'
</script>//二:在路由中实现懒加载
//当用户访问 /foo 路由时,才会加载 Foo.vue 组件
const routes = [{path: '/foo',component: () => import('./Foo.vue')}
];//三:使用 <Suspense> 组件处理加载状态
<template><Suspense>//默认加载的<template #default><Tableview /></template>//出错时加载的<template #fallback><div class="loading-state"><FailView /><p>内容正在加载中,请稍候...</p></div></template></Suspense>
</template>
47、前端如何解决不同浏览器及不同版本的兼容问题?
在前端开发中,解决不同浏览器及不同版本的兼容性问题是一个常见的挑战。以下是一些有效的解决方案和策略:
1. 使用标准的 HTML/CSS/JavaScript
遵循W3C等标准化组织制定的Web标准是解决兼容性问题的基础。使用HTML5、CSS3和ES6等现代标准可以确保代码在大多数现代浏览器中的兼容性。
2. 使用CSS Reset 或 Normalize.css
CSS Reset 可以消除浏览器默认样式的差异,使不同浏览器中的样式表现一致。常见的CSS Reset 库有 Normalize.css 和 Reset.css。
Normalize.css:保留了一些有用的默认样式,适合现代开发。
Reset.css:将所有元素的默认样式重置为一致的基准。
3. 利用现代化工具和库
使用现代化的工具和库可以帮助开发者更容易地解决兼容性问题:
Babel:将 ES6+ 代码编译为 ES5,确保在旧浏览器中的兼容性。
PostCSS 和 Autoprefixer:自动为 CSS 属性添加浏览器前缀,确保在不同浏览器中的兼容性。
4. 进行跨浏览器测试
跨浏览器测试是确保前端代码在不同浏览器中兼容性的关键步骤:
BrowserStack:在线跨浏览器测试工具,支持真实设备和浏览器测试。
Sauce Labs:提供广泛的浏览器和设备支持,支持自动化测试。
5. 采用渐进增强和优雅降级策略
渐进增强:从基础功能开始构建网站,逐步添加高级功能,确保即使浏览器不支持某些高级功能,用户仍能使用基本功能。
优雅降级:从高级功能开始构建网站,为不支持这些功能的浏览器提供替代方案。
6. 使用 Polyfill 和 Shim
Polyfill 和 Shim 可以在旧浏览器中实现新功能,提高兼容性:
Polyfill.io:根据浏览器特性动态加载所需的 Polyfill。
Modernizr:检测浏览器特性,并根据特性加载所需的 Polyfill。
7. 使用成熟的框架和库
使用成熟的前端框架和库可以减少兼容性问题:
React:虚拟 DOM 和组件化设计,减少兼容性问题。
Vue.js:双向数据绑定和组件化设计,提高开发效率。
Angular:模块化设计和依赖注入,减少兼容性问题。
8. 注意 JavaScript 兼容性
对于 JavaScript 的兼容性问题,可以采用以下方法:
事件绑定:统一使用兼容性更好的事件绑定方法,如 addEventListener 和 attachEvent 的兼容写法。
DOM 操作:避免使用过时的 DOM 方法,统一使用标准的 DOM 方法。
变量声明:在声明变量时,一律加上 var 关键字,避免歧义。
9. 使用 CSS 前缀
为 CSS 属性添加浏览器前缀可以确保在不同浏览器中的兼容性。例如,使用 Autoprefixer 自动添加前缀。
10. 关注浏览器更新
及时关注浏览器的更新和变化,可以帮助开发者更好地解决兼容性问题。
通过以上方法,可以有效解决前端开发中的浏览器兼容性问题,提高用户体验和代码质量。
48、webSocket的使用
WebSocket是在单个TCP连接上提供全双工通信的协议。它允许双方同时进行数据传输,而不需要等待对方的响应,简单说就是他是服务器和客户端相互主动传输信息的约定协议。
优点:
双向通信:WebSocket 允许客户端和服务器之间进行双向通信,客户端可以主动向服务器发送消息,而不需要等待服务器的请求
低延迟:WebSocket 连接在建立后保持打开状态,减少了每次请求和响应所需的开销,从而降低了延迟。
持久连接:WebSocket 连接是持久的,允许长时间保持连接而无需重新建立。
跨域支持:WebSocket 协议支持跨域通信,可以与不同源的服务器进行交互。
总之,WebSocket 提供了一种高效、灵活的方式来实现实时数据传输,特别适合需要快速和频繁更新数据的应用程序。//javascript代码
// 初始化 WebSocket 实例,连接到指定的 WebSocket 服务器 (后端地址10.10.10.100:8080)
const webSocket = new WebSocket('ws://10.10.10.100:8080/webSocket');
//检查WebSocket连接状态 if (webSocket.readyState === WebSocket.CONNECTING) {console.log("连接正在建立..."); } else if (webSocket.readyState === WebSocket.OPEN) {console.log("连接已建立!"); } else if (webSocket.readyState === WebSocket.CLOSING) {console.log("连接正在关闭..."); } else if (webSocket.readyState === WebSocket.CLOSED) {console.log("连接已关闭!"); }//连接成功时的回调函数
webSocket.onopen = (event)=>{console.log('连接已建立!'); //在这里可以执行连接成功后的操作,例如发送数据webSocket.send('Hello, WebSocket Server!');
};//接收到消息时的回调函数
webSocket.onmessage = (event)=>{//在这里可以处理接收到的消息console.log('接收到消息:', event.data);
};//连接关闭时的回调函数
webSocket.onclose = (event)=> {// 在这里可以执行连接关闭后的操作console.log('连接已关闭!');
};//连接发生错误时的回调函数
webSocket.onerror = (error)=> {//在这里可执行连接失败可重连console.error('连接发生错误:', error);
};
48.5vue3项目如何使用 Websocket 及时通信技术,处理审批消息,提高系统的实时性和效率
在 Vue 3 项目中使用 WebSocket 实现实时通信技术,可以显著提高系统的实时性和效率,特别是在处理审批消息等需要即时反馈的场景中。以下是一个基本的实现步骤和示例:实现步骤1、引入 WebSocket 客户端库:你可以使用浏览器原生的 WebSocket 对象,或者使用第三方库(如 socket.io-client)来简化 WebSocket 的使用。2、建立 WebSocket 连接:在 Vue 组件的生命周期钩子中(如 onMounted)建立 WebSocket 连接。3、处理 WebSocket 消息:定义消息处理函数,用于接收和处理来自服务器的消息。4、发送 WebSocket 消息:根据需要,通过 WebSocket 连接发送消息到服务器。5、关闭 WebSocket 连接:在组件卸载时(如 onUnmounted)关闭 WebSocket 连接,以释放资源。示例代码以下是一个使用原生 WebSocket 的简单示例:<template><div><h1>审批消息</h1><ul><li v-for="message in messages" :key="message.id">{{ message.content }}</li></ul></div>
</template><script>
import { ref, onMounted, onUnmounted } from 'vue';export default {setup() {const messages = ref([]);let socket;let reconnectInterval = 5000; // 重连间隔时间(毫秒)let heartbeatInterval;const heartbeatMessage = JSON.stringify({ type: 'ping' }); // 心跳消息格式const connectWebSocket = () => {socket = new WebSocket('ws://your-websocket-server-url');socket.onopen = () => {console.log('WebSocket connection established');// 可以在这里发送初始消息或订阅特定频道startHeartbeat();};socket.onmessage = (event) => {const message = JSON.parse(event.data);messages.value.push(message);};socket.onerror = (error) => {console.error('WebSocket error:', error);// 可以在这里执行一些错误处理逻辑,但通常重连逻辑在 onclose 中处理};socket.onclose = () => {console.log('WebSocket connection closed');stopHeartbeat();// 尝试重连setTimeout(connectWebSocket, reconnectInterval); };};//心跳消息const startHeartbeat = () => {if (!heartbeatInterval) {heartbeatInterval = setInterval(() => {if (socket && socket.readyState === WebSocket.OPEN) {socket.send(heartbeatMessage);}}, 30000); // 每30秒发送一次心跳消息}};const stopHeartbeat = () => {if (heartbeatInterval) {clearInterval(heartbeatInterval);heartbeatInterval = null;}}; onMounted(() => {connectWebSocket();});onUnmounted(() => {if (socket) {socket.close();}});return {messages,};},
};
</script><style scoped>
/* 添加一些基本样式 */
</style>1、自动重连机制:
在 onclose 回调中,使用 setTimeout 调用 connectWebSocket 函数,尝试在连接关闭后重新连接。
reconnectInterval 变量控制重连的时间间隔(这里设置为 5000 毫秒,即 5 秒)。2、心跳检测:
使用 setInterval 定期发送心跳消息(如每 30 秒发送一次)。
startHeartbeat 函数用于启动心跳检测,stopHeartbeat 函数用于停止心跳检测。
在 onopen 回调中启动心跳检测,在 onclose 回调中停止心跳检测。
3、WebSocket 状态检查:
在发送心跳消息前,检查 socket.readyState 是否为 WebSocket.OPEN,以确保连接是打开的。
4、资源释放:
在组件卸载时(onUnmounted),关闭 WebSocket 连接并停止心跳检测,以释放资源。
49、route和router的区别?
一: router(路由器)
1、定义:*router 是一个路由管理器,它负责管理整个应用的路由规则和导航逻辑。它是一个全局对象,通常在整个应用中只有一个实例。*它定义了如何根据不同的 URL 映射到不同的页面或组件,并提供了方法来控制导航(例如跳转、返回等)。
2、职责:
*定义路由规则:router 配置了所有可能的路由路径(URL)以及对应的组件或页面。
*监听 URL 变化:当用户在浏览器中输入 URL 或点击链接时,router 会监听这些变化,并根据配置的路由规则进行处理。
*控制导航:router 提供了方法(如 push、replace、go 等)来编程式地控制页面跳转。
*管理路由状态:router 管理整个应用的路由状态,包括当前的路径、历史记录等。
import { createRouter, createWebHistory } from 'vue-router';
import Home from './views/Home.vue';
import About from './views/About.vue';
const router = createRouter({history: createWebHistory(),routes: [{ path: '/', component: Home },{ path: '/about', component: About }]
});
export default router;二:route(路由)
1、定义:route 是一个具体的路由记录,它表示一个特定的路径(URL)以及与之关联的组件、配置等信息。每个 route 都是一个对象,包含了路径、组件、名称、元信息等属性。
2、职责:表示一个路由路径:route 定义了一个具体的 URL 路径。关联组件:route 指定了当用户访问该路径时,应该渲染哪个组件。携带参数:route 可以携带参数(如动态路由参数、查询参数等)。元信息:route 可以包含一些元信息(如标题、权限等),用于在导航时进行额外的处理。
const routes = [{path: '/user/:id',component: User,name: 'User',meta: { requiresAuth: true } // 元信息}
];
49.5、vue中静态路由和动态路由的区别?
一:静态路由静态路由是预定义的路由,它们在应用初始化时就已经确定,并且不会根据用户的交互或其他条件而改变。静态路由非常适合那些路径固定、不依赖于用户输入或应用状态的页面
// router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home.vue';
import About from '@/components/About.vue';Vue.use(Router);export default new Router({routes: [{path: '/',name: 'Home',component: Home},{path: '/about',name: 'About',component: About}]
});二:动态路由动态路由允许路径中包含动态片段,这些片段通常用于捕获 URL 中的变量部分。动态路由非常适合需要根据用户输入或应用状态来加载不同内容的页面,例如用户详情页、文章详情页等。
// router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import User from '@/components/User.vue';Vue.use(Router);export default new Router({routes: [{path: '/user/:id', // 动态路由参数 :idname: 'User',component: User}]
});
50、路由的模式有哪两种,有啥区别,优缺点是啥?
在前端开发中,常见的路由模式主要有两种:HashRouter 和 BrowserRouter。它们的主要区别在于URL的表现形式以及对服务器配置的要求。以下是它们的定义、区别、优缺点:
1. HashRouter(哈希模式)定义:通过在URL的哈希部分(#)进行路由,不会与服务器进行实际通信。优点:兼容性好,可以在不同的服务器上正常工作。不需要服务器配置,适合静态页面或简单的单页应用(SPA)。缺点:URL中包含哈希(#),不太美观。可能影响搜索引擎优化(SEO),因为搜索引擎可能不会解析哈希部分。不支持 HTML5的history.pushState和history.replaceState 方法。
2. BrowserRouter(历史模式)定义:使用 HTML5 的 history API 来处理路由,URL 更接近传统的路径形式。优点:URL 更美观,没有哈希(#),用户体验更好。支持 HTML5 的 history.pushState 和 history.replaceState方法,可以实现更复杂的路由逻辑。缺点:需要服务器配置支持,服务器需要能够处理所有路径并返回同一个HTML文件。如果服务器配置不当,可能会导致页面刷新时找不到路由。
3. 选择哪种模式?选择 HashRouter:如果你的应用不需要复杂的服务器配置,或者你希望快速部署一个简单的单页应用。如果你担心服务器配置的复杂性,或者你的服务器不支持HTML5的history API。选择 BrowserRouter:如果你希望提供更美观的URL,并且能够更好地支持搜索引擎优化(SEO)。如果你的服务器可以配置,或者你已经在使用支持 HTML5 的 history API 的服务器。
总结
HashRouter:适合简单应用,不需要服务器配置,但URL不够美观。
BrowserRouter:适合需要更美观URL的应用,但需要服务器配置支持。
51、判断数据类型的方式有哪些?
1. typeof 运算符
typeof 是一个运算符,用于返回变量的数据类型。它返回一个字符串,表示变量的类型。
语法:typeof value;
"string":字符串
"number":数字
"boolean":布尔值
"object":对象、数组、null
"function":函数
"undefined":未定义
"symbol":ES6 中的 Symbol 类型
"bigint":ES2020 中的 BigInt 类型2. instanceof 运算符
instanceof 用于检测一个对象是否是某个构造函数的实例。它检查对象的原型链中是否包含构造函数的 prototype 属性。
语法:object instanceof Constructor;
示例:
class MyClass {}
const obj = new MyClass();
console.log(obj instanceof MyClass); // true
console.log(obj instanceof Object); // true(因为 MyClass 的原型链上包含 Object.prototype)console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(null instanceof Object); // false3. Array.isArray()
Array.isArray() 是一个内置方法,用于检测一个值是否是数组。它返回一个布尔值。
语法: Array.isArray(value);
示例:
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
console.log(Array.isArray("hello")); // false
console.log(Array.isArray(null)); // false4. Object.prototype.toString.call()
Object.prototype.toString.call() 是一种更通用的方法,用于检测数据类型。它返回一个表示类型的字符串,格式为 [object Type]。
语法:Object.prototype.toString.call(value);
示例:
console.log(Object.prototype.toString.call("hello")); // "[object String]"
console.log(Object.prototype.toString.call(123)); // "[object Number]"
console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(function() {})); // "[object Function]"
console.log(Object.prototype.toString.call(Symbol('sym'))); // "[object Symbol]"
console.log(Object.prototype.toString.call(BigInt(123))); // "[object BigInt]"5. constructor 属性
每个对象都有一个 constructor 属性,指向创建它的构造函数。通过 constructor 属性,可以判断对象的类型。
示例:
const obj = {};
console.log(obj.constructor === Object); // trueconst arr = [];
console.log(arr.constructor === Array); // trueconst str = "hello";
console.log(str.constructor === String); // trueconst num = 123;
console.log(num.constructor === Number); // trueconst fn = function() {};
console.log(fn.constructor === Function); // true6. ArrayBuffer、TypedArray 和其他特殊类型
对于一些特殊类型(如 ArrayBuffer、TypedArray 等),可以使用 instanceof 或Object.prototype.toString.call() 来检测。7. 判断 null 和 undefined
由于 typeof null 返回 "object",因此需要特别处理 null 和 undefined。8. 判断对象是否为空
可以使用 Object.keys() 或 JSON.stringify() 来判断对象是否为空。总结
typeof:简单快捷,但对 null 和数组的判断不够准确。
instanceof:用于检测对象是否是某个构造函数的实例。
Array.isArray():专门用于检测数组。
Object.prototype.toString.call():最通用的方法,可以准确判断所有类型。
constructor:通过构造函数判断类型,但可能被修改。
特殊类型:使用 instanceof 或 Object.prototype.toString.call() 检测。
52、数组去重方法?
1、利用Array.from(new Set)去重
2、利用includes去重
3、利用map去重
4、利用indexOf去重
5、利用单层for循环去重
6、利用双层for循环去重
7、利用递归去重
8、利用Array.filter和map对象数组去重 (性能较高)
9、利用Array.filter和Array.includes 对象数组去重1、利用Array.from(new Set)去重
// Set是es6新增的数据结构,似于数组,但它的一大特性就是所有元素都是唯一的,没有重复的值,
// Array.from()就是将一个类数组对象或者可遍历对象转换成一个真正的数组,也是ES6的新增方法
let list = ['牛牛牛', 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 1, 2, 3, 4, 5, 6, 7, 8, 8,8,'牛牛牛',]
let newList = Array.from(new Set(list))
console.log(newList);2、利用includes去重
利用Array.includes 去重
//includes() 方法用来判断一个数组是否包含一个指定的值,如果是返回true,否则false。
let list = ['牛牛牛', 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 1, 2, 3, 4, 5, 6, 7, 8, 8,8,'牛牛牛',]
let newList = []
list.forEach((item) => {// 空数组newList 不包含item为false ,取反为true 执行数组添加操作// 如果数组包含了 item为true 取反为false 不执行数组添加操作if (!newList.includes(item)) {newList.push(item)}})console.log(newList);3、利用map去重
//map数据结构是es6中新出的语法,其本质也是键值对,只是其键不局限于普通对象的字符串
let list = ['牛牛牛', 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 1, 2, 3, 4, 5, 6, 7, 8, 8,8,'牛牛牛',]
let newList = [];
let map = new Map()
list.forEach((item) => {// 如果map.has指定的item不存在,那么就设置key和value 这个item就是当前map里面不存在的key,把这个item添加到新数组// 如果下次出现重复的item,那么map.has(item等于true取反 !map.has(item) 不执行if (!map.has(item)) {map.set(item,true)newList.push(item)}})console.log(newList);4、利用indexOf去重
//indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。如果没有找到匹配的字符串则返回 -1
let list = ['牛牛牛', 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 1, 2, 3, 4, 5, 6, 7, 8, 8,8,'牛牛牛',]
let newList = [];
list.forEach((item) => {// 空数组newList4第一次循环没有找到匹配的item 返回-1 执行数组添加操作// 如果数组在第n次循环中找到了newList4数组中item 例如:item等于6 而在newList4数组中已经有9 所以indexOf就不等于-1 不执行数组添加操作if (newList.indexOf(item) === -1) {newList.push(item)}})console.log(newList);5、利用单层for循环去重
// Array.splice() 方法用于添加或删除数组中的元素。会改变原数组
let list = ['牛牛牛', 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 1, 2, 3, 4, 5, 6, 7, 8, 8,8,'牛牛牛',]
let newList = [];
for (let i = 0; i < list.sort().length; i++) {if (list[i] == list[i + 1]) {list.splice(i, 1)i--}}console.log(list);6、利用双层for循环去重
// Array.splice() 方法用于添加或删除数组中的元素。会改变原数组
let list = ['牛牛牛', 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 1, 2, 3, 4, 5, 6, 7, 8, 98,8,8,'牛牛牛',]for (let i = 0; i < list.sort().length; i++) {for (let j = i + 1; j < list.sort().length; j++) {if (list[i] == list[j]) {list.splice(i, 1)j--}}}console.log(list);7、利用递归去重
let list = ['牛牛牛', 1, 1, 2, 2, 3, 1.06,0.2,3, 4, 4, 0.2,5, 5, 6, 6, 7, 1, 2, 3, 4, 5, 6, 7, 8, 98,8,8,'牛牛牛',]
function callBack(index) {// 数组的长度不能等于0if (index >= 1) {// 第一次如果数组最后一个索引和最后第二个索引相等if (list[index] === list[index - 1]) {// 那么就删除这个索引的元素list.splice(index, 1)}// 继续调用这个函数 第一次传入的参数是数组末尾第一个 第二次传入的参数是数组末尾第二个 层层递减callBack(index - 1)}}//传入排序好的数组的最大索引indexcallBack(list.sort().length - 1)console.log(list);8、利用Array.filter和map对象数组去重 (性能较高)
let map = new Map()
let list = [{name: '牛牛牛1',id: 1},{name: '牛牛牛1',id: 2},{name: '牛牛牛2',id: 3},{name: '牛牛牛3',id: 3},{name: '牛牛ya牛3',id: 4},{name: '牛ya牛ya牛3',id: 4}]// 对象数组去重function fn(list, key) {return list.filter((item) => !map.has(item[key].toString()) && map.set(item[key].toString()))}console.log(fn(list, 'id'));9、利用Array.filter和Array.includes 对象数组去重
let list = [{name: '牛牛牛1',id: 1},{name: '牛牛牛1',id: 2},{name: '牛牛牛2',id: 3},{name: '牛牛牛3',id: 3},{name: '牛牛ya牛3',id: 4}]function fn(arr) {let list = [];return arr.filter((item) => !list.includes(item.name) && list.push(item.name))}console.log( fn(list));
53、常见的HTTP响应状态码?
通常会出现五种状态码:
100 ~ 199
200 ~ 299
300 ~ 399
400 ~ 499
500 ~ 5991、100 ~ 199一般我们看不到,因为表示请求继续100: 继续请求,前面的一部分内容服务端已经接受到了,正在等待后续内容101: 请求者已经准备切换协议,服务器页表示同意
2、200 ~ 2992 开头的都是表示成功,本次请求成功了,只不过不一样的状态码有不一样的含义(语义化)200: 标准请求成功(一般表示服务端提供的是网页)**201: 创建成功(一般是注册的时候,表示新用户信息已经添加到数据库)203: 表示服务器已经成功处理了请求,但是返回的信息可能来自另一源204: 服务端已经成功处理了请求,但是没有任何数据返回3、300 ~ 3993 开头也是成功的一种,但是一般表示重定向301: 永久重定向****302: 临时重定向304: 使用的是缓存的数据*******(get 请求默认都会被缓存)305: 使用代理
4、400 ~ 4994 开头表示客户端出现错误了400: 请求的语法服务端不认识401: 未授权(你要登录的网站需要授权登录)403: 服务器拒绝了你的请求****404: 服务器找不到你请求的 URL*****407: 你的代理没有授权408: 请求超时410: 你请求的数据已经被服务端永久删除
5、500 ~ 5995 开头的表示服务端出现了错误500: 服务器内部错误 ****503: 服务器当前不可用(过载或者维护)505: 请求的协议服务器不支持
54、关于Promise
一:promise的使用用来封装异步操作的,专门用来解决异步 回调地狱 的问题,先创建一个promise对象, 创建一个函数参数,这个函数参数又有两个参数resolve, reject, 函数参数中写异步操作,操作成功调用resolve,操作失败调用reject, 然后promise对象就可以知道异步操作的状态,p.then 成功的时执行,p.catch失败的时候执行二:PROMISE(许诺,承诺)(es6)
promise的语法:
let p = new Promise(function (resolve, reject) {//异步操作 异步操作成功调用reslove(res) 异步操作失败的调用 reject()// resolve 表示成功的回调// reject 表示失败的回调
});p.then(function (res) {// 如果 p内部调用了resolve(结果) ,这个函数就会执行,resolve的结果会传递给 这里的形参res
})
p.catch(function (err) {// 如果 p内部调用了reject(结果) ,这个函数就会执行//reject的结果会传递给 这里的形参err
})
p.finally(function () {//不管是调用了成功还是失败,finally都会执行,最终执行的函数--一般用的少
});promise 是状态机 -- 记录了内部的异步操作的状态 1-promise 创建以后就处于 进行中 pending(执行中状态)2-一旦调用 resolve 就变成了 fulfilled 完成状态(成功状态)3-一旦调用 reject 就变成了 rejected 失败状态(失败状态)三:Promise的静态方法-race-all
1、Promise.all()
const p = Promise.all([p1, p2, p3]);
p.then(function(){})
.catch(function(){})上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,
p的状态由p1、p2、p3决定,分成两种情况。(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。2、Promise.race()
const p = Promise.race([p1, p2, p3]);
上面代码中,只要`p1`、`p2`、`p3`之中有一个实例率先改变状态,`p`的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给`p`的回调函数3、Promise.resolve()
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))4、Promise.reject()
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {// 出错了console.log(s)
});
55、Promise和async-await的区别,有何有缺点
Promise和async/await都是JavaScript中用于处理异步操作的重要工具,它们之间存在一些显著的区别,并且各自具有优缺点。一、区别
语法结构:
Promise使用.then()和.catch()方法来处理异步操作的结果和异常。
async/await则使用async关键字定义异步函数,使用await关键字等待异步操作的结果,并且可以使用try...catch语句处理异常。
代码可读性:
Promise的链式调用可能会导致代码结构复杂,多层嵌套使得代码难以阅读和维护。
async/await则让异步代码看起来更像同步代码,减少了回调函数的嵌套,提高了代码的可读性和可维护性。
错误处理:
Promise的错误处理依赖于.catch()方法,如果错误发生在链式调用的中间环节,可能需要多个.catch()来捕获。
async/await则可以使用try...catch语句直接捕获异步操作中的错误,使得错误处理更加直观和方便。
并发处理:
Promise可以并行执行多个异步操作,使用Promise.all()等方法可以等待所有异步操作完成。
async/await在处理多个异步操作时,默认是串行执行的,如果需要并行处理,则需要结合Promise使用,如Promise.all([asyncOperation1(), asyncOperation2()])。二、优缺点
Promise:优点:
提供了集中处理错误的机制,通过.catch()方法捕获整个链式调用中的任何错误。
可以通过Promise.all()等方法并发执行多个异步操作,提高代码性能。
链式调用语法使得代码在某些情况下可读性更高(尽管也可能导致代码复杂)。
缺点:
链式调用可能导致代码结构复杂,多层嵌套难以维护。
错误处理可能不够直观,需要多个.catch()来捕获错误。
一旦Promise被创建,它将立即执行,无法中途取消。
async/await:优点:
让异步代码看起来像同步代码,减少了回调函数的嵌套,提高了代码的可读性和可维护性。
使用try...catch语句处理异步操作中的错误,使得错误处理更加直观和方便。
提供了更好的调试体验,可以像调试同步代码一样使用断点和堆栈跟踪。
缺点:
在处理多个异步操作时,默认是串行执行的,不如Promise方便处理并发请求。
相对于Promise来说更加新,可能不支持所有版本的JavaScript引擎。
在某些情况下,处理异步操作的取消可能比较困难,需要额外的逻辑来实现。
56、数组中有哪些方法,哪些方法是不改变原数组的
一:改变原数组的方法
push():在数组末尾添加一个或多个元素,并返回新的数组长度。
pop():从数组末尾删除一个元素,并返回该元素的值。
shift():从数组开头删除一个元素,并返回该元素的值。
unshift():在数组开头添加一个或多个元素,并返回新的数组长度。
splice():通过删除或替换现有元素或者添加新元素来修改数组,并返回被删除的元素组成的数组。
sort():对数组的元素进行排序,并返回数组。默认按照字符串的Unicode码点排序,但可以通过提供比较函数来定制排序顺序。
reverse():颠倒数组中元素的顺序,并返回数组。二:不改变原数组的方法
concat():用于合并两个或多个数组,并返回一个新数组。原数组不会被改变。
slice():返回一个新的数组对象,这个新数组包含从开始到结束(不包括结束)选择的一部分原数组的浅拷贝。原数组不会被改变。
map():创建一个新数组,其结果是原数组中的每个元素都调用一个提供的函数后的返回值。原数组不会被改变。
filter():创建一个新数组,其包含通过所提供函数实现的测试的所有元素。原数组不会被改变。
reduce() 和 reduceRight():对数组中的每个元素执行一个由提供的reducer函数(升序执行或降序执行),将其结果汇总为单个返回值。原数组不会被改变。
forEach():对数组的每个元素执行一次提供的函数。虽然这个方法会对数组的每个元素进行操作,但它并不改变原数组的内容。
some() 和 every():测试数组的某些或每个元素是否通过由提供的函数实现的测试。这些方法不会改变原数组。
find() 和 findIndex():返回数组中满足提供的测试函数的第一个元素的值(或它的索引),否则返回undefined(或-1)。原数组不会被改变。
includes():判断一个数组是否包含一个特定的值,并返回true或false。原数组不会被改变。
indexOf() 和 lastIndexOf():返回在数组中可以找到给定元素的第一个(或最后一个)索引,如果不存在,则返回-1。原数组不会被改变。
57、前端如何减少首屏加载时间?
1. 优化资源加载压缩与合并:使用工具(如Webpack、Gulp)压缩CSS、JS文件,减少文件大小。合并多个CSS/JS文件,减少HTTP请求数量。图片优化:使用现代图片格式(如WebP),相比JPEG/PNG体积更小。延迟加载非关键图片(如懒加载),仅在用户滚动到视口时加载。字体优化:使用font-display: swap避免字体加载阻塞渲染。仅加载必要的字符集(如Subset)。
2. 代码拆分与按需加载Tree Shaking:移除未使用的代码,减少打包体积。动态导入:使用import()按需加载模块,避免一次性加载所有代码。路由级代码拆分:将不同页面的代码打包为独立文件,按需加载。
3. 服务器端优化启用Gzip/Brotli压缩:减少传输数据量。CDN加速:将静态资源托管到CDN,加速全球用户访问。缓存策略:设置HTTP缓存头(如Cache-Control)。使用Service Worker缓存关键资源。
4. 预加载与预渲染预加载(Preload):通过<link rel="preload">提前加载关键资源。预渲染(Prerender):使用工具(如Prerender.io)生成静态HTML,提升首屏速度。
5. 减少阻塞渲染CSS内联关键路径:将首屏所需的CSS直接嵌入HTML,避免阻塞渲染。异步加载JS:使用async或defer属性加载非关键JS。
58、如何解决白屏问题?
1. 骨架屏(Skeleton Screen)在资源加载前显示占位符(如灰色方块),提升用户体验。实现方式:使用CSS绘制占位元素。结合React/Vue组件动态渲染骨架屏。
2. 渐进式加载优先渲染首屏内容:将首屏HTML直接嵌入,避免依赖JS渲染。分步加载:先显示核心内容,再逐步加载其他部分。
3. 错误处理与降级加载失败时显示占位内容:使用try-catch捕获JS错误。提供友好的错误提示页面。服务端渲染(SSR):在服务器端生成HTML,减少客户端渲染时间。
4. 性能监控使用工具(如Lighthouse、WebPageTest)分析白屏原因。监控关键指标(如FCP、LCP),定位性能瓶颈。
59、如何解决1px问题?
1. 背景图方案
使用border-image或background-image绘制1px边框。
示例:
css
.border {border-image: url('border.png') 2 stretch;
}
2. 伪元素方案使用:before或:after伪元素模拟边框。
示例:
css
.border {position: relative;
}
.border::after {content: '';position: absolute;bottom: 0;left: 0;width: 100%;height: 1px;background-color: #000;transform: scaleY(0.5); /* 适配Retina屏 */transform-origin: bottom;
}
3. 媒体查询适配针对高分辨率屏幕(如2x、3x)调整边框宽度。
示例:
css
.border {border: 0.5px solid #000; /* 部分浏览器支持 */
}
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {.border {border-width: 0.25px; /* 进一步适配 */}
}
4. 使用viewport单位使用vw或vh单位动态计算边框宽度。
示例:
css
.border {border: 0.05vw solid #000;
}
5. 第三方库使用postcss-px-to-viewport插件,自动将px转换为vw单位。
总结
减少首屏加载时间:通过资源优化、代码拆分、服务器端加速等手段。
解决白屏问题:使用骨架屏、渐进式加载、SSR等技术。
解决1px问题:结合背景图、伪元素、媒体查询等方法适配高分辨率屏幕。
60、vue3项目可以用哪些懒加载技术提高性能?
在 Vue 3 项目中,可以通过多种懒加载技术来提高性能,减少初始加载时间和资源消耗.
(组件懒加载、路由懒加载、图片懒加载、数据懒加载、第三方库懒加载)
1. 组件懒加载
Vue 3 提供了动态组件加载的能力,可以通过 defineAsyncComponent 或 import() 语法实现组件的按需加载。
import { defineAsyncComponent } from 'vue';
// 使用 defineAsyncComponent
const AsyncComponent = defineAsyncComponent(() =>import('./components/MyComponent.vue')
);
// 或直接使用 import()
const AsyncComponent = () => import('./components/MyComponent.vue');2. 路由懒加载
结合 Vue Router,可以对路由组件进行懒加载,使得每个路由对应的组件只有在访问时才被加载
import { createRouter, createWebHistory } from 'vue-router';
const routes = [{path: '/home',component: () => import('./views/HomeView.vue'), // 路由懒加载},{path: '/about',component: () => import('./views/AboutView.vue'),},
];
const router = createRouter({history: createWebHistory(),routes,
});
export default router;
3. 图片懒加载
对于页面中的图片资源,可以使用懒加载技术,只有当图片进入可视区域时才加载图片,减少初始加载的资源消耗。
(1)使用 loading="lazy" 属性(原生支持):<img src="example.jpg" loading="lazy" alt="Example">
(2)使用第三方库(如 vue-lazyload)(在main.js配置)npm install vue-lazyloadimport { createApp } from 'vue';import App from './App.vue';import VueLazyload from 'vue-lazyload';const app = createApp(App);app.use(VueLazyload, {preLoad: 1.3,error: 'error.png',loading: 'loading.gif',attempt: 1,});app.mount('#app');<img v-lazy="imageUrl" alt="Lazy Loaded Image">4. 数据懒加载(分页加载/无限滚动) 对于长列表或大量数据,可以通过懒加载技术分批加载数据,减少初始渲染的数据量。 分页加载:在用户切换分页时,动态请求数据并渲染。无限滚动:结合 IntersectionObserver 或滚动事件,当用户滚动到页面底部时,动态加载更多数据。import { ref, onMounted, onBeforeUnmount } from 'vue';export default {setup() {const items = ref([]);let page = 1;let loading = false;const loadMore = () => {if (loading) return;loading = true;// 模拟异步请求setTimeout(() => {const newItems = Array.from({ length: 10 }, (_, i) => `Item ${(page - 1) * 10 + i + 1}`);items.value.push(...newItems);page++;loading = false;}, 1000);};const handleScroll = () => {const { scrollTop, clientHeight, scrollHeight } = document.documentElement;if (scrollTop + clientHeight >= scrollHeight - 10) {loadMore();}};onMounted(() => {loadMore(); // 初始加载window.addEventListener('scroll', handleScroll);});onBeforeUnmount(() => {window.removeEventListener('scroll', handleScroll);});return { items };},}; 5. 第三方库的懒加载功能一些第三方库(如 vue-virtual-scroller)提供了内置的懒加载功能,适用于大规模数据渲染npm install vue-virtual-scroller<template><virtual-scroller :items="items" :item-height="50"><template #default="{ item }"><div>{{ item }}</div></template></virtual-scroller></template><script>
import { ref } from 'vue';
import { VirtualScroller } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';export default {components: { VirtualScroller },setup() {const items = ref(Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`));return { items };},
};
</script>//name="fade" 指定了过渡类名前缀。
//Vue 会自动添加以下类名:
//fade-enter-from:进入前的状态。
//fade-enter-active:进入时的过渡效果。
//fade-leave-from:离开前的状态。
//fade-leave-active:离开时的过渡效果。
61、组件缓存keep-alive相关,如何使用
keep-alive 是 Vue.js 提供的一个内置组件,用于缓存动态组件的状态,避免组件在切换时被销毁和重新创建。这对于需要保留组件状态(如表单数据、滚动位置等)的场景非常有用。以下是关于 keep-alive 的详细使用方法和示例:一:基本用法
keep-alive 通常包裹在动态组件或路由视图(<router-view>)的外层,用于缓存这些组件。
<template><div><button @click="currentView = 'ViewA'">显示 ViewA</button><button @click="currentView = 'ViewB'">显示 ViewB</button><!-- 使用 keep-alive 缓存动态组件 --><keep-alive><component :is="currentView"></component></keep-alive></div>
</template><script>
import ViewA from './ViewA.vue';
import ViewB from './ViewB.vue';export default {data() {return {currentView: 'ViewA', // 默认显示的组件};},components: {ViewA,ViewB,},
};
</script>二:与路由结合使用
在 Vue Router 中,keep-alive 通常与 <router-view> 结合使用,以缓存路由组件。
<template><div id="app"><router-link to="/home">首页</router-link><router-link to="/about">关于</router-link><!-- 使用 keep-alive 缓存路由组件 --><keep-alive><router-view></router-view></keep-alive></div>
</template><script>
export default {// 通常不需要额外配置
};
</script>三:条件缓存
keep-alive 提供了 include 和 exclude 属性,用于条件性地缓存组件。
include:字符串或正则表达式,只有名称匹配的组件会被缓存。
exclude:字符串或正则表达式,名称匹配的组件不会被缓存。
<template><div><keep-alive include="ViewA"><component :is="currentView"></component></keep-alive></div>
</template><script>
import ViewA from './ViewA.vue';
import ViewB from './ViewB.vue';export default {data() {return {currentView: 'ViewA', // 默认显示的组件};},components: {ViewA,ViewB,},
};
</script>四:缓存生命周期钩子
当组件被 keep-alive 缓存时,会触发特定的生命周期钩子:
activated:组件被激活时调用。
deactivated:组件被停用时调用。当 ViewA 被切换到前台时,会触发 activated 钩子。
当 ViewA 被切换到后台时,会触发 deactivated 钩子。
<template><div><p>这是 ViewA</p></div>
</template><script>
export default {activated() {console.log('ViewA 被激活');},deactivated() {console.log('ViewA 被停用');},
};
</script>五:动态组件缓存示例
结合 v-if 和 keep-alive,可以实现更复杂的缓存逻辑。
<template><div><button @click="showComponent = 'A'">显示组件 A</button><button @click="showComponent = 'B'">显示组件 B</button><keep-alive><component v-if="showComponent === 'A'" :is="componentA"></component><component v-if="showComponent === 'B'" :is="componentB"></component></keep-alive></div>
</template><script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';export default {data() {return {showComponent: 'A',componentA: 'ComponentA',componentB: 'ComponentB',};},components: {ComponentA,ComponentB,},
};
</script>六:注意事项性能问题:缓存的组件会占用内存,确保只缓存必要的组件。对于大组件或频繁切换的组件,谨慎使用 keep-alive。组件销毁:被 keep-alive 缓存的组件不会触发 beforeDestroy 和 destroyed 钩子。如果需要清理资源,可以在 deactivated 钩子中处理。与 Vuex 结合:如果组件状态依赖 Vuex,可以考虑将状态存储在 Vuex 中,而不是依赖 keep-alive。
62、vue2和vue3的生命周期都分别有啥?
一、Vue 2 生命周期钩子
1. 创建阶段beforeCreate:组件实例初始化之后调用,此时数据观测(data)和事件机制尚未设置。created:组件实例创建完成后调用,此时数据观测已完成,但 DOM 尚未挂载,$el 不可用。
2. 挂载阶段beforeMount:在挂载开始之前调用,此时模板已编译完成,但尚未渲染到 DOM。mounted:组件挂载到 DOM 后调用,此时可以通过 $el 访问真实的 DOM 节点。
3. 更新阶段beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。updated:数据更新导致虚拟 DOM 重新渲染和打补丁之后调用。
4. 销毁阶段beforeDestroy:实例销毁之前调用,此时实例仍然完全可用。destroyed:实例销毁后调用,此时所有事件监听器和子实例均被销毁。
二、Vue 3 生命周期钩子1. 选项式 API(与 Vue 2 相同)Vue 3 的选项式 API 生命周期钩子与 Vue 2 完全一致,包括:beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroy(Vue 3 中已更名为 beforeUnmount)destroyed(Vue 3 中已更名为 unmounted)2. 组合式 API在组合式 API 中,生命周期钩子以函数形式导入自 vue 包,例如:onBeforeMountonMountedonBeforeUpdateonUpdatedonBeforeUnmountonUnmounted生命周期阶段 Vue 2 钩子 Vue 3 选项式 API 钩子 Vue 3 组合式 API 钩子创建前 beforeCreate beforeCreate 不适用(setup 执行前)创建后 created created 不适用(setup 执行后)挂载前 beforeMount beforeMount onBeforeMount挂载后 mounted mounted onMounted更新前 beforeUpdate beforeUpdate onBeforeUpdate更新后 updated updated onUpdated销毁前 beforeDestroy beforeUnmount onBeforeUnmount销毁后 destroyed unmounted onUnmounted
四、注意事项beforeCreate 和 created 在组合式 API 中的处理:在组合式 API 中,setup 函数在 beforeCreate 和 created 之间执行,因此无需显式使用这两个钩子。beforeDestroy 和 destroyed 的重命名:Vue 3 中,beforeDestroy 重命名为 beforeUnmount,destroyed 重命名为 unmounted,以更准确地描述生命周期阶段。推荐使用组合式 API:组合式 API 提供了更灵活的逻辑组织方式,尤其适合复杂组件或需要共享逻辑的场景。
63、vue2定义数据为什么要在data中?
在 Vue 2 中,组件的数据需要定义在 data 选项中,这是因为Vue的响应式系统依赖于对data属性的初始化来追踪数据的变化。
1. Vue2响应式系统的原理
Vue2的响应式系统基于 Object.defineProperty来劫持对象的属性访问和修改。当组件实例化时,Vue2会遍历data 对象中的所有属性,并使用 Object.defineProperty 将它们转换为响应式属性。(1)为什么需要初始化?Vue 需要在组件实例化时就知道 data 中有哪些属性,以便为这些属性设置 getter 和 setter。如果某个属性没有在 data 中定义,Vue 无法追踪它的变化,也就无法实现响应式更新。(2)动态添加属性的问题如果在组件创建后动态地向 data 对象添加新的属性,Vue 无法检测到这些新属性的变化,因为它们没有在初始化时被劫持。为了解决这个问题,Vue2 提供了 this.$set 方法(或 Vue.set 全局方法)来显式地将新属性添加为响应式属性。
2. data 必须是一个函数(在组件中)在 Vue 组件中,data 必须是一个返回对象的函数,而不是一个直接的对象。这是因为组件可能会被复用多次,而每个组件实例都需要有自己的独立数据。(1)为什么是函数?如果 data 是一个对象,那么所有组件实例将共享同一个数据对象,导致数据之间的冲突。通过将 data 定义为一个函数,并让每个组件实例调用这个函数来返回一个新的对象,可以确保每个实例都有独立的数据。export default {data() {return {message: 'Hello'};}};
3. 常见问题为什么不能直接在模板中使用未定义的属性?如果在模板中引用了一个未在 data 中定义的属性,Vue 会发出警告,因为该属性不是响应式的。如何动态添加响应式属性?使用 $set 方法:this.$set(this.someObject, 'newProperty', 'value');
4. Vue3 的改进在 Vue 3 中,响应式系统基于 Proxy 实现,不再依赖于 Object.defineProperty。因此,Vue 3 不再要求 data 必须是一个函数,也不再需要手动使用 $set 来添加响应式属性。不过,在 Vue 3 的组合式 API 中,通常使用 reactive 或 ref 来定义响应式数据,而不是 data 选项。
64、 v-if和v-show的区别,以及都经历了哪些生命周期在vue3中
在 Vue 3 中,v-if 和 v-show 都是用于控制元素显示或隐藏的指令,但它们的工作原理和生命周期触发机制有所不同。1. 工作原理的区别v-if:基于条件进行真实的 DOM 元素的创建或销毁。当条件为 false 时,元素不会存在于 DOM 中。当条件为 true 时,元素会被创建并插入 DOM。v-show:基于条件通过设置 CSS 的 display 属性来控制元素的显示或隐藏。元素始终存在于 DOM 中,只是通过 display: none 隐藏。
2. 生命周期触发的区别v-if:当条件从 false 变为 true 时:触发子组件的 beforeCreate、created、beforeMount、mounted 生命周期钩子。当条件从 true 变为 false 时:触发子组件的 beforeUnmount、unmounted 生命周期钩子。如果父组件的 v-if 条件发生变化,子组件的生命周期钩子也会被触发。v-show:元素始终存在于 DOM 中,因此不会触发子组件的创建或销毁相关的生命周期钩子。当条件变化时,仅触发元素的显示或隐藏,不会影响生命周期。
3. 性能和适用场景v-if:切换开销较高:因为涉及 DOM 的创建和销毁。初始渲染开销较低:因为只有在条件为 true 时才会渲染。适用场景:条件较少变化时使用,例如表单的某些部分仅在特定条件下显示。v-show:切换开销较低:因为仅涉及 CSS 的切换。初始渲染开销较高:因为元素始终存在于 DOM 中。适用场景:需要频繁切换显示状态时使用,例如模态框的显示和隐藏。
65、vue2中响应式原理,对哪些是监听不到的
在 Vue 2 中,响应式系统是基于 Object.defineProperty 实现的。Vue 2 的响应式系统可以监听对象属性的读取和设置操作,
1. 新增属性
Vue 2 无法检测到对象属性的动态添加。如果你在对象初始化后添加新的属性,Vue 不会使其成为响应式的。
new Vue({el: '#app',data() {return {obj: {existingProperty: 'I exist'}};},mounted() {// 动态添加新属性,Vue 无法检测到this.obj.newProperty = 'I am new';console.log(this.obj.newProperty); // 输出: I am new// 但这个新属性不会触发视图更新}
});// HTML
// <div id="app">
// <p>{{ obj.existingProperty }}</p>
// <p>{{ obj.newProperty }}</p> <!-- 不会更新 -->
// </div>解决方案:使用 Vue.set 方法。
Vue.set(this.obj, 'newProperty', 'I am new');
或 this.$set(this.obj, 'newProperty', 'I am new');
原理:Vue.set 会调用 Object.defineProperty 为新属性添加 getter 和 setter,并将其加入依赖收集系统2. 删除属性
Vue 2 无法检测到对象属性的删除。
new Vue({el: '#app',data() {return {obj: {property: 'I exist'}};},mounted() {// 删除属性,Vue无法检测到delete this.obj.property;console.log(this.obj.property); // 输出: undefined// 但这个变化不会触发视图更新}
});// HTML
// <div id="app">
// <p>{{ obj.property }}</p> <!-- 不会更新 -->
// </div>解决方案:使用 Vue.delete 方法(实际上是调用 delete 操作符,但确保在 Vue 的响应式上下文中使用)。
Vue.delete(this.obj, 'property');
或 this.$delete(this.obj, 'property');
3. 数组索引或长度的修改
Vue 2 对数组的响应式处理有一些特殊情况。直接通过索引修改数组或修改数组的长度不会被检测到。new Vue({el: '#app',data() {return {items: [1, 2, 3]};},methods: {modifyArray() {// 直接修改索引,Vue 无法检测到this.items[1] = 42;// 修改数组长度,Vue 无法检测到this.items.length = 2;}}
});// HTML
// <div id="app">
// <ul>
// <li v-for="(item, index) in items" :key="index">{{ item }}</li>
// </ul>
// <button @click="modifyArray">Modify Array</button>
// </div>解决方案:使用 Vue 提供的数组变异方法,如 splice。
this.items.splice(1, 1, 42); // 替换索引 1 的元素
this.items.splice(2); // 删除索引 2 及之后的元素
this.Array.splice(index, 1, 'newValue');4. 通过 Map 或 Set 修改数据
Vue 2 的响应式系统不直接支持 Map 和 Set,因此对这些数据结构的修改不会被检测到。
66、父组件和子组件的生命周期执行顺序
1. 创建阶段父组件的生命周期钩子会先于子组件执行。顺序如下:父组件的 beforeCreate父组件的 created父组件的 beforeMount子组件的生命周期开始执行:子组件的 beforeCreate子组件的 created子组件的 beforeMount子组件的 mounted父组件的 mounted2. 更新阶段当父组件或子组件的数据发生变化时,会触发更新阶段。更新阶段的生命周期顺序:父组件的 beforeUpdate子组件的 beforeUpdate子组件的 updated父组件的 updated
3. 销毁阶段当父组件或子组件被销毁时,生命周期顺序如下:父组件的 beforeDestroy(Vue 2)/beforeUnmount(Vue 3)子组件的 beforeDestroy(Vue 2)/beforeUnmount(Vue 3)子组件的 destroyed(Vue 2)/unmounted(Vue 3)父组件的 destroyed(Vue 2)/unmounted(Vue 3)
四、注意事项异步更新:Vue的更新是异步的,数据变化不会立即触发 DOM 更新。在 updated 钩子中,DOM 已经更新完成。避免内存泄漏:在 beforeDestroy/beforeUnmount 钩子中,清理定时器、事件监听器等。父子组件通信:父组件通过 props 向子组件传递数据,子组件通过 $emit 向父组件发送事件。Vue 3 差异:Vue 3 使用 beforeUnmount 和 unmounted 替代 Vue 2 的 beforeDestroy 和 destroyed。
67、Vuex 和 Pinia 的区别?
Vuex 是 Vue.js 官方推荐的状态管理库,适用于 Vue 2 和 Vue 3。而 Pinia 是 Vue 生态中新一代的状态管理库,专为 Vue 3 设计,并被认为是 Vuex 的下一代替代方案。特性 Vuex Pinia
核心设计 基于Mutation和Action 基于State、Getters 和 Actions
TypeScript 支持,但需要额外配置 原生支持TypeScript
模块化 使用modules 使用stores,结构更灵活
API 风格 较为复杂,需严格遵循Mutation提交 更简洁,更符合现代开发习惯
开发体验 相对繁琐,需要手动管理mutations和actions 简化状态管理逻辑,支持组合式 API
性能 性能良好 性能优化,体积更小1、创建Vuex Store
//store.js
import { createStore } from 'vuex';
const store = createStore({state() {return {count: 0,};},mutations: {increment(state) {state.count++;},decrement(state) {state.count--;},},actions: {asyncIncrement({ commit }) {setTimeout(() => {commit('increment');}, 1000);},},getters: {doubleCount(state) {return state.count * 2;},},
});export default store;2. Pinia 示例
// store.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {state: () => ({count: 0,}),getters: {doubleCount: (state) => state.count * 2,},actions: {increment() {this.count++;},decrement() {this.count--;},asyncIncrement() {setTimeout(() => {this.count++;}, 1000);},},
});
68、双向绑定和单项绑定的区别?
双向绑定和单向绑定的区别在前端开发中,数据绑定是视图(UI)和模型(数据)之间保持同步的机制。单向绑定和双向绑定是两种主要的数据绑定方式,它们的核心区别在于数据流动的方向和同步方式。1、单向绑定 v-bind数据从模型(数据源)单向流向视图(UI)。
特点:数据更新时,视图自动更新(模型 → 视图)。视图上的用户交互(如输入框值改变)不会直接更新模型。
使用场景:数据展示为主,用户不需要直接修改数据。例如:显示用户信息、只读表单
2、双向绑定 v-model数据在模型和视图之间双向流动。
特点:模型更新时,视图自动更新(模型 → 视图)。视图上的用户交互(如输入框值改变)会直接更新模型(视图 → 模型)。
使用场景:需要用户输入并实时更新数据。例如:表单输入、实时搜索框。特性 单向绑定 双向绑定数据流动 模型 → 视图 模型 ↔ 视图代码复杂性 简单,逻辑清晰 复杂,需处理视图和模型的同步逻辑性能 性能较高,视图更新仅由模型驱动 性能稍低,需监听视图变化并更新模型适用场景 数据展示、只读表单 表单输入、实时搜索、用户交互频繁场景调试难度 容易调试,数据流单一 调试较难,需跟踪视图和模型的双向变化 3、单向绑定
<div id="app"><p>{{ message }}</p><!-- 用户输入不会改变 message --><input type="text" :value="message">
</div><script>new Vue({el: '#app',data: {message: 'Hello, 单向绑定!'}});
</script>4、双向绑定
<div id="app"><p>{{ message }}</p><!-- 用户输入会改变 message --><input type="text" v-model="message">
</div><script>new Vue({el: '#app',data: {message: 'Hello, 双向绑定!'}});
</script>
69、git中克隆指定分支如何操作?
方法 1:克隆整个仓库后切换到指定分支
使用 git clone 命令克隆整个仓库:git clone https://github.com/user/repo.git
进入克隆的目录:cd repo
使用 git branch -r 查看远程分支列表:git branch -r
使用 git checkout 切换到目标分支:git checkout <分支名>方法 2:使用 --single-branch 参数克隆指定分支使用--single-branch和--branch参数:git clone --single-branch --branch <分支名> <仓库地址>
进入克隆的目录:cd repo
验证当前分支:git branch
70、es6中拼接字符串的方法有哪些?
1. 模板字符串(Template Literals)
const name = "Alice";
const age = 25;
const greeting = `Hello, my name is ${name} and I am ${age} years old.`;
console.log(greeting);
// 输出: Hello, my name is Alice and I am 25 years old.2. 加号运算符(+)
const firstName = "Bob";
const lastName = "Smith";
const fullName = firstName + " " + lastName;
console.log(fullName);
// 输出: Bob Smith3. 数组的 join() 方法
const parts = ["JavaScript", "is", "awesome"];
const sentence = parts.join(" ");
console.log(sentence);
// 输出: JavaScript is awesome4. 字符串的 concat() 方法
const str1 = "Hello";
const str2 = "World";
const result = str1.concat(" ", str2);
console.log(result);
// 输出: Hello World5. 重复字符串(repeat() 方法)
const word = "Ha";
const laugh = word.repeat(3);
console.log(laugh);
// 输出: HaHaHa
71、在前端判断用户设备类型有多种方法,以下是常见的几种?
1. User-Agent字符串分析
//获取User-Agent字符串
let userAgent = navigator.userAgent;
console.log(userAgent);//解析User-Agent字符串:
function getDeviceType() {let userAgent = navigator.userAgent;if (/iPhone|iPad|iPod/.test(userAgent)) {return 'iOS';} else if (/Android/.test(userAgent)) {return 'Android';} else if (/Windows/.test(userAgent)) {return 'Windows';} else if (/Macintosh/.test(userAgent)) {return 'Mac';} else {return 'Unknown';}
}
console.log(getDeviceType());2. CSS媒体查询
通过CSS媒体查询可以根据设备的特性(如屏幕宽度、高度、分辨率等)应用不同的样式规则,也可以在JavaScript中使用window.matchMedia进行动态媒体查询。
//(1)基本媒体查询:
/* 针对小屏幕设备(如手机) */
@media only screen and (max-width: 600px) {body {background-color: lightblue;}
}
/* 针对中等屏幕设备(如平板) */
@media only screen and (min-width: 601px) and (max-width: 1200px) {body {background-color: lightgreen;}
}
/* 针对大屏幕设备(如桌面) */
@media only screen and (min-width: 1201px) {body {background-color: lightcoral;}
}
//(2)动态媒体查询
let mediaQuery = window.matchMedia('(max-width: 600px)');
function handleDeviceChange(e) {if (e.matches) {console.log('This is a mobile device');} else {console.log('This is not a mobile device');}
}
mediaQuery.addListener(handleDeviceChange);
handleDeviceChange(mediaQuery);3. JavaScript检测
//检测屏幕尺寸
function getDeviceTypeByScreenSize() {let width = window.innerWidth;if (width <= 600) {return 'Mobile';} else if (width > 600 && width <= 1200) {return 'Tablet';} else {return 'Desktop';}
}
console.log(getDeviceTypeByScreenSize());
//检测触摸事件支持
function isTouchDevice() {return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}
console.log(isTouchDevice() ? 'Touch device' : 'Non-touch device');4. 使用第三方库
一些第三方库提供了更复杂的设备检测功能,例如detect.js、react-device-detect等。
//detect.js:
import detect from 'detect.js';
function isMobile() {return detect.device.type === 'phone' || detect.device.type === 'tablet';
}
console.log(isMobile() ? '移动端' : 'PC端');//react-device-detect:
import { isMobile } from 'react-device-detect';
if (isMobile) {console.log("当前是移动端");
} else {console.log("当前是PC端");
}5. 使用框架提供的工具
//vue-device-detect:
import { device } from 'vue-device-detect';
export default {mounted() {console.log(device.isMobile ? '移动端' : 'PC端');}
}//react-device-detect:
import { isMobile, isTablet, isDesktop } from 'react-device-detect';
function DeviceType() {if (isMobile) {return "Mobile";} else if (isTablet) {return "Tablet";} else if (isDesktop) {return "Desktop";} else {return "Unknown";}
}
72、在浏览器中执行100万个任务而不导致卡顿,可以采用以下几种方法?
1. 任务分批执行
//将100万个任务分成多个小批次,每次只执行少量任务(例如1000个)。这样可以避免一次性执行所有任务导致浏览器线程阻塞。
//1、使用 setTimeout
const tasks = Array.from({ length: 1000000 }, (_, i) => i); // 模拟任务数组function processTasksInChunks() {if (tasks.length === 0) return;const chunk = tasks.splice(0, 1000); // 每次处理1000个任务chunk.forEach(task => {// 执行任务console.log(task);});setTimeout(processTasksInChunks, 0); // 在下一个事件循环中继续处理
}processTasksInChunks();//2、使用 requestAnimationFrame
const tasks = Array.from({ length: 1000000 }, (_, i) => i); // 模拟任务数组
function processTasksWithRAF() {if (tasks.length === 0) return;const chunk = tasks.splice(0, 1000); // 每次处理1000个任务chunk.forEach(task => {// 执行任务console.log(task);});requestAnimationFrame(processTasksWithRAF); // 在下一帧继续处理
}
processTasksWithRAF();2. 使用 Web Workers
//将任务移到单独的线程中执行,避免阻塞主线程
//1、主线程代码
const worker = new Worker('worker.js');
worker.postMessage({ tasks: Array.from({ length: 1000000 }, (_, i) => i) });worker.onmessage = function(event) {console.log('Task completed:', event.data);
};
//worker.js
onmessage = function(event) {const tasks = event.data.tasks;tasks.forEach(task => {// 执行任务console.log(task);});postMessage('All tasks completed');
};3. 使用 requestIdleCallback
//requestIdleCallback 是浏览器的API,可以在主线程空闲时执行非紧急任务
const tasks = Array.from({ length: 1000000 }, (_, i) => i);function processTasksWithIdle(deadline) {while (deadline.timeRemaining() > 0 && tasks.length > 0) {const task = tasks.shift();console.log(task);}if (tasks.length > 0) {requestIdleCallback(processTasksWithIdle);}
}requestIdleCallback(processTasksWithIdle);4. 使用生成器和时间分片
//通过生成器将任务分片执行,避免长时间占用主线程
function* longTaskGenerator() {let i = 0;while (i < 1000000) {yield i++;}
}function runLongTask() {const generator = longTaskGenerator();function processChunk() {for (let j = 0; j < 1000; j++) {const result = generator.next();if (result.done) return;console.log(result.value);}setTimeout(processChunk, 0);}processChunk();
}
runLongTask();
5. 虚拟DOM
//如果任务涉及大量DOM操作,可以使用虚拟DOM技术,只渲染当前可见区域的任务节点
import { Virtuoso } from 'react-virtuoso';const tasks = Array.from({ length: 1000000 }, (_, i) => `Task ${i}`);<VirtuosototalCount={tasks.length}itemContent={(index) => <div>{tasks[index]}</div>}
/>;
73、使用v-for时为什么要添加key?key可以取值为随机数吗?
//为什么需要 key?(1)高效的 DOM 更新:Vue 使用虚拟 DOM 来高效地更新真实 DOM。当列表数据发生变化时,Vue 会尝试通过 key 来识别哪些元素需要被复用、重新排序或销毁/重建。如果没有 key,Vue 默认会使用索引来比较新旧节点,这可能导致不必要的 DOM 操作,尤其是在列表顺序发生变化时。(2)正确跟踪组件状态:如果列表项是组件,key 用于确保 Vue 能够正确地跟踪每个组件实例的状态。例如,输入框的值、复选框的选中状态等。(3)避免潜在的 Bug:没有 key 时,Vue 可能会复用错误的 DOM 节点,导致 UI 状态不一致或意外行为。
//为什么 key 不建议取值为随机数?
虽然技术上可以将 key 设置为随机数,但这通常不是一个好的选择,原因如下:(1)key 必须是稳定的:key 的作用是唯一标识列表中的每一项,并且在数据更新时保持稳定。如果 key 是随机数,每次渲染时都会生成新的值,Vue 会认为所有节点都是新的,从而导致整个列表被重新渲染,而不是仅更新变化的部分。
这会完全抵消 key 的性能优化效果,导致性能下降。(2)可能导致状态丢失:如果列表项是组件,并且组件内部有状态(例如表单输入值),随机数 key 会导致 Vue 无法正确复用组件实例。每次渲染时,Vue 会认为这是一个全新的组件,从而丢失组件的状态。(3)不符合设计意图:key 的设计初衷是用于唯一标识数据项,而不是随机生成。通常,key 应该与数据项的唯一标识符(如 ID)相关联。
74、文本字体如何 做到动态的缩放?
1. 使用CSS的相对单位(vw/vh/rem)
核心思想:通过相对单位(如vw、rem)定义字体大小,使其随视口宽度或根元素字体大小动态调整。
(1)vw单位:基于视口宽度的百分比。body {font-size: 2vw; /* 字体大小为视口宽度的2% */}(2)rem单位:基于根元素(html)的字体大小,结合媒体查询动态调整根字体大小。html {font-size: 16px; /* 默认根字体大小 */}@media (max-width: 768px) {html {font-size: 14px; /* 窗口变小时减小根字体大小 */}}p {font-size: 1.2rem; /* 字体大小随根字体大小变化 */}2. 使用CSS的clamp()函数 核心思想:clamp()函数允许设置一个最小值、首选值和最大值,确保字体大小在指定范围内动态调整。body {font-size: clamp(14px, 2vw, 20px); /* 最小14px,最大20px,随视口宽度动态调整 */}3. 使用CSS的calc()函数核心思想:通过calc()函数结合固定值和相对单位计算字体大小。body {font-size: calc(10px + 1vw); /* 基础值10px,加上视口宽度的1% */}
4. 使用JavaScript动态调整字体大小核心思想:通过JavaScript监听窗口大小变化,动态调整字体大小。function adjustFontSize() {const width = window.innerWidth;const baseSize = 16; // 基准字体大小const minSize = 12; // 最小字体大小const maxSize = 24; // 最大字体大小// 根据窗口宽度计算字体大小const fontSize = Math.max(minSize, Math.min(maxSize, baseSize * (width / 1000)));document.documentElement.style.fontSize = fontSize + 'px';
}window.addEventListener('resize', adjustFontSize);
window.addEventListener('load', adjustFontSize); // 页面加载时调整5. 使用CSS框架的响应式字体工具类
核心思想:利用CSS框架(如Bootstrap、Tailwind CSS)提供的响应式工具类快速实现字体大小调整。
Bootstrap:通过自定义样式或结合媒体查询实现。
Tailwind CSS:使用响应式断点工具类,如text-sm、text-lg等。
<p class="text-sm md:text-base lg:text-lg">响应式字体</p>6. 使用CSS变量结合媒体查询
核心思想:通过CSS变量定义字体大小,结合媒体查询动态调整变量值。:root {--font-size: 16px;}@media (max-width: 768px) {:root {--font-size: 14px;}}body {font-size: var(--font-size);}
7. 使用viewport单位结合postcss-px-to-viewport插件核心思想:通过PostCSS插件将设计稿中的px单位自动转换为vw单位,实现字体大小的动态缩放。配置PostCSS插件,将px转换为vw。示例配置(postcss.config.js):
module.exports = {plugins: {'postcss-px-to-viewport': {viewportWidth: 375, // 设计稿宽度unitPrecision: 5,viewportUnit: 'vw',selectorBlackList: ['ignore'], // 忽略的类名minPixelValue: 1,mediaQuery: false,},},
};
8. 使用CSSOM View API结合window.getComputedStyle
核心思想:通过JavaScript动态获取元素计算后的样式,并调整字体大小。
function adjustTextSize() {const element = document.querySelector('.dynamic-text');const computedStyle = window.getComputedStyle(element);let fontSize = parseFloat(computedStyle.fontSize);if (window.innerWidth < 768) {fontSize *= 0.9; // 窗口变小时减小字体大小} else {fontSize *= 1.1; // 窗口变大时增大字体大小}element.style.fontSize = fontSize + 'px';
}window.addEventListener('resize', adjustTextSize);
window.addEventListener('load', adjustTextSize);
75、如何将reactive中的对象变为响应式的?
在 Vue 3 中,reactive 是一个用于创建响应式对象的 Composition API 方法。当你使用 reactive 创建对象时,它已经是响应式的,不需要额外的步骤来“使其变为响应式”。不过,如果你遇到需要处理嵌套对象或动态添加属性的情况,可以参考以下一些注意事项和技巧:
1、直接使用 reactive
import { reactive } from 'vue';
const state = reactive({count: 0,message: 'Hello Vue 3'
});
// state 是响应式的
state.count++; // 这将触发视图更新2、处理嵌套对象reactive 会递归地将对象的所有属性变为响应式,因此嵌套对象也会自动变为响应式const state = reactive({user: {name: 'Alice',age: 30}
});// 修改嵌套属性
state.user.name = 'Bob'; // 这将触发视图更新3、动态添加属性在 Vue 3 中,使用 reactive 创建的对象可以直接动态添加新属性,并且这些新属性也会是响应式的。const state = reactive({});// 动态添加属性state.newProperty = 'I am reactive!'; // 这将触发视图更新
4、使用 toRefs 或 toRef(如果需要解构)如果你需要从 reactive 对象中解构属性并保持响应性,可以使用 toRefs 或 toRef。import { reactive, toRefs } from 'vue';const state = reactive({count: 0,message: 'Hello'});const { count, message } = toRefs(state);// count 和 message 仍然是响应式的
76、对单点登录了解多少,是怎么实现的?
单点登录(Single Sign-On,SSO)是一种身份认证机制,允许用户通过一次登录即可访问多个相互信任的应用系统,无需重复输入用户名和密码。其核心是通过一个独立的认证中心(Identity Provider, IdP)来管理用户的身份认证,并通过令牌(Token)在各个应用系统(Service Provider, SP)之间共享用户的登录状态。一、单点登录的实现原理
1、认证中心(IdP):负责用户的身份认证,生成令牌(Token)。令牌通常是一个加密的字符串,包含用户的基本信息(如用户ID、角色等)。
2、应用系统(SP):信任认证中心,依赖认证中心验证用户的身份。在用户访问时,检查是否已经持有有效的令牌。如果没有,则重定向到认证中心进行登录。
3、令牌传递:用户登录成功后,认证中心将令牌返回给用户。用户访问其他应用系统时,令牌会随请求一起传递,应用系统通过验证令牌的有效性来确认用户的身份。
4、会话管理:认证中心和各个应用系统需要维护会话状态,确保令牌的有效性和安全性。二、单点登录的实现方式
1、基于 Cookie 的实现:认证中心生成令牌后,将其存储在浏览器的 Cookie 中。应用系统通过读取 Cookie 中的令牌来验证用户身份。
2、基于 Token 的实现:认证中心生成令牌后,将其返回给用户(通常通过 URL 参数或 HTTP 头部)。用户访问应用系统时,将令牌传递给应用系统,应用系统通过验证令牌来确认用户身份。
3、基于 OAuth 2.0 的实现:使用 OAuth 2.0 协议,通过授权码模式或隐式模式实现单点登录。认证中心作为授权服务器,应用系统作为客户端。
三、代码案例
1. 认证中心(IdP)认证中心负责生成 JWT 令牌。
(一)后端(Java - Spring Boot)后端负责处理用户登录和生成 JWT 令牌,同时提供受保护的资源接口。1. 添加依赖在 pom.xml 中添加以下依赖:<dependencies><!-- Spring Boot Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- JWT 依赖 --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version></dependency>
</dependencies>(2)创建 JWT 工具类import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;import java.security.Key;
import java.util.Date;public class JwtUtil {private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256); // 生成随机密钥private static final long EXPIRATION_TIME = 3600 * 1000; // 1小时// 生成令牌public static String generateToken(String userId) {return Jwts.builder().setSubject(userId).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)).signWith(SECRET_KEY).compact();}// 验证令牌public static String validateToken(String token) {try {return Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token).getBody().getSubject();} catch (JwtException e) {return null; // 令牌无效或已过期}}
}(3)创建控制器
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;@RestController
public class AuthController {// 模拟用户登录@PostMapping("/login")public Map<String, String> login(@RequestParam String username, @RequestParam String password, HttpServletResponse response) {// 简单验证(实际项目中应查询数据库)if ("user".equals(username) && "password".equals(password)) {String token = JwtUtil.generateToken(username);// 将令牌存储在响应头中(可选)response.setHeader("Authorization", "Bearer " + token);Map<String, String> result = new HashMap<>();result.put("message", "Login successful");result.put("token", token);return result;} else {throw new RuntimeException("Invalid username or password");}}// 受保护的资源@GetMapping("/protected")public Map<String, String> protectedResource(@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {throw new RuntimeException("Unauthorized");}String token = authorizationHeader.substring(7);String userId = JwtUtil.validateToken(token);if (userId == null) {throw new RuntimeException("Invalid or expired token");}Map<String, String> result = new HashMap<>();result.put("message", "Access granted");result.put("user_id", userId);return result;}
}(二)前端(Vue.js 或原生 JavaScript)
前端负责与后端交互,完成登录并访问受保护资源。
<div id="app"><div><h2>登录</h2><input v-model="username" placeholder="用户名" /><input v-model="password" type="password" placeholder="密码" /><button @click="login">登录</button></div><div v-if="token"><h2>受保护资源</h2><button @click="fetchProtectedResource">访问受保护资源</button><p v-if="protectedData">{{ protectedData }}</p></div>
</div><script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script>
new Vue({el: '#app',data: {username: '',password: '',token: null,protectedData: null},methods: {async login() {try {const response = await fetch('http://localhost:8080/login', {method: 'POST',headers: { 'Content-Type': 'application/x-www-form-urlencoded' },body: `username=${this.username}&password=${this.password}`});const data = await response.json();if (data.token) {this.token = data.token;alert('登录成功');} else {alert('登录失败');}} catch (error) {console.error(error);}},async fetchProtectedResource() {try {const response = await fetch('http://localhost:8080/protected', {headers: { 'Authorization': `Bearer ${this.token}` }});if (response.ok) {const data = await response.json();this.protectedData = JSON.stringify(data);} else {alert('访问受保护资源失败');}} catch (error) {console.error(error);}}}
});
</script>2. 应用系统(SP)
应用系统负责验证 JWT 令牌。
77、Cookie可以跨域吗?
Cookie 默认情况下不能跨域,但通过特定机制(如 Domain 属性、跨域共享技术)可以在一定条件下实现跨域访问。以下是详细解析:
1. Cookie 的基本行为同源策略限制:Cookie 默认遵循浏览器的同源策略,仅在相同域名、协议和端口下共享。作用域:Cookie 的作用域由 Domain 和 Path 属性决定。默认行为:若未显式设置 Domain,Cookie 仅对设置它的域名及其子域名有效。显式设置 Domain:若将 Domain 设置为父域名(如 example.com),则该 Cookie 对所有子域名(如 sub.example.com)可用。2. 跨域访问 Cookie 的条件
(1)显式设置 Domain:
例如,在 sub.example.com 设置的 Cookie中,将Domain设置为example.com,则该Cookie对 example.com及其所有子域名(如 another.sub.example.com)可见。document.cookie = "key=value; Domain=example.com; Path=/";
(2)跨域请求的场景跨域 AJAX 请求:若服务器在响应头中设置了 Access-Control-Allow-Credentials: true,且客户端请求中包含 credentials: 'include',则浏览器会携带符合条件的 Cookie。限制:Domain 必须匹配或为父域名,且 Path 必须匹配请求路径。跨域 iframe:若父页面和 iframe 的 Domain 属性相同或为父子域名关系,且 Path 匹配,则 Cookie 可共享。
3.跨域 Cookie 的安全风险CSRF 攻击:若 Cookie 可被恶意网站访问,攻击者可能利用其伪造请求。数据泄露:不当的 Domain 设置可能导致 Cookie 被其他子域名读取。缓解措施:使用 HttpOnly 标志,防止客户端脚本访问 Cookie。使用 Secure 标志,确保 Cookie 仅通过 HTTPS 传输。设置 SameSite 属性(Strict、Lax 或 None),限制 Cookie 的跨站发送。
4. 替代方案跨域身份验证:使用 OAuth 2.0 等协议,通过授权服务器颁发令牌,避免直接依赖 Cookie。跨域存储:使用 localStorage 或 sessionStorage,但需注意这些存储仅限于同源。通过后端代理或 API 实现跨域数据共享。
5. 示例代码
(1)设置跨域 Cookie
// 在 sub.example.com 设置 Cookie,使其对 example.com 及其子域名可用
document.cookie = "key=value; Domain=example.com; Path=/; Secure; HttpOnly";(2)跨域 AJAX 请求携带 Cookie
fetch('https://api.example.com/data', {method: 'GET',credentials: 'include', // 必须显式指定携带 Cookie
});6. 总结默认行为:Cookie 不能跨域,仅在同源或显式设置的 Domain 和 Path 范围内有效。跨域条件:需通过设置 Domain、Path,并结合 SameSite、Secure 等属性控制。安全建议:谨慎设置 Domain,优先使用 SameSite=Strict 和 HttpOnly,避免跨域 Cookie 被滥用。
78、跨页面通信方式有哪些?
跨页面通信是指在浏览器中不同页面(包括同一网站的多个标签页、不同窗口或不同来源的页面)之间传递数据或消息的技术。实现跨页面通信的方式多种多样,根据具体需求和场景可以选择不同的方法。以下是常见的跨页面通信方式:1. 使用localStorage或sessionStorage结合storage事件原理:localStorage 和 sessionStorage 是浏览器提供的本地存储机制,支持在同源页面之间共享数据。当一个页面修改存储内容时,会触发其他页面的 storage 事件。适用场景:同一网站的不同标签页或窗口之间实时通信。需要持久化存储数据(localStorage)或仅在当前会话中有效(sessionStorage)。优点:简单易用,无需额外依赖。支持跨标签页通信。缺点:无法直接实现点对点通信。需要通过轮询或事件监听来检测数据变化。2. 使用BroadcastChannel API原理:BroadcastChannel 是一种现代浏览器提供的 API,允许同一来源的不同标签页、窗口或 iframe 之间通过频道进行实时通信。适用场景:需要高效、实时的跨标签页通信。浏览器支持 BroadcastChannel(现代浏览器普遍支持)。优点:简单易用,无需服务器支持。支持实时通信,性能较高。缺点:仅支持同源页面。旧版浏览器不支持。
3. 使用postMessage API原理:postMessage 是一种安全的跨源通信方式,允许不同来源的窗口、iframe 或标签页之间传递消息。适用场景:需要跨域通信(例如,不同网站之间的通信)。点对点通信。优点:支持跨源通信。安全性高,支持消息来源验证。缺点:需要明确的目标窗口或 iframe。实现复杂度较高
4. 使用 SharedWorker原理:SharedWorker 是一种可以在多个标签页或窗口之间共享的 Web Worker。通过共享的 Worker 脚本,不同页面可以与同一个 Worker 通信,从而实现数据共享。适用场景:需要后台线程处理复杂计算或数据共享。同一来源的不同标签页之间通信。优点:支持后台线程处理,不阻塞主线程。数据共享效率高。缺点:浏览器支持有限(部分旧版浏览器不支持)。实现复杂度较高。
5. 使用Service Worker原理:Service Worker 是一种运行在浏览器后台的脚本,可以拦截网络请求、缓存资源,并作为消息代理实现跨页面通信。适用场景:需要离线支持或消息推送。同一来源的不同标签页之间通信。优点:支持离线功能和消息推送。可以作为消息代理,实现复杂的通信逻辑。缺点:实现复杂度较高。需要 HTTPS 环境。
6. 使用 Cookies原理:Cookies 是存储在客户端的小型数据,可以在同一来源的不同页面之间共享。通过设置和读取 Cookies,可以实现简单的跨页面通信。适用场景:需要存储少量共享数据。同一来源的不同页面之间通信。优点:简单易用,浏览器支持广泛。缺点:数据量有限(通常不超过 4KB)。每次请求都会携带 Cookies,可能影响性能。安全性较低,容易被篡改。
7. 使用 URL 参数或哈希值原理:通过在 URL 中添加查询参数或哈希值,可以在页面之间传递数据。目标页面可以通过解析 URL 获取这些数据。适用场景:需要传递简单的数据(如标识符、状态等)。页面跳转时传递数据。优点:简单易用,无需额外代码。缺点:数据暴露在 URL 中,可能存在安全风险。不适合传递大量或敏感数据。8. 使用WebSocket原理:WebSocket 是一种全双工通信协议,允许客户端和服务器之间进行实时通信。通过服务器中转,不同客户端(包括不同标签页或窗口)可以实现通信。适用场景:需要实时通信(如聊天应用、实时通知等)。跨标签页或跨用户通信。优点:支持实时通信,性能高。可以实现复杂的通信逻辑。缺点:需要服务器支持。实现复杂度较高。
9. 使用第三方库或服务原理:借助第三方库(如 Firebase、Pusher 等)或服务,可以实现跨页面或跨用户的实时通信。适用场景:需要快速实现复杂的通信功能。跨用户或跨设备通信。优点:开发效率高,功能强大。支持复杂的通信场景。缺点:依赖第三方服务,可能存在安全和隐私问题。需要额外的网络请求。
总结
同源通信:推荐使用 BroadcastChannel、localStorage + storage 事件或 SharedWorker。
跨域通信:推荐使用 postMessage 或 WebSocket。
实时通信:推荐使用 WebSocket 或 BroadcastChannel。
简单数据共享:可以使用 Cookies 或 URL 参数。
跨页面通信案例
<!DOCTYPE html>
<html>
<head><title>页面A</title>
</head>
<body><h1>页面A</h1><button id="sendMessage">发送消息到其他选项卡</button><script>document.getElementById('sendMessage').addEventListener('click', () => {localStorage.setItem('sharedData', 'Hello from Page A!');});// 监听 storage 事件(可选,用于确认其他页面是否收到消息)window.addEventListener('storage', (event) => {if (event.key === 'sharedData') {console.log('页面A接收到更新后的数据:', event.newValue);}});</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head><title>页面B</title>
</head>
<body><h1>页面B</h1><div id="receivedData"></div><script>// 监听 storage 事件window.addEventListener('storage', (event) => {if (event.key === 'sharedData') {document.getElementById('receivedData').innerText = `Received: ${event.newValue}`;}});</script>
</body>
</html>
79、在通过接口请求后台数据的时候,如何暂停或者手动暂停?
一:使用 AbortController(推荐用于 Fetch 请求)
使用 AbortController(推荐用于 Fetch 请求)
原理:AbortController 是一个 Web 标准接口,可以用来中止 Fetch 请求。通过创建一个 AbortController 实例,并将其 signal 属性传递给 Fetch 请求,就可以在需要时调用 abort() 方法来终止请求
const controller = new AbortController();
const { signal } = controller;
const url = "https://example.com/data";
fetch(url, { signal }).then(response => response.json()).then(data => console.log(data)).catch(error => console.error(error));// 当需要暂停请求时
controller.abort();二:使用 Axios 的 CancelToken
原理:axios 提供了 CancelToken 来取消请求。通过创建一个 CancelToken,并在请求中传递该 token,可以在需要时调用 cancel() 方法来取消请求
import axios from 'axios';// 创建取消令牌
const cancelTokenSource = axios.CancelToken.source();// 发起请求
axios.get('/some-url', {cancelToken: cancelTokenSource.token
}).then(response => {// 处理响应
}).catch(error => {if (axios.isCancel(error)) {console.log('Request canceled', error.message);} else {// 处理其他错误}
});// 当需要取消请求时
cancelTokenSource.cancel('Operation canceled by the user.');三:使用自定义的“假暂停”机制
原理:通过设计一个控制器 Promise,并结合请求使用 Promise.all 封装。当请求完成时,检查控制器是否处于暂停状态,如果没有暂停,则正常返回数据;如果处于暂停状态,则等待恢复操作。
function createPauseControllerPromise() {const result = {isPause: false,resolveWhenResume: false,resolve(value) {},pause() {this.isPause = true;},resume() {if (!this.isPause) return;this.isPause = false;if (this.resolveWhenResume) {this.resolve();}},promise: Promise.resolve()};const promise = new Promise((res) => {result.resolve = res;});result.promise = promise;return result;
}function requestWithPauseControl(request) {const controller = createPauseControllerPromise();const controlRequest = request().then((data) => {if (!controller.isPause) controller.resolve();controller.resolveWhenResume = controller.isPause;return data;});const result = Promise.all([controlRequest, controller.promise]).then((data) => data[0]);result.finally(() => controller.resolve());(result as any).pause = controller.pause.bind(controller);(result as any).resume = controller.resume.bind(controller);return result;
}// 使用方法
const result = requestWithPauseControl(() => fetch('https://example.com/data').then(res => res.json()));
result.then(data => console.log(data));// 暂停请求
result.pause();// 恢复请求
setTimeout(() => {result.resume();
}, 4000);四:使用第三方库
原理:一些第三方库提供了更高级的功能和易用的 API 来简化请求管理,例如 axios-cancel 插件。
import axios from 'axios';
import axiosCancel from 'axios-cancel';axiosCancel(axios);const source = axios.CancelToken.source();axios.get('/api/data', {cancelToken: source.token
}).then(response => {// 处理响应
});// 取消请求
source.cancel('请求已被取消');
80、前端当页面刷新,vuex里的状态数据变了,怎么解决
当页面刷新时,Vuex 中的状态数据会丢失,这是因为 Vuex 的状态存储在内存中,而页面刷新会导致内存中的数据被清空。以下是几种解决方法:
1. 使用浏览器本地存储
(1)手动实现
在 Vuex 的 mutations 中,每次状态变更时手动将状态保存到 localStorage 或 sessionStorage 中。在页面加载时,从本地存储中读取状态并初始化 Vuex。
// store/index.js
import { createStore } from 'vuex';const store = createStore({state: {user: JSON.parse(localStorage.getItem('user')) || null,},mutations: {setUser(state, user) {state.user = user;localStorage.setItem('user', JSON.stringify(user));},},
});(2)自动持久化
通过 Vuex 的插件机制自动保存所有状态
const localStoragePlugin = store => {if (localStorage.getItem('vuex')) {store.replaceState(Object.assign({}, store.state, JSON.parse(localStorage.getItem('vuex'))));}store.subscribe((mutation, state) => {localStorage.setItem('vuex', JSON.stringify(state));});
};const store = createStore({// ...state, mutations, actions 等plugins: [localStoragePlugin],
});2. 使用 vuex-persistedstate 插件
这是一个专门为 Vuex 设计的持久化插件
npm install vuex-persistedstate
import createPersistedState from 'vuex-persistedstate';const store = createStore({// ...plugins: [createPersistedState()],
});3. 使用 IndexedDB
对于需要存储大量数据的应用,可以使用 IndexedDB。可以借助 localForage 库来简化 IndexedDB 的使用
npm install localforage
import localforage from 'localforage';
import createPersistedState from 'vuex-persistedstate';const dbPersist = {async getItem(key) {return (await localforage.getItem(key)) || null;},async setItem(key, value) {await localforage.setItem(key, value);},async removeItem(key) {await localforage.removeItem(key);},
};const store = createStore({// ...plugins: [createPersistedState({ storage: dbPersist })],
});4. 后端存储
对于需要跨标签页或跨浏览器会话共享状态的高级应用,可以考虑将状态存储在后端数据库中。通过在 Vuex 的 actions 中调用后端 API 来获取和保存状态
5. 页面刷新事件监听
可以通过监听页面刷新事件手动保存状态,并在页面加载时恢复状态
// 保存Vuex数据到localStorage
const saveVuexData = () => {const state = store.state;localStorage.setItem('vuexState', JSON.stringify(state));
};// 从localStorage恢复Vuex数据
const recoverVuexData = () => {const savedState = localStorage.getItem('vuexState');if (savedState) {store.replaceState(JSON.parse(savedState));}
};// 在页面刷新前调用
window.addEventListener('beforeunload', saveVuexData);
window.addEventListener('load', recoverVuexData);
81、前端中什么是数据埋点?
1. 什么是数据埋点?
数据埋点是指在前端页面或应用中,通过代码在特定位置插入“监测点”,用于收集用户行为数据(如点击、浏览、输入等)或业务指标(如页面停留时间、转化率等)。埋点的核心目的是获取用户行为数据,为产品优化、运营决策提供依据。
2. 数据埋点的作用
用户行为分析:
了解用户如何使用产品(如点击了哪些按钮、浏览了哪些页面)。
产品优化:
发现用户痛点(如某个功能使用率低、页面跳出率高)。
运营决策:
评估活动效果(如某个按钮的点击转化率)。
异常监控:
捕获用户操作中的异常行为(如频繁刷新页面)。
3. 数据埋点的实现方式
(1)手动埋点
定义:
开发者在代码中手动插入埋点逻辑,通过调用埋点 SDK 或直接发送请求到后端。
示例:
// 示例:使用自定义事件埋点
document.getElementById('myButton').addEventListener('click', function() {// 假设有一个埋点 SDKtrackEvent('button_click', { buttonId: 'myButton', page: 'home' });
});
特点:
优点:灵活、可定制。
缺点:代码侵入性强,维护成本高
(2)可视化埋点
定义:
通过可视化工具(如神策、GrowingIO)在页面上“圈选”元素,自动生成埋点代码。
特点:
优点:无需修改代码,适合非技术人员操作。
缺点:依赖工具,灵活性较低。
(3)无埋点(全埋点)
定义:
预先在页面中加载埋点 SDK,自动收集所有用户行为数据(如点击、滚动、输入等)。
示例:
使用 Mixpanel 或 Google Analytics 的无埋点功能。
特点:
优点:无需手动埋点,数据全面。
缺点:数据量大,需要后期清洗和分析。
(4)代码埋点(后端埋点)
定义:
在后端接口中记录用户行为(如用户点击按钮后触发的 API 请求)。
特点:
优点:数据准确,适合敏感数据。
缺点:无法捕获前端交互细节。
4. 数据埋点的常见场景
页面交互:
按钮点击、链接跳转、表单提交。
页面浏览:
页面停留时间、滚动深度。
用户行为:
搜索关键词、筛选条件、收藏操作。
业务指标:
转化率、订单金额、用户留存。
5. 数据埋点的挑战
数据准确性:
埋点代码可能因代码更新、版本冲突导致数据丢失。
数据隐私:
埋点可能涉及用户隐私数据(如 IP 地址、设备信息),需遵守法律法规(如 GDPR)。
数据清洗:
埋点数据可能包含噪声(如测试数据、爬虫流量),需要清洗。
6. 数据埋点的最佳实践
明确目标:
确定需要收集哪些数据,避免过度埋点。
统一规范:
制定埋点命名规则(如事件名、属性名)。
版本控制:
埋点代码与业务代码同步更新,避免遗漏。
数据验证:
埋点上线后,通过日志或工具验证数据准确性。
隐私合规:
确保埋点数据符合隐私政策,避免敏感信息泄露。
7. 常用埋点工具
第三方工具:
Google Analytics、Mixpanel、神策、GrowingIO。
自建埋点系统:
使用开源框架(如 Segment.io)或自定义埋点 SDK。
82、防抖和节流
一、防抖:
防抖是指在事件被触发后,等待一段时间再执行回调函数。如果在这段时间内事件又被触发,则重新计时。
延迟执行:只有在事件停止触发后,经过指定的时间间隔,回调函数才会执行。
适用场景:输入框实时搜索(用户停止输入后再发送请求)。窗口调整大小事件(resize)。滚动事件(scroll)中加载数据。
代码示例:
function debounce(func, delay) {let timer;return function(...args) {const context = this;clearTimeout(timer);timer = setTimeout(() => {func.apply(context, args);}, delay);};
}// 使用示例
const handleInput = debounce(() => {console.log('搜索请求发送');
}, 500);document.getElementById('searchInput').addEventListener('input', handleInput);二、节流
节流是指在规定的时间间隔内,只允许回调函数执行一次。即使事件被频繁触发,回调函数也会按照固定的时间间隔执行。
限制频率:无论事件触发多少次,回调函数都只在固定的时间间隔内执行一次。
适用场景:滚动事件(scroll)中记录滚动位置。按钮点击事件(防止快速多次点击)。页面滚动到底部加载更多数据。
function throttle(func, limit) {let lastFunc;let lastRan;return function(...args) {const context = this;if (!lastRan) {func.apply(context, args);lastRan = Date.now();} else {clearTimeout(lastFunc);lastFunc = setTimeout(() => {if ((Date.now() - lastRan) >= limit) {func.apply(context, args);lastRan = Date.now();}}, limit - (Date.now() - lastRan));}};
}// 使用示例
const handleScroll = throttle(() => {console.log('滚动事件触发');
}, 1000);window.addEventListener('scroll', handleScroll);三、防抖与节流的区别特性 防抖(Debounce) 节流(Throttle)
执行时机 事件停止触发后,经过指定时间执行 每隔固定时间间隔执行一次
应用场景 输入框搜索、窗口调整大小、滚动停止后加载数据 滚动事件、按钮点击、页面滚动到底部加载更多数据
核心逻辑 延迟执行,重新触发则重新计时 限制频率,固定时间间隔内只执行一次
83、nodejs中有哪些框架,都是做什么的?
1. Express.js
用途:Express 是最流行的轻量级 Web 应用框架,用于构建 Web 应用和 API。
特点:简单易用,提供路由、中间件、模板引擎等功能。社区庞大,插件丰富,适合快速开发。
适用场景:RESTful API、小型到中型的 Web 应用。
2. Koa.js
用途:Koa 是一个更现代、更轻量级的 Web 框架,由 Express 的原班人马开发。
特点:基于 ES6 的 async/await,代码更简洁。没有内置中间件,完全依赖社区中间件。
适用场景:需要更灵活、更现代的 Web 应用。
3. NestJS
用途:NestJS 是一个用于构建高效、可扩展的服务器端应用的渐进式框架。
特点:结合了 OOP(面向对象编程)、FP(函数式编程)和 FRP(函数响应式编程)。支持 TypeScript,提供强大的依赖注入系统。
适用场景:大型企业级应用、微服务架构。
4. Fastify
用途:Fastify 是一个高性能的 Web 框架,专注于速度和低开销。
特点:基于 V8 引擎优化,性能极高。支持插件系统,易于扩展。
适用场景:需要高并发的 API 服务。
5. Hapi
用途:Hapi 是一个用于构建 Web 应用和服务的强大框架。
特点:内置插件系统,支持配置驱动开发。适合构建大型应用,强调安全性和可扩展性。
适用场景:企业级应用、API 服务。
6. AdonisJS
用途:AdonisJS 是一个全栈 Web 应用框架,灵感来自 Laravel。
特点:提供完整的开发工具链,包括 ORM、队列、认证等。支持 TypeScript,代码结构清晰。
适用场景:需要快速开发全栈应用的团队。
7. Meteor
用途:Meteor 是一个全栈 JavaScript 框架,用于构建实时 Web 应用。
特点:提供实时数据同步,支持跨平台开发。包含前端和后端的一体化解决方案。
适用场景:实时应用、聊天应用、协作工具。
8. Sails.js
用途:Sails.js 是一个用于构建自定义、企业级 Node.js 应用的 MVC 框架。
特点:支持自动生成的 REST API。兼容大多数数据库,包括 MongoDB 和 MySQL。
适用场景:需要快速生成 CRUD 接口的项目。
9. FeathersJS
用途:FeathersJS 是一个用于构建实时应用和微服务的框架。
特点:提供即用型的实时 API。支持多种数据库和服务适配器。
适用场景:实时应用、微服务架构。
10. Total.js
用途:Total.js 是一个模块化的全栈框架,支持 MVC 架构。
特点:提供丰富的模块和组件。支持 NoSQL 数据库和多种客户端框架。
适用场景:需要模块化开发的项目。
11. LoopBack
用途:LoopBack 是一个用于构建 API 的高度可扩展框架。
特点:自动生成 API,支持多种数据源。提供强大的 CLI 工具和可视化编辑器。
适用场景:需要快速生成 API 的项目。
12. Next.js(基于 Node.js 的部分功能)
用途:Next.js 是一个 React 框架,但部分功能依赖 Node.js(如 API 路由)。
特点:支持服务器端渲染(SSR)、静态站点生成(SSG)。提供文件系统路由和 API 路由。
适用场景:React 全栈应用、需要 SEO 的项目。
13. Nuxt.js(基于 Node.js 的部分功能)
用途:Nuxt.js 是一个 Vue.js 框架,但部分功能依赖 Node.js(如服务器端渲染)。
特点:支持 SSR、SSG 和动态路由。提供自动代码分割和优化。
适用场景:Vue.js 全栈应用、需要 SEO 的项目。
84、在vue3项目中,下拉框的数据需要从后端返回,数据量庞大,如何进行优化,
实现思路
初始加载:加载初始的一部分数据。
监听滚动:监听下拉框的滚动事件,当用户滚动到接近底部时,加载更多数据。
动态追加数据:将新加载的数据追加到现有数据中。<template><div class="dropdown" @scroll="handleScroll" ref="dropdown"><ul><li v-for="item in items" :key="item.id">{{ item.name }}</li></ul><div v-if="loading" class="loading">Loading...</div></div>
</template><script>
import { ref, onMounted } from 'vue';export default {setup() {const items = ref([]);const loading = ref(false);const dropdown = ref(null);const page = ref(1);const pageSize = 20; // 每页加载20条数据const hasMore = ref(true); // 是否有更多数据// 模拟从后端获取数据const fetchData = async (page, pageSize) => {// 这里用 setTimeout 模拟异步请求return new Promise((resolve) => {setTimeout(() => {const data = [];const totalItems = 100; // 假设总共有100条数据const start = (page - 1) * pageSize;const end = Math.min(start + pageSize, totalItems);for (let i = start; i < end; i++) {data.push({ id: i + 1, name: `Item ${i + 1}` });}resolve(data);}, 500);});};const loadMoreData = async () => {if (loading.value || !hasMore.value) return;loading.value = true;const newData = await fetchData(page.value, pageSize);if (newData.length < pageSize) {hasMore.value = false; // 没有更多数据了}items.value = [...items.value, ...newData];page.value++;loading.value = false;};const handleScroll = () => {const element = dropdown.value;if (element) {const { scrollTop, scrollHeight, clientHeight } = element;if (scrollTop + clientHeight >= scrollHeight - 10) {loadMoreData();}}};onMounted(() => {loadMoreData(); // 初始加载数据});return {items,loading,dropdown,handleScroll,};},
};
</script><style>
.dropdown {height: 200px;overflow-y: auto;border: 1px solid #ccc;
}
.loading {text-align: center;padding: 10px;
}
</style>
85、在vue3项目中如何做代码分割?
在 Vue 3 项目中,代码分割(Code Splitting)是一个优化应用性能的重要技术,特别是在处理大型单页应用(SPA)时。代码分割可以让你将代码拆分成更小的块,按需加载,从而减少初始加载时间。以下是几种在 Vue 3 项目中实现代码分割的方法:1、使用 Vue Router 的动态导入
Vue Router 是 Vue 官方提供的路由解决方案,支持通过动态导入(Dynamic Import)实现路由级别的代码分割。
优点路由级别的代码分割是最常见的场景。只有当用户访问某个路由时,才会加载对应的组件代码。步骤:
在 router/index.js 中,将组件通过 import() 动态导入。
Webpack 或 Vite 会自动将这些组件打包成单独的代码块。
import { createRouter, createWebHistory } from 'vue-router';const routes = [{path: '/',name: 'Home',component: () => import('@/views/Home.vue'), // 动态导入},{path: '/about',name: 'About',component: () => import('@/views/About.vue'), // 动态导入},
];const router = createRouter({history: createWebHistory(),routes,
});export default router;2、使用 Vue 的 defineAsyncComponent
Vue 3 提供了 defineAsyncComponent 方法,用于动态加载组件。
优点适用于局部组件的按需加载。可以结合加载状态(如加载指示器)使用。步骤:
使用 defineAsyncComponent 包装需要异步加载的组件。
这种方式适用于非路由组件的代码分割。
import { defineAsyncComponent } from 'vue';export default {components: {AsyncComponent: defineAsyncComponent(() =>import('@/components/MyComponent.vue') // 动态导入组件),},template: `<div><h1>Hello Vue 3</h1><AsyncComponent /></div>`,
};3、手动拆分公共代码(Webpack 或 Vite 配置)
如果你希望对某些公共库(如 lodash、axios)或工具代码进行代码分割,可以通过构建工具的配置来实现。
优点:减少重复代码,提高缓存利用率。适用于第三方库或公共工具代码的分割。Webpack 示例
在webpack.config.js 中,使用SplitChunksPlugin 配置公共代码分割:
module.exports = {optimization: {splitChunks: {chunks: 'all', // 对所有代码块进行分割cacheGroups: {vendors: {test: /[\\/]node_modules[\\/]/,name: 'vendors',chunks: 'all',},},},},
};
Vite 示例
Vite 默认已经对 node_modules 中的依赖进行了优化,通常不需要额外配置。如果需要更细粒度的控制,可以通过 build.rollupOptions 配置:import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';export default defineConfig({plugins: [vue()],build: {rollupOptions: {output: {manualChunks: {vendor: ['vue', 'axios'], // 将 vue 和 axios 打包到 vendor 块},},},},
});4、结合 Suspense 组件处理异步加载
在 Vue 3 中,<Suspense> 组件可以用来处理异步组件的加载状态
import { defineAsyncComponent, defineComponent, Suspense } from 'vue';const AsyncComponent = defineAsyncComponent(() =>import('@/components/MyComponent.vue')
);export default defineComponent({template: `<div><h1>Async Component Example</h1><Suspense><template #default><AsyncComponent /></template><template #fallback><p>Loading...</p></template></Suspense></div>`,
});5、使用 prefetch 和 preload 优化加载prefetch:在浏览器空闲时预加载资源,适合用户可能访问的页面。preload:在页面加载时优先加载资源,适合当前页面需要的资源。
动态导入时,Webpack 会自动为路由组件添加 webpackPrefetch 或 webpackPreload 属性:
const routes = [{path: '/about',name: 'About',component: () =>import(/* webpackPrefetch: true */ '@/views/About.vue'), // 预取},
];
86、vue项目中如何通过路由传参携带数据到其他页面,供其他页面使用共享数据
在 Vue 项目中,通过路由传参携带数据到其他页面是一种常见的需求。Vue Router 提供了多种方式来实现这一点,主要包括 路径参数、查询参数 和 使用 Vuex 或 Provide/Inject 进行状态管理。下面分别介绍这些方法:1. 路径参数
路径参数是 URL 的一部分,通常用于标识资源。例如,/user/:id 中的 :id 就是路径参数。
(1)在路由配置中定义路径参数:
const routes = [{ path: '/user/:id', component: UserComponent }
];
(2)导航到目标页面时传递参数:
this.$router.push({ name: 'user', params: { id: 123 } });
(3)在目标组件中接收参数:export default {mounted() {console.log(this.$route.params.id); // 输出 123}};2. 查询参数
查询参数是 URL 中的 ?key=value 部分,通常用于传递可选信息。
(1)导航到目标页面时传递查询参数:this.$router.push({ path: '/user', query: { id: 123, name: 'John' } });
(2)在目标组件中接收参数:export default {mounted() {console.log(this.$route.query.id); // 输出 123console.log(this.$route.query.name); // 输出 John}};
87、vue项目中自定义指令
//在 Vue 项目中,自定义指令可以用来封装一些 DOM 操作或逻辑,以便在多个组件中复用。Vue 提供了全局和局部两种方式来定义自定义指令。
一、全局自定义指令
全局自定义指令可以在整个应用中使用。我们可以通过 app.directive 方法(Vue 3)或 Vue.directive 方法(Vue 2)来定义。
Vue 3 示例:
// main.js
import { createApp } from 'vue';
import App from './App.vue';const app = createApp(App);// 定义全局自定义指令 v-focus
app.directive('focus', {// 当绑定元素插入到 DOM 中时触发mounted(el) {el.focus(); // 让元素自动获得焦点}
});app.mount('#app');//使用全局自定义指令
<template><div><input v-focus placeholder="自动获得焦点" /></div>
</template>
解释: v-focus 是我们定义的全局指令。当 <input> 元素插入到 DOM 中时,mounted 钩子会被触发,执行 el.focus(),让输入框自动获得焦点。二、局部自定义指令
局部自定义指令只能在定义它的组件中使用。
<template><div><input v-color="'red'" placeholder="文字颜色为红色" /></div>
</template><script>
export default {directives: {// 定义局部自定义指令 v-colorcolor: {// 绑定元素插入到 DOM 中时触发mounted(el, binding) {el.style.color = binding.value; // 设置文字颜色}}}
};
</script>
在上面的代码中,v-color 是局部指令。
使用时传入参数 'red',通过 binding.value 获取参数值,并设置到元素的 style.color 上。三、自定义指令的钩子函数
自定义指令提供了一些钩子函数,可以根据需要实现不同的逻辑:created:元素绑定到指令时调用(仅调用一次)。
beforeMount:在元素插入 DOM 之前调用。
mounted:元素插入到 DOM 后调用。
beforeUpdate:在绑定元素的父组件更新前调用。
updated:在绑定元素的父组件更新后调用。
beforeUnmount:在绑定元素卸载前调用(Vue 3)。
unmounted:在绑定元素卸载后调用(Vue 3)。//动态背景色
//v-bg-color 指令会根据传入的参数动态设置背景色。当绑定的值发生变化时,updated 钩子会被触发,更新背景色。
<template><div><div v-bg-color="'blue'" style="width: 100px; height: 100px;"></div></div>
</template><script>
export default {directives: {bgColor: {mounted(el, binding) {el.style.backgroundColor = binding.value; // 设置背景色},updated(el, binding) {el.style.backgroundColor = binding.value; // 更新背景色}}}
};
</script>四、带参数和修饰符的自定义指令
//带参数和修饰符
//v-permission:"delete":传递参数 "delete"。
//v-permission.admin:使用修饰符 .admin。
//在 mounted 钩子中,通过 binding.arg 获取参数,通过 binding.modifiers 获取修饰符。根据权限逻辑,决定是否显示元素。
<template><div><button v-permission:"delete" v-permission.admin>删除</button></div>
</template><script>
export default {directives: {permission: {mounted(el, binding, vnode) {const { arg, modifiers } = binding;console.log('权限操作:', arg); // 输出 "delete"console.log('修饰符:', modifiers); // 输出 { admin: true }// 模拟权限校验逻辑if (!hasPermission(arg, modifiers)) {el.style.display = 'none'; // 如果没有权限,隐藏元素}}}}
};function hasPermission(action, modifiers) {// 模拟权限校验const userRoles = ['admin']; // 当前用户角色return ((action === 'delete' && userRoles.includes('admin')) ||(modifiers.admin && userRoles.includes('admin')));
}
</script>
88、vue3项目中常用的自定义Hooks有哪些,如何定义,如何使用
在 Vue 3 中,虽然“自定义 Hooks”这个术语通常与 React 关联,但 Vue 3 引入的组合式 API(Composition API)允许我们实现类似的功能。通过组合式 API,你可以将逻辑封装到可重用的函数中,这些函数可以看作是 Vue 的“自定义 Hooks”。常用的自定义 Hooks
使用状态管理:封装 ref 或 reactive 的逻辑。
处理表单输入:封装表单状态和验证逻辑。
异步数据获取:封装 API 请求和错误处理。
计时器和动画:封装 setInterval、setTimeout 或动画逻辑。
权限和角色管理:封装用户权限检查逻辑。
如何定义自定义 Hooks
自定义 Hooks 是普通的 JavaScript 函数,它们使用 Vue 的组合式 API,如 ref、reactive、computed、watch 等。import { ref, onMounted } from 'vue';export function useFetch(url) {const data = ref(null);const error = ref(null);const loading = ref(true);async function fetchData() {try {const response = await fetch(url);if (!response.ok) {throw new Error('Network response was not ok');}data.value = await response.json();} catch (err) {error.value = err.message;} finally {loading.value = false;}}onMounted(() => {fetchData();});return { data, error, loading };
}//如何使用自定义 Hooks
在组件中使用自定义 Hooks 时,只需导入并调用它,然后解构返回的响应式数据或方法。
<template><div><p v-if="loading">Loading...</p><p v-else-if="error">Error: {{ error }}</p><div v-else><h1>Data:</h1><pre>{{ data }}</pre></div></div>
</template><script>
import { useFetch } from './useFetch'; // 假设 useFetch 定义在同目录下的 useFetch.js 文件中export default {setup() {const { data, error, loading } = useFetch('https://api.example.com/data');return { data, error, loading };}
};
</script>
89、vue2可以自定义Hooks吗?若果可以,定义一个处理时间格式的hooks,并给出具体代码,以及使用方法?
在 Vue 2 中,虽然没有 Vue 3 中的 Composition API(即 setup 函数和 ref、reactive 等),但可以通过自定义 mixin 或高阶组件来实现类似的功能。虽然严格意义上不是“Hooks”,但可以通过这些方式实现复用逻辑。一、使用mixin
//1. 定义时间格式化 mixin// timeFormatMixin.js
export default {methods: {// 格式化时间的函数formatDate(date, format = 'yyyy-MM-dd HH:mm:ss') {if (!date) return '';date = new Date(date); // 确保 date 是 Date 对象const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, '0');const day = String(date.getDate()).padStart(2, '0');const hours = String(date.getHours()).padStart(2, '0');const minutes = String(date.getMinutes()).padStart(2, '0');const seconds = String(date.getSeconds()).padStart(2, '0');return format.replace('yyyy', year).replace('MM', month).replace('dd', day).replace('HH', hours).replace('mm', minutes).replace('ss', seconds);},},
};//2. 在组件中使用这个 mixin
// MyComponent.vue
<template><div><p>当前时间:{{ formattedTime }}</p></div>
</template><script>
import timeFormatMixin from './timeFormatMixin';export default {mixins: [timeFormatMixin],data() {return {currentTime: new Date(), // 当前时间};},computed: {// 使用 mixin 中的方法格式化时间formattedTime() {return this.formatDate(this.currentTime);},},
};
</script>二、使用高阶组件实现类似功能
//定义高阶组件 withTimeFormat.js
export default (WrappedComponent) => {return {name: 'WithTimeFormat',props: {date: {type: [Date, String, Number],default: () => new Date(),},format: {type: String,default: 'yyyy-MM-dd HH:mm:ss',},},computed: {formattedDate() {const date = new Date(this.date);const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, '0');const day = String(date.getDate()).padStart(2, '0');const hours = String(date.getHours()).padStart(2, '0');const minutes = String(date.getMinutes()).padStart(2, '0');const seconds = String(date.getSeconds()).padStart(2, '0');return this.format.replace('yyyy', year).replace('MM', month).replace('dd', day).replace('HH', hours).replace('mm', minutes).replace('ss', seconds);},},render(h) {return h(WrappedComponent, {props: {...this.$props,formattedDate: this.formattedDate,},});},};
};//使用高阶组件
// MyComponent.vue
<template><div><p>格式化时间:{{ formattedDate }}</p></div>
</template><script>
import withTimeFormat from './withTimeFormat';export default withTimeFormat({name: 'MyComponent',props: {formattedDate: {type: String,required: true,},},
});
</script>//在父组件中使用
// ParentComponent.vue
<template><my-component :date="new Date()" format="yyyy年MM月dd日 HH时mm分ss秒" />
</template><script>
import MyComponent from './MyComponent.vue';export default {components: {MyComponent,},
};
</script>
90、
91、
92、
93、
94、
95、
96、
97、
98、
99、