写点什么

美团前端必会 vue 面试题合集

作者:bb_xiaxia1998
  • 2023-02-20
    浙江
  • 本文字数:9338 字

    阅读完需:约 31 分钟

delete 和 Vue.delete 删除数组的区别

  • delete 只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。

  • Vue.delete 直接删除了数组 改变了数组的键值。

Vue3 有了解过吗?能说说跟 vue2 的区别吗?

1. 哪些变化



从上图中,我们可以概览Vue3的新特性,如下:


  • 速度更快

  • 体积减少

  • 更易维护

  • 更接近原生

  • 更易使用


1.1 速度更快


vue3相比vue2


  • 重写了虚拟Dom实现

  • 编译模板的优化

  • 更高效的组件初始化

  • undate性能提高 1.3~2 倍

  • SSR速度提高了 2~3 倍



1.2 体积更小


通过webpacktree-shaking功能,可以将无用模块“剪辑”,仅打包需要的


能够tree-shaking,有两大好处:


  • 对开发人员,能够对vue实现更多其他的功能,而不必担忧整体体积过大

  • 对使用者,打包出来的包体积变小了


vue可以开发出更多其他的功能,而不必担忧vue打包出来的整体体积过多



1.3 更易维护


compositon Api


  • 可与现有的Options API一起使用

  • 灵活的逻辑组合与复用

  • Vue3模块可以和其他框架搭配使用



更好的 Typescript 支持


VUE3是基于typescipt编写的,可以享受到自动的类型定义提示



1.4 编译器重写



1.5 更接近原生


可以自定义渲染 API



1.6 更易使用


响应式 Api 暴露出来



轻松识别组件重新渲染原因



2. Vue3 新增特性


Vue 3 中需要关注的一些新功能包括:


  • framents

  • Teleport

  • composition Api

  • createRenderer


2.1 framents


Vue3.x 中,组件现在支持有多个根节点


<!-- Layout.vue --><template>  <header>...</header>  <main v-bind="$attrs">...</main>  <footer>...</footer></template>
复制代码


2.2 Teleport


Teleport 是一种能够将我们的模板移动到 DOMVue app 之外的其他位置的技术,就有点像哆啦 A 梦的“任意门”


vue2中,像 modals,toast 等这样的元素,如果我们嵌套在 Vue 的某个组件内部,那么处理嵌套组件的定位、z-index 和样式就会变得很困难


通过Teleport,我们可以在组件的逻辑位置写模板代码,然后在 Vue 应用范围之外渲染它


<button @click="showToast" class="btn">打开 toast</button><!-- to 属性就是目标位置 --><teleport to="#teleport-target">    <div v-if="visible" class="toast-wrap">        <div class="toast-msg">我是一个 Toast 文案</div>    </div></teleport>
复制代码


2.3 createRenderer


通过createRenderer,我们能够构建自定义渲染器,我们能够将 vue 的开发模型扩展到其他平台


我们可以将其生成在canvas画布上



关于createRenderer,我们了解下基本使用,就不展开讲述了


import { createRenderer } from '@vue/runtime-core'
const { render, createApp } = createRenderer({ patchProp, insert, remove, createElement, // ...})
export { render, createApp }
export * from '@vue/runtime-core'
复制代码


2.4 composition Api


composition Api,也就是组合式api,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理



关于compositon api的使用,这里以下图展开



简单使用:


export default {    setup() {        const count = ref(0)        const double = computed(() => count.value * 2)        function increment() {            count.value++        }        onMounted(() => console.log('component mounted!'))        return {            count,            double,            increment        }    }}
复制代码


3. 非兼容变更


3.1 Global API


  • 全局 Vue API 已更改为使用应用程序实例

  • 全局和内部 API 已经被重构为可 tree-shakable


3.2 模板指令


  • 组件上 v-model 用法已更改

  • <template v-for>和 非 v-for节点上key用法已更改

  • 在同一元素上使用的 v-ifv-for 优先级已更改

  • v-bind="object" 现在排序敏感

  • v-for 中的 ref 不再注册 ref 数组


3.3 组件


  • 只能使用普通函数创建功能组件

  • functional 属性在单文件组件 (SFC)

  • 异步组件现在需要 defineAsyncComponent 方法来创建


3.4 渲染函数


  • 渲染函数API改变

  • $scopedSlots property 已删除,所有插槽都通过 $slots 作为函数暴露

  • 自定义指令 API 已更改为与组件生命周期一致

  • 一些转换class被重命名了:

  • v-enter -> v-enter-from

  • v-leave -> v-leave-from

  • 组件 watch 选项和实例方法 $watch不再支持点分隔字符串路径,请改用计算函数作为参数

  • Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x 现在使用应用程序容器的 innerHTML


3.5 其他小改变


  • destroyed 生命周期选项被重命名为 unmounted

  • beforeDestroy 生命周期选项被重命名为 beforeUnmount

  • [prop default工厂函数不再有权访问 this 是上下文

  • 自定义指令 API 已更改为与组件生命周期一致

  • data 应始终声明为函数

  • 来自 mixindata 选项现在可简单地合并

  • attribute 强制策略已更改

  • 一些过渡 class 被重命名

  • 组建 watch 选项和实例方法 $watch不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。

  • <template> 没有特殊指令的标记 (v-if/else-if/elsev-forv-slot) 现在被视为普通元素,并将生成原生的 <template> 元素,而不是渲染其内部内容。

  • Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x 现在使用应用容器的 innerHTML,这意味着容器本身不再被视为模板的一部分。


3.6 移除 API


  • keyCode 支持作为 v-on 的修饰符

  • $on$off$once 实例方法

  • 过滤filter

  • 内联模板 attribute

  • $destroy 实例方法。用户不应再手动管理单个Vue 组件的生命周期。

Vue 中封装的数组方法有哪些,其如何实现页面更新

在 Vue 中,对响应式处理利用的是 Object.defineProperty 对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行 hack,让 Vue 能监听到其中的变化。 那 Vue 是如何实现让这些数组方法实现元素的实时更新的呢,下面是 Vue 中对这些方法的封装:


// 缓存数组原型const arrayProto = Array.prototype;// 实现 arrayMethods.__proto__ === Array.prototypeexport const arrayMethods = Object.create(arrayProto);// 需要进行功能拓展的方法const methodsToPatch = [  "push",  "pop",  "shift",  "unshift",  "splice",  "sort",  "reverse"];
/** * Intercept mutating methods and emit events */methodsToPatch.forEach(function(method) { // 缓存原生数组方法 const original = arrayProto[method]; def(arrayMethods, method, function mutator(...args) { // 执行并缓存原生数组功能 const result = original.apply(this, args); // 响应式处理 const ob = this.__ob__; let inserted; switch (method) { // push、unshift会新增索引,所以要手动observer case "push": case "unshift": inserted = args; break; // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。 case "splice": inserted = args.slice(2); break; } // if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听 // notify change ob.dep.notify();// 通知依赖更新 // 返回原生数组方法的执行结果 return result; });});
复制代码


简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用 notify,通知渲染 watcher,执行 update。

slot 是什么?有什么作用?原理是什么?

slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。


  • 默认插槽:又名匿名查抄,当 slot 没有指定 name 属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。

  • 具名插槽:带有具体名字的插槽,也就是带有 name 属性的 slot,一个组件可以出现多个具名插槽。

  • 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。


实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?

受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性,那框架本身是如何实现的呢?


我们查看对应的 Vue 源码:vue/src/core/instance/index.js


export function set (target: Array<any> | Object, key: any, val: any): any {  // target 为数组    if (Array.isArray(target) && isValidArrayIndex(key)) {    // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误    target.length = Math.max(target.length, key)    // 利用数组的splice变异方法触发响应式      target.splice(key, 1, val)    return val  }  // key 已经存在,直接修改属性值    if (key in target && !(key in Object.prototype)) {    target[key] = val    return val  }  const ob = (target: any).__ob__  // target 本身就不是响应式数据, 直接赋值  if (!ob) {    target[key] = val    return val  }  // 对属性进行响应式处理  defineReactive(ob.value, key, val)  ob.dep.notify()  return val}
复制代码


我们阅读以上源码可知,vm.$set 的实现原理是:


  • 如果目标是数组,直接使用数组的 splice 方法触发相应式;

  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

谈谈对 keep-alive 的了解

keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性 include/exclude ,2个生命周期 activated deactivated


参考 前端进阶面试题详细解答

vue 如何监听对象或者数组某个属性的变化

当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为 Object.defineProperty()限制,监听不到变化。


解决方式:


  • this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么 value)


this.$set(this.arr, 0, "OBKoro1"); // 改变数组this.$set(this.obj, "c", "OBKoro1"); // 改变对象
复制代码


  • 调用以下几个数组的方法


splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
复制代码


vue 源码里缓存了 array 的原型链,然后重写了这几个方法,触发这几个方法的时候会 observer 数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使用 splice 方法会比较好自定义,因为 splice 可以在数组的任何位置进行删除/添加操作


vm.$set 的实现原理是:


  • 如果目标是数组,直接使用数组的 splice 方法触发相应式;

  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

常见的事件修饰符及其作用

  • .stop:等同于 JavaScript 中的 event.stopPropagation() ,防止事件冒泡;

  • .prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);

  • .capture :与事件冒泡的方向相反,事件捕获由外到内;

  • .self :只会触发自己范围内的事件,不包含子元素;

  • .once :只会触发一次。

Vue 组件间通信有哪几种方式?

​ Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。


(1)props / $emit 适用 父子组件通信


这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。


(2)ref$parent / $children 适用 父子组件通信


  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

  • $parent / $children:访问父 / 子实例


(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信


这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。


(4)$attrs/$listeners 适用于 隔代组件通信


  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件


(5)provide / inject 适用于 隔代组件通信


祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。


(6)Vuex 适用于 父子、隔代、兄弟组件通信


Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。


  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

简述 mixin、extends 的覆盖逻辑

(1)mixin 和 extends mixin 和 extends 均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。


  • mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。

  • extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。


(2)mergeOptions 的执行过程


  • 规范化选项(normalizeProps、normalizelnject、normalizeDirectives)

  • 对未合并的选项,进行判断


if (!child._base) {  if (child.extends) {    parent = mergeOptions(parent, child.extends, vm);  }  if (child.mixins) {    for (let i = 0, l = child.mixins.length; i < l; i++) {      parent = mergeOptions(parent, child.mixins[i], vm);    }  }}
复制代码


  • 合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里。

  • 返回合并结果 options。

mixin 和 mixins 区别

mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。


Vue.mixin({  beforeCreate() {    // ...逻辑        // 这种方式会影响到每个组件的 beforeCreate 钩子函数  },});
复制代码


虽然文档不建议在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。


mixins 应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。

computed 的实现原理

computed 本质是一个惰性求值的观察者。


computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。


其内部通过 this.dirty 属性标记计算属性是否需要重新求值。


当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,


computed watcher 通过 this.dep.subs.length 判断有没有订阅者,


有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)


没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

MVC 和 MVVM 区别

MVC


MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范


  • Model(模型):是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据

  • View(视图):是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的

  • Controller(控制器):是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据


MVC 的思想:一句话描述就是 Controller 负责将 Model 的数据用 View 显示出来,换句话说就是在 Controller 里面把 Model 的数据赋值给 View。


MVVM


MVVM 新增了 VM 类


  • ViewModel 层:做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。


MVVM 与 MVC 最大的区别就是:它实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应 Vue 数据驱动的思想)


整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性


注意:Vue 并没有完全遵循 MVVM 的思想 这一点官网自己也有说明


那么问题来了 为什么官方要说 Vue 没有完全遵循 MVVM 思想呢?


  • 严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了 $refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。

keep-alive 使用场景和原理

keep-alive 是 Vue 内置的一个组件,可以实现组件缓存,当组件切换时不会对当前组件进行卸载。


  • 常用的两个属性 include/exclude,允许组件有条件的进行缓存。

  • 两个生命周期 activated/deactivated,用来得知当前组件是否处于活跃状态。

  • keep-alive 的中还运用了 LRU(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰。

了解 nextTick 吗?

异步方法,异步渲染最后一步,与 JS 事件循环联系紧密。主要使用了宏任务微任务(setTimeoutpromise那些),定义了一个异步方法,多次调用nextTick会将方法存入队列,通过异步方法清空当前队列。

v-show 与 v-if 有什么区别?

v-if真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。


v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。


所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

Vue 的基本原理

当一个 Vue 实例创建时,Vue 会遍历 data 中的属性,用 Object.defineProperty(vue3.0 使用 proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

子组件可以直接改变父组件的数据吗?

子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。


Vue 提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。


只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

什么是 mixin ?

  • Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。

  • 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。

  • 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。

Vue3.x 响应式数据原理

Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。


proxy 基本用法


// proxy默认只会代理第一层对象,只有取值再次是对象的时候再次代理,不是一上来就代理,提高性能。不像vue2.x递归遍历每个对象属性let handler = {    set(target, key, value) {        return Reflect.set(target, key, value);    },    get(target, key) {        if (typeof target[key] == 'object' && target[key] !== null) {            return new Proxy(target[key], handler); // 懒代理,只有取值再次是对象的时候再次代理,提高性能        }        return Reflect.get(target, key);    }}let obj = { school: { name: 'poetry', age: 20 } };let proxy = new Proxy(obj, handler);
// 返回对象的代理proxy.school
复制代码


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
美团前端必会vue面试题合集_Vue_bb_xiaxia1998_InfoQ写作社区