一些常见的vue2 / vue3面试题汇总,后续不断更新。。。

421 阅读17分钟

§ vue2和vue3的区别以及vue3的新特性:

1.重写双向数据绑定

2.VDOM性能瓶颈

3.Fragment

在Vue2中: 组件必须有一个根标签
在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
好处: 减少标签层级, 减小内存占用

4.Tree-shaking的支持

5.Composition API

§ vue中v-model数据双向绑定原理:

v-model的作用是更新数据,视图发生变化,我们可以用v-bind把value绑定到msg上,当用户更新数据的时候,我们可以监听input事件,input事件和chang事件有什么区别呢?input事件当用户输入的时候就会触发,chang事件是当用户输入完成之后触发,然后用window.addeventlistener监听用户输入事件,然后把用户输入的内容绑定到msg上,这样的话,v-model整个就实现了

§ ref和reactive的区别:

ref:

通过Object.defineProperty()给value的属性添加getter、setter来实现响应式,一般用来处理基本数据类型,也能处理复杂数据类型,只不过内部会自动将对象转换为reactive的代理对象,在js中要加.value,在模版中不需要

reactive:

通过Proxy对目标对象中的所有属性动态地进行数据劫持,并通过Reflect操作对象内部数据来实现响应式,一般用来处理复杂数据类型,会实现递归深度响应式

§ watch和watchEffect的区别:

这两个函数都能监听数据的变化,但从实用角度上讲,他们十分类似,都能通过监听数据变化来触发回调函数

watchEffect会自动追踪函数内部使用的数据变化,当数据变化时,触发回调函数,所以watchEffect更智能

watch是需要指定被监听的数据,当被指定的数据发生变化时,触发回调函数

watchEffect的函数会立即执行一次,并在数据发生变化的时候再次执行

watch的回调函数只有在侦听的数据发生变化的时候才会执行,默认不会立即执行

watch可以更精细的控制监听行为,比如设置deep,immediate,flush

watchEffect更适合简单的场景,不需要额外的配置,相当于默认开启了deep和immediate

watch() 和 watchEffect() 通过 stop() 来停止侦听

const stop = watch(source, callback) 

// 当已不再需要该侦听器时:
stop()

watch() 和 watchEffect() 通过 onCleanup() 来清除副作用

watch(id, async (newId, oldId, onCleanup) => {
    const { response, cancel } = doAsyncWork(newId)
    // 当 `id` 变化时,`cancel` 将被调用
    // 取消之前的未完成的请求
    onCleanup(cancel)
    data.value = await response
})

§ watch和computed的区别和应用场景:

区别:

都是观察数据变化的(相同)

计算属性将会混入到 vue 的实例中,所以需要监听自定义变量;watch 监听 data 、props 里面数据的变化;

computed 有缓存,它依赖的值变了才会重新计算,watch 没有;

watch 支持异步,computed 不支持;

watch 是一对多(监听某一个值变化,执行对应操作);computed 是多对一(监听属性依赖于其他属性)

watch 监听函数接收两个参数,第一个是最新值,第二个是输入之前的值;

computed 属性是函数时,都有 get 和 set 方法,默认走 get 方法,get 必须有返回值(return)

watch 的参数:

  • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
  • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器
  • flush:调整回调函数的刷新时机。参考回调的刷新时机及 watchEffect()
  • onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器
  • once:回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。

computed 缓存原理:

conputed本质是一个惰性的观察者;当计算数据存在于 data 或者 props里时会被警告;

vue 初次运行会对 computed 属性做初始化处理(initComputed),初始化的时候会对每一个 computed 属性用

watcher 包装起来 ,这里面会生成一个 dirty 属性值为 true;然后执行 defineComputed 函数来计算,计算之后会将 dirty 值变为 false,这里会根据 dirty 值来判断是否需要重新计算;如果属性依赖的数据发生变化,computed 的 watcher 会把 dirty 变为 true,这样就会重新计算 computed 属性的值。

§ vue 中 nextTick(() => {}) 作用和原理:

nextTick方法将回调延迟到下次DOM更新循环之后执行。Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。

nextTick 的核心是运用了 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微任务和宏任务,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列

首先我再讲一下原生JS原型链:

原型链的本质是一个链表,当你new一个构造函数的时候,它会返回一个实例,当你在实例上面没找到,它会顺着它的proto属性指向它的原型,去它的原型上去找,如果原型上没找到,它会顺着它的原型的原型去找,一直找到大Object的原型,原型链的终点是 null,因为Object是构造函数,所以原型链终点是Object.prototype.proto,也就是null

它的应用场景是比如jquery源码,它所有的源码都放在了$原型上,以便于我们每个文件都能使用,以及vue2本身不支持数组的双向绑定,所以vue的作者改变了源码,通过原型链的继承属性,来让push支持双向绑定

然后我再讲一下原生JS的运行机制:

原生JS执行是单线程的,基于事件循环。事件循环大致分为以下步骤:

1.所有同步任务都在主线程上执行,形成一个执行栈。

2.异步任务放进任务队列,异步任务分为宏任务和微任务

3.执行栈所有同步任务执行完成,就会执行任务队列。对应的异步任务,结束等待状态,进入执行栈,开始执行。

4.主线程不断重复上面的第三步。

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task(宏任务) 结束后,都要清空所有的 micro task(微任务)。

宏任务: script、setTimeout、setInterval、Node中的setImmediate 等
微任务: Promise.then、MutationObserver、Node 中的 Process.nextTick等

原生JS事件循环:

JS事件循环,Event Loop

1.同步任务和异步任务分别进入不同的执行场所,同步任务进入主线程,异步任务进入Event Table并且注册回调函数

2.当执行的事情完成之后,EventTable会将这个函数植入任务队列,等待主线程的任务执行完毕

3.当栈中的代码执行完毕,执行栈中的任务为空时,就会读取任务的回调

4.如此循环,就形成了事件循环的机制

原生JS事件表格

JS事件表格,Event Table

1.Event Table 可以理解为一张事件和回调函数的对应表

2.Event Table 用来存储JS中的异步事件以及对应的回调函数列表用的

3.当执行的事件完成时,Event Table会将这个回调函数移入宏任务队列或微任务队列

在事件循环中,微任务比宏任务具有更高的优先级,因此微任务会先于宏任务执行。

具体来说,当一个宏任务执行完毕后,会立即检查微任务队列中是否有待执行的微任务。如果有,则按照先进先出的顺序依次执行所有的微任务,直到微任务队列为空。然后才会执行下一个宏任务。

console.log('start');

setTimeout(function() {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
    console.log('Promise');
});

console.log('end');

执行顺序是:

执行同步代码,输出 start

遇到setTimeout,将其回调函数放入宏任务队列;

遇到Promise,将其回调函数放入微任务队列;

输出 end

当前宏任务执行完毕,执行微任务队列中的所有微任务,输出 Promise

执行浏览器UI线程的渲染工作;

检查是否有Web Worker任务,如果有则执行;

执行下一个宏任务,取出setTimeout的回调函数,输出 setTimeout

§ vue2和vue3生命周期函数的区别:

vue3的setup语法糖没有beforeCreate和created,直接用setup代替

onBeforeMont:创建前 => 在onBeforeMont读不到dom,因为这个时候,dom还没有被渲染

onMounted:创建后 => 在onMounted可以读到dom,因为这个时候,dom已经被渲染

onBeforeUpdate:更新前 => 在更新组件中获取dom,onBeforeUpdate获取到的同样也是更新之前的dom

onUpdated:更新后 => 在更新组件中获取dom,onUpdated获取到的才是更新之后的dom

onBeforeUnmount:销毁前

onUnmounted:销毁后

还有用于调试的两个生命周期函数:

onRenderTracked:默认先执行

onRenderTriggered:当触发更新生命周期函数的时候执行并返回更新之后的数据

§ vue父子组件生命周期执行顺序:

父组件:beforeCreate -> created -> beforeMount

子组件:beforeCreate -> created -> beforeMount -> mounted

父组件:mounted

更新过程中:

父组件:beforeUpdate

子组件:beforeUpdate -> updated

父组件:updated

销毁过程中:

父组件/子组件:beforeDestroy

父组件/子组件:destroyed

§ vue2和vue3的响应式原理:

vue2:Object.defineProperty()

在依赖和收集中有三个非常核心的类,分别是Observer,watcher,dep,当组件实例化的时候,Observer也会进行实例化,它将会对data中的属性进行遍历,用defineProperty对每一个属性进行劫持,然后针对每一个属性会生成一个该属性对应的dep,并且在get中调用dep.depend方法,将dep实例和watch实例,并且用一个数组subs进行关联,在set里调用dep.notify,notify将对subs进行遍历,执行watcher中绑定的render来更新组件

vue3:Proxy()

Proxy是ES6新增的方法,主要用于创建一个对象的代理,从而实现基本操作的拦截和定义

  • Object.defineProperty只能遍历对象属性进行劫持

  • Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的

  • Proxy可以直接监听数组的变化(push、shift、splice)

  • Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等,这是Object.defineProperty不具备的

§ vue中虚拟dom原理,diff算法源码

虚拟dom:

虚拟dom是通过JS生成的一个AST节点树,AST是抽象语法树,之所以使用AST抽象语法树,是因为很多地方都会用到,比如TS转JS,babel插件(ES6转ES5),或者JS通过V8引擎帮我们转字节码的时候,都会用到,当然在Vue中,也是帮我们将dom转换为虚拟dom,为什么不直接操作dom?因为一个dom的属性是非常多的,如果直接操作dom,会十分浪费性能。操作虚拟dom的好处还可以做很多算法的优化,比如比较常见的diff算法

diff算法:

v-for中的key只能是唯一值,不能重复

1.无key的diff算法:

dom会生成两个vnode,一个是新的vnode,一个是旧的vnode,第一步是通过for循环去patch,重新渲染元素,第二步是删除旧的vnode,第三步是新增新的vnode。diff算法会做替换,虚拟dom会做新旧对比,新的会把旧的替换掉,如果发现多了的,就新增并插入,如果发现少了就删除。

2.有key的diff算法:

同样也会进行新旧对比,有key的diff算法一共分为五步: 前置算法,只对比前面的,while循环,通过isSameVNodeType方法进行判断type和key比较是否一样,如果一样,返回true,则复用,其中type就是div,key就是我们绑定的key值,当发现不一样的时候,就会break跳出循环,进行尾序算法对比,前两种算法的作用是头和头,尾和尾进行对比

vue3和vue2的双端diff算法,不同的是,vue2也是头和头,尾和尾,头和尾,尾和头,这是和vue3的区别,vue3进行了diff算法优化,vue3还做了最长递增子序列算法,当头尾比对完之后,发现多了,就会走第三步,第三步是新增,首先还是while循环,通过patch,如果参数为null,就会走新增,如果发现少了,就会通过unmount执行卸载

3.乱序无序等等不可控因素:

1.构建新节点的映射关系:key值对应 0 1 2 3 4 索引,构建map关系

2.记录新节点在旧节点中的位置数组:如果有多余的旧节点,则删除掉,如果新节点不包含旧节点,也给删掉,如果节点出现交叉,说明是移动,并且要去求最长递增子序列,并且moved赋值为true

3.求最长递增子序列升序算法:写了一个getSequence函数方法,实现了贪心算法+二分查找实现最长递增子序列算法,求出来之后判断,如果当前遍历的这个节点不在子序列中,说明要求移动,否则如果在子序列中,那么就直接跳过

§ v-if和v-show的区别:

在 vue2 中 v-for 的优先级更高,但是在 vue3 中优先级改变了。v-if 的优先级更高。

v-show:

给该元素增加style为display:none,但是dom元素还在,当条件切换的时候,只是做css的display:none和block的切换

v-show切换的时候,不会出触发组件的生命周期

v-if:

true或者false,是将dom元素整个添加或者删除

切换过程中会执行组件的编译和卸载过程,并且销毁和重建内部事件监听和子组件

v-if由false切换为true的时候,会触发组件的beforeCrate,create,beforeMount,mounted

v-if由true切换为false的时候,会触发组件的beforeDestory和distoryed

所以v-if做切换的时候会增加性能的消耗

§ vue3的通讯方式

父子组件:defineProps()

子父组件:defineEmit()

子父组件双向绑定:v-model

子父组件 ref 和 defineExpose()

provide/inject 跨级别组件通讯

是通过原型链实现的。作用是在根组件注册一次,在其他的子组件都可以访问到数据

§ vue路由router,哈希和history的区别是什么?

1.哈希类型:

路径会带#,因为是location.hash去匹配的,当我们在控制台输入的时候,会返回一个#/

监听路由变化的原理是通过window.addEventListener的event回调函数去监听左右浏览器回退或前进箭头的变化,event回调函数去返回newUrl和oldUrl,从而实现跳转

window.location.hash = "路由地址"

2.history类型:

路径不带#,因为history是基于h5的history方法实现的

监听路由变化的原理是通过window.addEventListener,有一个popState回调函数,通过event回调函数去监听,event会返回一个state对象,返回的对象里包含back(上一个url地址)和current(当前url地址)

跳转原理是通过history.pushState结合vue内置跳转方法实现跳转的

补充:

vue2:哈希:hash

vue3:哈希:createWebHash

vue2:history:history

vue3:history:createWebHistory

§ vue中key的作用是什么?

在Vue中,key 是一个特殊的属性,它主要用于给通过 v-for 指令渲染的列表中的每个元素或组件分配一个唯一的标识符

这个属性在Vue的虚拟DOM渲染和更新过程中起着至关重要的作用

1. 提供稳定的身份标识

唯一性:key 的值必须是唯一且稳定的,它用于识别每个节点的身份。当使用 v-for 迭代列表时,Vue 使用 key 来跟踪每个节点的身份和状态。 稳定性:使用稳定的 key 值可以帮助 Vue 更准确地判断节点的变化情况,从而减少不必要的DOM操作。

2. 优化渲染性能

高效更新:Vue 在进行虚拟DOM的diff算法比较新旧节点时,如果节点具有相同的 key,Vue 会认为它们是相同的节点,从而避免不必要的重新渲染,提高渲染效率。 复用元素:当列表数据变化时(如添加、删除、重新排序),Vue 会尽量复用现有的DOM元素,而不是重新创建它们。使用 key 可以帮助 Vue 更准确地识别哪些元素是可以复用的,哪些是需要更新的。

3. 保持组件状态

状态保留:在使用 v-for 渲染列表项时,如果列表项包含状态(如输入框的值、复选框的选中状态等),并且列表项的顺序会发生变化,使用 key 可以确保在重新渲染时,Vue 能够正确地保留每个列表项的状态。

4. 需要避免一些问题

错误复用:如果不使用 key 或 key 值不唯一,Vue 在更新列表时可能会错误地复用DOM元素,导致显示错误的数据或状态

要确保key的唯一性:在大多数情况下,可以使用数据的唯一标识符(如id)作为 key

添加key可以增加稳定性:key 的值应该是稳定的,不应频繁变化。避免使用index索引作为 key,特别是在列表会进行动态排序或过滤的情况下

§ vue如果出现重复的key,会怎样

1. 渲染错误

身份混淆:

vue使用key来跟踪每个节点的身份。当key重复时,Vue可能无法准确判断哪些节点是应该被保留、更新还是删除的。这会导致渲染结果出现错误,比如显示的数据与预期不符。

状态错乱:

如果列表项包含状态(如输入框的内容、复选框的选中状态等),且这些列表项通过v-for渲染并使用重复的key,那么当列表更新时,这些状态可能会被错误地应用到其他元素上,导致用户界面的混乱。

2. 性能问题

不必要的DOM操作:

由于Vue无法准确判断节点的变化,它可能会执行不必要的DOM操作来尝试修正错误。这不仅会降低应用的性能,还可能引入更多的bug。

更新效率低下:

在Vue的虚拟DOM(Virtual DOM)算法中,key是优化渲染性能的关键因素之一。当key重复时,Vue无法有效地利用这些优化,导致更新效率低下。

3. 报错提示

开发环境警告:

在开发模式下,Vue会检测到重复的key并发出警告。这有助于开发者及时发现并修复问题。

生产环境潜在风险:

然而,在生产环境中,这些警告可能会被禁用或忽略,导致问题难以被发现和解决。