写点什么

2023 前端一面 vue 面试题合集

作者:yyds2026
  • 2023-02-27
    浙江
  • 本文字数:9816 字

    阅读完需:约 32 分钟

函数式组件优势和原理

函数组件的特点


  1. 函数式组件需要在声明组件是指定 functional:true

  2. 不需要实例化,所以没有this,this通过render函数的第二个参数context来代替

  3. 没有生命周期钩子函数,不能使用计算属性,watch

  4. 不能通过$emit 对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件

  5. 因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement

  6. 函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到$attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)


优点


  1. 由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件

  2. 函数式组件结构比较简单,代码结构更清晰


使用场景:


  • 一个简单的展示组件,作为容器组件使用 比如 router-view 就是一个函数式组件

  • “高阶组件”——用于接收一个组件作为参数,返回一个被包装过的组件


例子


Vue.component('functional',{ // 构造函数产生虚拟节点的    functional:true, // 函数式组件 // data={attrs:{}}    render(h){        return h('div','test')    }})const vm = new Vue({    el: '#app'})
复制代码


源码相关


// functional componentif (isTrue(Ctor.options.functional)) { // 带有functional的属性的就是函数式组件  return createFunctionalComponent(Ctor, propsData, data, context, children)}
// extract listeners, since these needs to be treated as// child component listeners instead of DOM listenersconst listeners = data.on // 处理事件// replace with listeners with .native modifier// so it gets processed during parent component patch.data.on = data.nativeOn // 处理原生事件
// install component management hooks onto the placeholder nodeinstallComponentHooks(data) // 安装组件相关钩子 (函数式组件没有调用此方法,从而性能高于普通组件)
复制代码

双向数据绑定的原理

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:


  1. 需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化

  2. compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

  3. Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个 update()方法 ③待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调,则功成身退。

  4. MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

Vue 组件如何通信?

Vue 组件通信的方法如下:


  • props/$emit+v-on: 通过 props 将数据自上而下传递,而通过 $emit 和 v-on 来向上传递信息。

  • EventBus: 通过 EventBus 进行信息的发布与订阅

  • vuex: 是全局数据管理库,可以通过 vuex 管理全局的数据流

  • $attrs/$listeners: Vue2.4 中加入的$attrs/$listeners可以进行跨级的组件通信

  • provide/inject:以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础


还有一些用 solt 插槽或者 ref 实例进行通信的,使用场景过于有限就不赘述了。

Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。


其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

虚拟 DOM 实现原理?

  • 虚拟 DOM 本质上是 JavaScript 对象,是对真实 DOM 的抽象

  • 状态变更时,记录新树和旧树的差异

  • 最后把差异更新到真正的 dom 中

说说 Vue 的生命周期吧

什么时候被调用?


  • beforeCreate :实例初始化之后,数据观测之前调用

  • created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、 watch/event 事件回调。无 $el .

  • beforeMount:在挂载之前调用,相关 render 函数首次被调用

  • mounted:了被新创建的vm.$el替换,并挂载到实例上去之后调用改钩子。

  • beforeUpdate:数据更新前调用,发生在虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。

  • updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。

  • beforeDestroy:实例销毁前调用,实例仍然可用。

  • destroyed:实例销毁之后调用,调用后,Vue 实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除


每个生命周期内部可以做什么?


  • created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。

  • mounted:实例已经挂载完成,可以进行一些 DOM 操作。

  • beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。

  • updated:可以执行依赖于 DOM 的操作,但是要避免更改状态,可能会导致更新无线循环。

  • destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。


ajax 放在哪个生命周期?:一般放在 mounted 中,保证逻辑统一性,因为生命周期是同步执行的, ajax 是异步执行的。单数服务端渲染 ssr 同一放在 created 中,因为服务端渲染不支持 mounted 方法。 什么时候使用 beforeDestroy?:当前页面使用 $on ,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove


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

$nextTick 是什么?

Vue 实现响应式并不是在数据发生后立即更新 DOM,使用 vm.$nextTick 是在下次 DOM 更新循环结束之后立即执行延迟回调。在修改数据之后使用,则可以在回调中获取更新后的 DOM

diff 算法

答案


时间复杂度: 个树的完全 diff 算法是一个时间复杂度为 O(n*3) ,vue 进行优化转化成 O(n)


理解:


  • 最小量更新, key 很重要。这个可以是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个 DOM 节点

  • 扩展 v-for 为什么要有 key ,没有 key 会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改 DOM),加 key 只会移动减少操作 DOM。

  • 只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。

  • 只进行同层比较,不会进行跨层比较。


diff 算法的优化策略:四种命中查找,四个指针


  1. 旧前与新前(先比开头,后插入和删除节点的这种情况)

  2. 旧后与新后(比结尾,前插入或删除的情况)

  3. 旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)

  4. 旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)


--- 问完上面这些如果都能很清楚的话,基本 O 了 ---


以下的这些简单的概念,你肯定也是没有问题的啦😉

vue 中使用了哪些设计模式

1.工厂模式 - 传入参数即可创建实例


虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode


2.单例模式 - 整个程序有且仅有一个实例


vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉


3.发布-订阅模式 (vue 事件机制)


4.观察者模式 (响应式数据原理)


5.装饰模式: (@装饰器的用法)


6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略


...其他模式欢迎补充

keep-alive 使用场景和原理

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


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

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

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

你有对 Vue 项目进行哪些优化?

(1)代码层面的优化


  • v-if 和 v-show 区分使用场景

  • computed 和 watch 区分使用场景

  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

  • 长列表性能优化

  • 事件的销毁

  • 图片资源懒加载

  • 路由懒加载

  • 第三方插件的按需引入

  • 优化无限列表性能

  • 服务端渲染 SSR or 预渲染


(2)Webpack 层面的优化


  • Webpack 对图片进行压缩

  • 减少 ES6 转为 ES5 的冗余代码

  • 提取公共代码

  • 模板预编译

  • 提取组件的 CSS

  • 优化 SourceMap

  • 构建结果输出分析

  • Vue 项目的编译优化


(3)基础的 Web 技术的优化


  • 开启 gzip 压缩

  • 浏览器缓存

  • CDN 的使用

  • 使用 Chrome Performance 查找性能瓶颈

了解 nextTick 吗?

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

谈谈 Vue 和 React 组件化的思想

  • 1.我们在各个页面开发的时候,会产生很多重复的功能,比如 element 中的 xxxx。像这种纯粹非页面的 UI,便成为我们常用的 UI 组件,最初的前端组件也就仅仅指的是 UI 组件

  • 2.随着业务逻辑变得越来多是,我们就想要我们的组件可以处理很多事,这就是我们常说的组件化,这个组件就不是 UI 组件了,而是包具体业务的业务组件

  • 3.这种开发思想就是分而治之。最大程度的降低开发难度和维护成本的效果。并且可以多人协作,每个人写不同的组件,最后像撘积木一样的把它构成一个页面

Vue3.0 和 2.0 的响应式原理区别

Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。


相关代码如下


import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑import { isObject } from "./util"; // 工具方法
export function reactive(target) { // 根据不同参数创建不同响应式对象 return createReactiveObject(target, mutableHandlers);}function createReactiveObject(target, baseHandler) { if (!isObject(target)) { return target; } const observed = new Proxy(target, baseHandler); return observed;}
const get = createGetter();const set = createSetter();
function createGetter() { return function get(target, key, receiver) { // 对获取的值进行放射 const res = Reflect.get(target, key, receiver); console.log("属性获取", key); if (isObject(res)) { // 如果获取的值是对象类型,则返回当前对象的代理对象 return reactive(res); } return res; };}function createSetter() { return function set(target, key, value, receiver) { const oldValue = target[key]; const hadKey = hasOwn(target, key); const result = Reflect.set(target, key, value, receiver); if (!hadKey) { console.log("属性新增", key, value); } else if (hasChanged(value, oldValue)) { console.log("属性值被修改", key, value); } return result; };}export const mutableHandlers = { get, // 当获取属性时调用此方法 set, // 当修改属性时调用此方法};
复制代码

v-model 的原理?

我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:


  • text 和 textarea 元素使用 value 属性和 input 事件;

  • checkbox 和 radio 使用 checked 属性和 change 事件;

  • select 字段将 value 作为 prop 并将 change 作为事件。


以 input 表单元素为例:


<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
复制代码


如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:


父组件:<ModelChild v-model="message"></ModelChild>
子组件:<div>{{value}}</div>
props:{ value: String},methods: { test1(){ this.$emit('input', '小红') },},
复制代码

Vue 的生命周期方法有哪些

  1. Vue 实例有一个完整的生命周期,也就是从开始创建初始化数据编译模版挂载Dom -> 渲染更新 -> 渲染卸载等一系列过程,我们称这是Vue的生命周期

  2. Vue生命周期总共分为 8 个阶段创建前/后载入前/后更新前/后销毁前/后


beforeCreate => created => beforeMount => Mounted => beforeUpdate => updated => beforeDestroy => destroyedkeep-alive下:activated deactivated



其他几个生命周期



  1. 要掌握每个生命周期内部可以做什么事


  • beforeCreate 初始化vue实例,进行数据观测。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务

  • created 组件初始化完毕,可以访问各种数据,获取接口数据等

  • beforeMount 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上

  • mounted 实例已经挂载完成,可以进行一些DOM操作

  • beforeUpdate 更新前,可用于获取更新前各种状态。此时view层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。

  • updated 完成view层的更新,更新后,所有状态已是最新。可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。

  • destroyed 可以执行一些优化操作,清空定时器,解除绑定事件

  • vue3 beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消

  • vue3 unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器



<div id="app">{{name}}</div><script>    const vm = new Vue({        data(){            return {name:'poetries'}        },        el: '#app',        beforeCreate(){            // 数据观测(data observer) 和 event/watcher 事件配置之前被调用。            console.log('beforeCreate');        },        created(){            // 属性和方法的运算, watch/event 事件回调。这里没有$el            console.log('created')        },        beforeMount(){            // 相关的 render 函数首次被调用。            console.log('beforeMount')        },        mounted(){            // 被新创建的 vm.$el 替换            console.log('mounted')        },        beforeUpdate(){            //  数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。            console.log('beforeUpdate')        },        updated(){            //  由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。            console.log('updated')        },        beforeDestroy(){            // 实例销毁之前调用 实例仍然完全可用            console.log('beforeDestroy')        },        destroyed(){             // 所有东西都会解绑定,所有的事件监听器会被移除            console.log('destroyed')        }    });    setTimeout(() => {        vm.name = 'poetry';        setTimeout(() => {            vm.$destroy()          }, 1000);    }, 1000);</script>
复制代码


  1. 组合式 API 生命周期钩子


你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。


下表包含如何在 setup() 内部调用生命周期钩子:



因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写


export default {  setup() {    // mounted    onMounted(() => {      console.log('Component is mounted!')    })  }}
复制代码


setupcreated谁先执行?


  • beforeCreate:组件被创建出来,组件的methodsdata还没初始化好

  • setup:在beforeCreatecreated之间执行

  • created:组件被创建出来,组件的methodsdata已经初始化好了


由于在执行setup的时候,created还没有创建好,所以在setup函数内我们是无法使用datamethods的。所以vue为了让我们避免错误的使用,直接将setup函数内的this执行指向undefined


import { ref } from "vue"export default {  // setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去  setup(){    let count = ref(1)    function myFn(){      count.value +=1    }    return {count,myFn}  },
}
复制代码


  1. 其他问题


  • 什么是 vue 生命周期? Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载 Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。

  • vue 生命周期的作用是什么? 它的生命周期中有多个事件钩子,让我们在控制整个 Vue 实例的过程时更容易形成好的逻辑。

  • vue 生命周期总共有几个阶段? 它可以总共分为8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。

  • 第一次页面加载会触发哪几个钩子? 会触发下面这几个beforeCreatecreatedbeforeMountmounted

  • 你的接口请求一般放在哪个生命周期中? 接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created

  • DOM 渲染在哪个周期中就已经完成?mounted中,

  • 注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted




### Vue模版编译原理
vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。
- **解析阶段**:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。- **优化阶段**:遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。- **生成阶段**:将最终的AST转化为render函数字符串。


### Vue 中给 data 中的对象属性添加一个新的属性时会发生什么?如何解决?
```javascript<template> <div> <ul> <li v-for="value in obj" :key="value"> {{value}} </li> </ul> <button @click="addObjB">添加 obj.b</button> </div></template>
<script> export default { data () { return { obj: { a: 'obj.a' } } }, methods: { addObjB () { this.obj.b = 'obj.b' console.log(this.obj) } } }</script>
复制代码


点击 button 会发现,obj.b 已经成功添加,但是视图并未刷新。这是因为在 Vue 实例创建时,obj.b 并未声明,因此就没有被 Vue 转换为响应式的属性,自然就不会触发视图的更新,这时就需要使用 Vue 的全局 api $set():


addObjB () (   this.$set(this.obj, 'b', 'obj.b')   console.log(this.obj)}
复制代码


$set()方法相当于手动的去把 obj.b 处理成一个响应式的属性,此时视图也会跟着改变了。

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 的功能所调用的方法)

Vue 中如何进行依赖收集?

  • 每个属性都有自己的dep属性,存放他所依赖的watcher,当属性变化之后会通知自己对应的watcher去更新

  • 默认会在初始化时调用render函数,此时会触发属性依赖收集 dep.depend

  • 当属性发生修改时会触发watcher更新dep.notify()



依赖收集简版


let obj = { name: 'poetry', age: 20 };
class Dep { constructor() { this.subs = [] // subs [watcher] } depend() { this.subs.push(Dep.target) } notify() { this.subs.forEach(watcher => watcher.update()) }}Dep.target = null;observer(obj); // 响应式属性劫持
// 依赖收集 所有属性都会增加一个dep属性,// 当渲染的时候取值了 ,这个dep属性 就会将渲染的watcher收集起来// 数据更新 会让watcher重新执行
// 观察者模式
// 渲染组件时 会创建watcherclass Watcher { constructor(render) { this.get(); } get() { Dep.target = this; render(); // 执行render Dep.target = null; } update() { this.get(); }}const render = () => { console.log(obj.name); // obj.name => get方法}
// 组件是watcher、计算属性是watchernew Watcher(render);
function observer(value) { // proxy reflect if (typeof value === 'object' && typeof value !== null) for (let key in value) { defineReactive(value, key, value[key]); }}function defineReactive(obj, key, value) { // 创建一个dep let dep = new Dep();
// 递归观察子属性 observer(value);
Object.defineProperty(obj, key, { get() { // 收集对应的key 在哪个方法(组件)中被使用 if (Dep.target) { // watcher dep.depend(); // 这里会建立 dep 和watcher的关系 } return value; }, set(newValue) { if (newValue !== value) { observer(newValue); value = newValue; // 让key对应的方法(组件重新渲染)重新执行 dep.notify() } } })}
// 模拟数据获取,触发getterobj.name = 'poetries'
// 一个属性一个dep,一个属性可以对应多个watcher(一个属性可以在任何组件中使用、在多个组件中使用)// 一个dep 对应多个watcher // 一个watcher 对应多个dep (一个视图对应多个属性)// dep 和 watcher是多对多的关系
复制代码


用户头像

yyds2026

关注

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

还未添加个人简介

评论

发布
暂无评论
2023前端一面vue面试题合集_Vue_yyds2026_InfoQ写作社区