vue问题及理解
1、介绍一下vue2和vue3的区别
- Vue2和Vue3的主要区别体现在双向数据绑定原理、生命周期钩子函数、API、多根节点、性能和体积等方面。
- 双向数据绑定原理:Vue2使用Object.defineProperty实现双向数据绑定,而Vue3则利用ES6的Proxy特性来实现,这提供了对数据的更全面监控,包括对数组和对象的深度监听,而Vue2在处理数组变化时需要使用特定的方法(如this.$set),Vue3则可以直接检测到数组内部的变化。
- 生命周期钩子函数:Vue2的生命周期钩子函数包括beforeCreate、created、beforeMount、mounted等,而Vue3引入了新的生命周期钩子,如setup、onBeforeMount、onMounted等,并且提供了更多的调试钩子,如onRenderTracked和onRenderTriggered。Vue3的生命周期钩子更加模块化和组织化,便于代码的维护和管理。
- API:Vue2采用选项式API,而Vue3引入了组合式API(Composition API),这使得代码更加结构化和可维护。Vue3的API设计更加现代化,支持更好的TypeScript集成,提供了更好的类型支持和代码组织方式。
- 多根节点:Vue2中每个组件只能有一个根节点,而在Vue3中,每个组件可以拥有多个根节点,这为组件的设计提供了更大的灵活性。Vue3通过v-bind="$attrs"允许开发者明确指定属性应该分配给哪个根节点,解决了多根节点情况下的属性分配问题。
- 性能和体积:Vue3在性能上有了显著的提升,包括更快的速度、更小的体积、更高效的组件初始化和更新性能。Vue3通过重写虚拟DOM实现和优化编译模板,提高了性能并减少了最终打包的体积。此外,Vue3还引入了新的特性如fragments、Teleport等,进一步增强了框架的功能性和易用性。
2、介绍一下vue2和vue3响应式原理
- Vue2响应式示例:
var vm = new Vue({ data: { message: 'Hello' } }) // 访问数据属性时,触发响应式系统 var msg = vm.message // 读取 vm.message = 'Hello World' // 设置
- Vue 3响应式示例:
import { reactive } from 'vue' const state = reactive({ message: 'Hello' }) // 访问响应式对象属性时,会自动跟踪依赖 let msg = state.message // 读取 state.message = 'Hello World' // 设置
3、介绍一下v-model的实现原理
v-model
是 Vue.js 中用于双向绑定的一个指令,它可以将 input、textarea 和 select 元素等表单元素的值与 Vue 实例的数据保持同步。v-model
实质上是一个语法糖,它在内部为不同的输入类型提供了不同的value
和input
事件。其中原理很简单,只要就是两部分,一个是事件监听,一个是属性绑定。- v-bind:绑定响应式数据
- 触发 input 事件 并传递数据 (核心和重点)
4、vue2和vue3中v-if和v-for的优先级
- Vue是一个流行的JavaScript框架,它提供了一种响应式的数据绑定机制和便捷的U!组件化方式。在Vue中,v-if和v-for是两个常用的指令,用于控制组件的显示和列表渲染。在Vue2和Vue3中,v-if和v-for的优先级有所不同,需要我们学习和掌握。
- Vue2中v-if的优先级是:v-for的优先级要高于v-ǐ,也就是说,在Vue2中,如果一个组件同时使用了v-if和v-for指令,那么v-for指令会先执行,然后才会判断v-i的条件是否成立。这可能会导致某些不必要的渲染,例如在循环染大量数据时,v-if指令可能会频繁地判断条件,导致性能下降。
- Vue3中v-if和v-for的优先级发生了改变,v-if的优先级高于v-for。这意味着,在Ve3中,如果一个组件同时使用了v-if和v-for指令,那么v-if指令会先执行,然后才会循环渲染数据。这个改变可以提高Ve3的性能,在循环渲染大量数据时,可以避免不必要的渲染,提高页面的应速度。
- 因此,在学习Vue2和Vue3中v-if和v-for的优先级时,我们需要注意这个改变。在Vue2中,应该尽量避免在循环渲染数据时使用v-if指令。在Vue3中,可以更加自由地使用v-if和v-for指令,以达到更高的性能和更好的用户体验。同时,在编写Vue代码时,还需要注意代码的简洁和可维护性,避免出现几余代码和复杂逻辑,以提高代码的可读性和可维护性。
5、介绍一下computed和watch的区别
- 通俗来讲,既能用 computed 实现又可以用 watch 监听来实现的功能,推荐用 computed, 重点在于 computed 的缓存功能 ,computed 计算属性是用来声明式的描述一个值依赖了其它的值,当所依赖的值或者变量 改变时,计算属性也会跟着改变; watch 监听的是已经在 data 中定义的变量,当该变量变化时,会触发 watch 中的方法。
- computed是计算属性;watch是监听,监听data中的数据变化。
- computed支持缓存,当其依赖的属性的值发生变化时,计算属性会重新计算,反之,则使用缓存中的属性值;watch不支持缓存,当对应属性发生变化的时候,响应执行。
- computed不支持异步,有异步操作时无法监听数据变化;watch支持异步操作。
- computed第一次加载时就监听;watch默认第一次加载时不监听。
- immediate设置成true时,第一次加载才会监听 computed中的函数必须调用return;watch不是。
- 使用场景:computed:一个属性受到多个属性影响,如:购物车商品结算。watch:一个数据影响多条数据,如:搜索数据。数据变化响应,执行异步操作,或高性能消耗的操作,watch为最佳选择。
6、介绍一下watch和watchEffet的区别
- watch函数与watchEffect函数都是监听器,在写法和用法上有一定区别,是同一功能的两种不同形态,底层都是一样的。
-
watch显式指定依赖数据,依赖数据更新时执行回调函数具有一定的惰性lazy 第一次页面展示的时候不会执行,只有数据变化的时候才会执行(设置immediate: true时可以变为非惰性,页面首次加载就会执行)监视ref定义的响应式数据时可以获取到原值既要指明监视的属性,也要指明监视的回调
-
watchEffect自动收集依赖数据,依赖数据更新时重新执行自身;立即执行,没有惰性,页面的首次加载就会执行;无法获取到原值,只能得到变化后的值;不用指明监视哪个属性,监视的回调中用到哪个属性就监视哪个属性
-
watchEffect与computed有点像:但是computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。computed若是值没有被使用时不会调用,但是watchEffect始终会调用一次
7、介绍一下vue2和vue3中watch的区别
- 在 Vue2 的 options API 中, watch 与 methods 一样作为一个模块,如果要监听多个变量,要在其中一个一个定义相应的监听事件
- VUE3 的 Composition API可以多次使用 watch 方法,通过多个watch 方法来监听多个对象。
8、vue3中的生命周期钩子发生了哪些变化
- 在Vue3中,一些常见的钩子函数发生了变化。例如,vue2中的created和beforeCreate钩子函数被替换为了setup(),并且setup()在二者之前执行。beforeMount和mounted函数被替换成了onBeforeMount和onMounted。beforeUpdate 和update被替换为 onBeforeUpdate和onupdate。beforeDestroy和destroyed被替换为beforeUnmount和unmounted。这些钩子函数的执行顺序与Vue2的版本相同,但是有所不同的是,在Vue3中,它们是使用ES6类定义的。
- setup 函数是一个全新的组件选项。它是 Composition API 的核心,用于初始化组件实例
- setup 函数接收两个参数:props 和 context。其中 props 是父组件传递给当前组件实例的属性,而 context 则包含了一些 helper 方法和组件选项(如 attrs、slots 和 emit 等)。
- 在 setup 中,我们可以使用 Vue 3 提供的多个工具函数来定义响应式数据、监听生命周期钩子、处理计算属性、声明事件处理函数等。这些函数包括:reactive:用于创建响应式对象ref:用于创建一个单一的响应式值;computed:用于创建计算属性;watch:用于监听响应式数据的变化;onMounted、onUpdated 和 onUnmounted:用于监听生命周期钩子;toRefs:用于将响应式对象转换为普通对象;inject 和 provide:用于跨层级组件传递数据;getCurrentInstance:用于访问当前组件实例;使用 setup 函数的优点是可以将相似的逻辑组织在一起,便于代码的维护和重用。此外,setup 函数需要返回一个对象,用于暴露组件状态和方法给模板使用,因此也提高了代码的可读性和组件的封装性。
onBeforeMount
和onMounted
是 Vue 3 中的生命周期钩子,它们分别在组件挂载之前和之后运行。
<script setup>
import { onBeforeMount,onMounted, reactive } from 'vue'
onBeforeMount(() => {
console.log('Before mount')
})
const state = reactive({
message: ''
})
onMounted(() => {
// 发送 AJAX 请求,获取数据
fetch('/api/data')
.then(res => res.json())
.then(data => {
state.message = data.message
})
})
</script>
onBeforeUpdate
和onUpdated
是 Vue 3 中的生命周期钩子函数,它们分别在组件更新之前和之后运行。<script setup> import { onBeforeUpdate,onUpdated, ref } from 'vue' let count = 1 onBeforeUpdate(() => { console.log('Before update', count) }) const handleClick = () => { count++ } onMounted(() => { // 模拟异步获取消息 setTimeout(() => { message.value = 'Hello, Vue 3!' }, 2000) }) onUpdated(() => { console.log('DOM updated') }) const handleClick = () => { alert(message.value) } </script> <template> <div> <p>{{ count }}</p> <button @click="handleClick">增加</button> </div> </template>
- 当组件不再需要时,Vue3将依次执行beforeUnmount和unmounted钩子函数。beforeUnmount钩子函数在组件卸载之前被调用,通常用于清理一些事件监听器或取消一些异步任务。unmounted钩子函数在组件完全被卸载后被调用,此时,组件可以回收内存等资源。
<script setup> import { onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue' const timer = ref(null) onBeforeUnmount(() => { clearInterval(timer.value) }) const startTimer = () => { timer.value = setInterval(() => { console.log('Hello, world!') }, 1000) } const stopTimer = () => { clearInterval(timer.value) } const message = ref('') let subscription = null onMounted(() => { // 模拟创建一个订阅 subscription = setInterval(() => { message.value = new Date().toLocaleTimeString() }, 1000) }) onUnmounted(() => { // 在组件卸载后取消订阅 clearInterval(subscription) }) const unsubscribe = () => { clearInterval(subscription) } </script> <template> <div> <p>定时器示例</p> <button @click="startTimer">开始</button> <button @click="stopTimer">停止</button> </div> </template>
9、router跳转和a标签跳转的区别
- <a> 标签定义超链接,用于从一张页面链接到另一张页面。从一张页面跳转到另一张页面,但从这里来说就违背了多视图的单页Web应用这个概念。通过a标签进行跳转,页面会被重新渲染,即相当于重新打开一个新的网页,体现为视觉上的“闪烁”(如果是本地的项目基本看不出来)
-
<router-link> 组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to 属性指定目标地址,可以通过配置 tag 属性生成别的标签。通过 router-link 进行跳转不会跳转到新的页面,也不会重新渲染,它会选择路由所指的组件进行渲染,避免了重复渲染的“无用功”。
-
对比<a>,router-link 组件避免了不必要的重渲染,它只更新变化的部分从而减少DOM性能消耗。Vue的创新之处在于,它利用虚拟 DOM 的概念和 diff 算法实现了对页面的"按需更新",Vue-router很好地继承了这一点,重渲染是我们不希望看到的,因为无论跳转到哪个页面,只需要渲染一次就够了。反观<a>标签,每次跳转都得重渲染一次。
10、为什么data会是一个函数
-
在前端开发中,特别是在使用Vue.js框架时,"data" 通常是一个函数,而不是一个变量。这是因为在Vue组件中,"data" 是一个函数,用于返回包含组件状态的对象。这样做有以下几个原因:
-
避免数据冲突:如果 "data" 是一个对象字面量,那么在多个组件中,如果有相同的属性名,它们将共享同一个数据。这可能会导致数据冲突和不正确的状态更新。通过将 "data" 作为一个函数,每次创建一个新的组件实例时,都会调用该函数,从而返回一个新的、独立的状态对象。
-
更好的封装和组件化:通过将 "data" 作为一个函数,可以将组件的状态封装在函数内部,使得组件的逻辑更加清晰和模块化。这样也使得组件的测试更加容易,因为你可以更容易地模拟和重置组件的状态。
-
更好的性能:由于 "data" 是一个函数,而不是一个对象字面量,Vue可以更好地跟踪依赖关系和变化。这有助于提高Vue的响应性和性能。
11、说一下你对diff算法的理解
比较新旧节点的差异:diff算法首先对比新旧虚拟DOM树的节点,找出它们之间的差异。
生成差异操作(patch):然后,diff算法会生成一组操作(也称为patch),这些操作可以将旧的虚拟DOM树转变为新的虚拟DOM树。
应用差异操作更新DOM树:最后,这些操作被应用到实际的DOM树上,从而更新视图。
- diff算法的递归遍历实现方法:
Vue使用递归遍历的方式执行diff算法。当发现节点不再存在时,将其标记为删除;当发现新节点时,将其标记为添加;当节点仍然存在但内容改变时,将其标记为更新。
- diff算法优化:原地复用、按key值比较、采用双端队列等方式进行优化
Vue采用多种策略来优化diff算法,包括原地复用节点、按key值比较节点以及采用双端队列等方式。这些优化策略有助于提高性能,减少不必要的DOM操作。
12、diff算法的优势是什么
-
diff算法的优势主要体现在提高DOM更新的效率上。
-
diff算法,特别是在虚拟DOM中采用,通过将树形结构按照层级分解,只比较同级元素,不同层级的节点只有创建和删除操作。这种算法的设计原则是深度优先,同层比较,并且比较只会在同层级进行,不会跨层级比较。这意味着,当虚拟DOM更新时,diff算法能够精确地识别出哪些部分需要更新,避免了不必要的DOM操作,从而提高了更新的效率。
-
具体来说,diff算法的工作流程包括:将新旧虚拟DOM节点数组进行比较,通常通过定义四个指针(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx)分别指向两个数组的头部和尾部。通过循环遍历这两个数组,尝试找到可以复用的节点,减少不必要的DOM操作。在比较过程中,循环从两边向中间收拢,进一步优化性能。
js问题及理解
1、说一下宏任务和微任务
- 宏任务与微任务是什么?宏任务与微任务表示异步任务的两种分类
- 宏任务:包括整体代码script(可以理解为外层同步代码)、settimeout、setInterval、i/o、ui render
- 微任务:promise、object.observe、MutationObserver(监听DOM树的变化)、process.nextTick
- 因为异步任务放在队列中,自然而然宏任务与微任务就存放在宏任务队列与微任务队列中代码的执行顺序:先执行渲染主线程中的同步代码,那异步任务怎样执行的?
- 先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕(事件循环EventLoop)。
- 简言之就是放入队列时宏任务、微任务不分优先级,但是将队列中的任务拿到执行栈中执行微任务优先(当微任务队列的所有任务全部执行完后,才开始执行宏任务)
2、你如何使用es6的新特性实现数组去重呢?
- 使用 Set 数据结构:因为该结构中的元素是唯一的,不允许重复;
Set
对象和展开运算符...
来实现数组去重。const array = [1, 2, 3, 2, 4, 1]; const uniqueArray = [...new Set(array)]; console.log(uniqueArray); // 输出: [1, 2, 3, 4]
- 使用 filter() 方法:遍历数组,筛选出不重复的元素
const array = [1, 2, 2, 3, 4, 4, 5]; const uniqueArray = array.filter((element, index, self) => { return self.indexOf(element) === index; }); console.log(uniqueArray); // 输出 [1, 2, 3, 4, 5]
- 使用 reduce() 方法:遍历数组,将不重复的元素添加到累积的结果数组中
const array = [1, 2, 2, 3, 4, 4, 5]; const uniqueArray = array.reduce((accumulator, currentValue) => { if (!accumulator.includes(currentValue)) { accumulator.push(currentValue); } return accumulator; }, []); console.log(uniqueArray); // 输出 [1, 2, 3, 4, 5]
- for 循环嵌套,利用 splice 去重
function uniqueArr(arr) { for (let i = 0; i < arr.length; i++) { for (let j = i + 1; j < arr.length; j++) { if (arr[i] == arr[j]) { arr.splice(j, 1) j-- } } } return arr } console.log(uniqueArr([1, 1, 2, 2, 3, 4, 5, 5, 4, 3, 2, 1])) // [1, 2, 3, 4, 5] console.log(uniqueArr([1, 1, 2, 5, 6, 3, 5, 5, 6, 8, 9, 8])) // [1, 2, 5, 6, 3, 8, 9]
- 新建数组,利用 includes 去重
function uniqueArr(arr) { let resArr = [] arr.forEach(item => { if (!res.includes(item)) { res.push(item) } }); return resArr } console.log(uniqueArr([1, 1, 2, 2, 3, 4, 5, 5, 4, 3, 2, 1])) // [1, 2, 3, 4, 5]
- 先用 sort 排序,然后用一个指针从第0位开始,配合 while 循环去重
function uniqueArr(arr) { arr.sort() let pointer = 0 while (arr[pointer]) { if (arr[pointer] == arr[pointer + 1]) { // 删除下一项 arr.splice(pointer + 1, 1) } else { // 指针往下移 pointer++ } } return arr } console.log(uniqueArr([1, 1, 2, 2, 3, 4, 5, 5, 4, 3, 2, 1])) console.log(uniqueArr([1, 1, 2, 5, 6, 3, 5, 5, 6, 8, 9, 8]))
3、后端返回给你的res可能是null和[],你要如何区分呢?
4、说一下Map的定义方式
- 方法一:使用对象字面量表达式
var map = { key1: value1, key2: value2, // 添加更多的键值对... };
- 方法二:使用
Map
类var map = new Map(); map.set(key1, value1); map.set(key2, value2);
- 方法三:使用二维数组
var map = [[key1, value1], [key2, value2]];
- 方法四:使用
Object.create()
方法。这种方法使用Object.create()
方法创建一个没有原型链的对象,并在该对象上添加键值对。var map = Object.create(null); map[key1] = value1; map[key2] = value2;
5、Object的哪些方法用的比较多
- toString():用于将对象转换为字符串表示形式,使得对象可以被打印或输出。
- equals(Object obj):用于比较两个对象是否相等,通常需要重写以实现自定义对象的相等逻辑。
- hashCode():返回对象的哈希码值,通常与哈希表结构一起使用,用于快速查找对象。
- getClass():返回对象的运行时类,用于获取对象的类信息。
- clone():创建并返回对象的一个拷贝,用于复制对象。
- wait(), notify(), notifyAll():这些方法用于线程间的同步和通信,实现多线程环境下的等待、唤醒操作。
6、说一下forEach、for in、for of的区别
forEach
是Array数组的一个方法,用于遍历数组中的每个元素。这个方法对数组的每个元素执行一次提供的函数。这个函数接受三个参数:元素值、元素索引和数组本身。- 注意:
forEach
无法提前结束遍历,而且你不能使用break
、continue
和return
来控制forEach
。
javascriptlet array = [1, 2, 3, 4, 5];
array.forEach(function(value, index, array) {
console.log(value); // 当前元素值
console.log(index); // 当前元素索引
console.log(array); // 数组本身
});
for...in
语句用于遍历对象的可枚举属性,包括从原型链上继承的属性。它遍历的是对象的属性名,而不是属性值。在遍历数组时,它遍历的是数组索引。- 注意:
for...in
不仅遍历数组自身的属性,还会遍历它从原型链上继承的属性。因此,如果数组原型链上有可枚举属性,for...in
也会遍历到这些属性。
javascriptlet array = [1, 2, 3, 4, 5];
for (let index in array) {
console.log(index); // 当前元素索引
console.log(array[index]); // 当前元素值
}
for...of
语句在可迭代对象(包括数组、字符串、Set和Map等)上创建一个迭代循环,它遍历的是可迭代对象的值。- 注意:
for...of
只遍历可迭代对象自身的值,不会遍历原型链上的值。
let array = [1, 2, 3, 4, 5];
for (let value of array) {
console.log(value); // 当前元素值
}
forEach
主要用于遍历数组,且不能提前结束遍历。for...in
主要用于遍历对象的可枚举属性,包括从原型链上继承的属性。在遍历数组时,它遍历的是数组索引。for...of
主要用于遍历可迭代对象的值,它不会遍历原型链上的值。
css问题及理解
1、css如何实现响应式布局
- 使用flex布局,优点是代码简单、布局方便;
- 使用绝对布局,结合使用media可以实现响应式布局;
- 使用grid布局,优点是写法简便;
- 使用float布局,优点是兼容性比较好。
2、如果一个老项目交到你手里,项目里的尺寸都写死了,你要怎么把它转化成响应式布局呢
- 媒体查询(Media Queries):使用CSS的媒体查询功能,可以根据设备的特定条件(如屏幕宽度)应用不同的CSS样式。例如,你可以为不同的屏幕尺寸设置不同的背景颜色,如上述示例所示,通过定义不同的媒体查询条件来改变页面的布局和样式。
- 弹性布局:利用CSS的Flexbox或Grid布局系统,可以创建灵活的页面布局,这些布局系统能够根据屏幕尺寸自动调整元素的大小和位置。
- 使用响应式框架:考虑使用成熟的响应式框架,如Bootstrap,这些框架通常提供了预定义的类,可以简化响应式布局的开发。例如,Bootstrap 4及更高版本提供了灵活的网格系统,可以轻松地创建响应式布局。
- 避免使用固定尺寸:在CSS中避免使用固定的像素尺寸,而应使用相对单位(如百分比、vw/vh单位、em或rem等),这样元素的尺寸可以根据屏幕尺寸的变化而相应调整。
- 测试与调整:在不同的设备和屏幕尺寸上进行测试,确保布局在各种条件下都能良好地显示。根据测试结果调整CSS规则,以达到最佳的响应式效果。
- 重构或优化代码:如果项目的HTML和CSS代码结构较为复杂或不够优化,可能需要进行一定的重构工作,以确保响应式设计的实现更加高效和可靠。