Vue面试题、超级干货、春招必备、带你牛转乾坤(附答案)

2,634 阅读10分钟

声明:本篇文章纯属笔记性文章,非整体原创,是对vue知识的整理, 参考文章:1.vue官网 2.掘金Vue系列文章 3其它优秀微信公众号文章

基础篇

那首先谈谈你对 Vue 的理解吧?❗

答案

官网介绍:渐进式 JavaScript 框架、核心库加插件、动态创建用户界面(异步获取后台数据,数据展示在页面) 特点: MVVM 模式;代码简介体积小,运行效率高,适合移动PC端开发;本身只关注 UI (和 React 相似),可以轻松引入 Vue 插件或其他的第三方库进行开发。

说说你对MVVM的理解? ​

答案

  • Model-View-ViewModel的缩写,Model代表数据模型,View代表UI组件,ViewModel将Model和View关联起来

  • 数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据

说说computed与watch的区别?

答案

计算属性computed和监听器watch都可以观察属性的变化从而做出响应,不同的是:

计算属性computed更多是作为缓存功能的观察者,它可以将一个或者多个data的属性进行复杂的计算生成一个新的值,提供给渲染函数使用,当依赖的属性变化时,computed不会立即重新计算生成新的值,而是先标记为脏数据,当下次computed被获取时候,才会进行重新计算并返回。

而监听器watch并不具备缓存性,监听器watch提供一个监听函数,当监听的属性发生变化时,会立即执行该函数。

小伙子说说 Vue 的渲染过程?

答案

  1. 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
  • parse 函数解析 template,生成 ast(抽象语法树)

  • optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)

  • generate 函数生成 render 函数字符串

  1. 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象

  2. 调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

简单介绍下Vue的生命周期?

答案

beforeCreated :是new Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。

created :在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。

beforeMount :发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。

mounted :在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。

beforeUpdate :发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。

updated :发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

beforeDestory :发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。

destoryed :发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

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

答案

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

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

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

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

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

为什么组件的data必须是一个函数? 

答案

一个组件可能在很多地方使用,也就是会创建很多个实例,如果data是一个对象的话,对象是引用类型,一个实例修改了data会影响到其他实例,所以data必须使用函数,为每一个实例创建一个属于自己的data,使其同一个组件的不同实例互不影响,避免污染实例。

Vue组件生命周期调用顺序?

答案

渲染顺序:先父后子,完成顺序:先子后父更新顺序:父更新导致子更新,子更新完成后父销毁顺序:先父后子,完成顺序:先子后父

Key属性的作用是什么?

答案

在对节点进行diff的过程中,判断是否为相同节点的一个很重要的条件是key是否相等,如果是相同节点,则会尽可能的复用原有的DOM节点。所以key属性是提供给框架在diff的时候使用的,而非开发者。

了解nextTick吗?

答案

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

v-for 和 v-if 为什么不能连用?

答案

v-for 会比 v-if 的优先级更高,连用的话会把 v-if 的每个元素都添加一下,造成性能问题。

为什么要使用异步组件?

答案

  1. 节省打包出的结果,异步组件分开打包,采用 jsonp 的方式进行加载,有效解决文件过大的问题。

  2. 核心就是包组件定义变成一个函数,依赖 import () 语法,可以实现文件的分割加载。

说说Vue组件通信方式有哪些?

答案

  • 父子间通信:父亲提供数据通过属性 props 传给儿子;儿子通过 $on 绑父亲的事件,再通过 $emit 触发自己的事件(发布订阅)

  • 利用父子关系 parentparent 、 children

获取父子组件实例的方法。

  • 父组件提供数据,子组件注入。 provide 、 inject ,插件用得多。

  • ref 获取组件实例,调用组件的属性、方法

  • 跨组件通信 Event Bus (Vue.prototype.bus=newVue)其实基于 bus = new Vue)其实基于 bus=newVue)其实基于 on 与$emit

  • vuex 状态管理实现通信

谈谈对keep-alive的了解?(简单)

答案

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

简单说下Vue事件绑定原理?(简单)

答案

每一个Vue实例都是一个Event Bus,当子组件被创建的时候,父组件将事件传递给子组件,子组件初始化的时候是有 on方法将事件注册到内部,在需要的时候使用on 方法将事件注册到内部,在需要的时候使用 emit 触发函数,而对于原生native事件,使用addEventListener绑定到真实的DOM元素上。

简单说下 v-if 和 v-show的区别?(简单)

答案

  • 当条件不成立时,v-if不会渲染DOM元素,v-show操作的是样式(display),切换当前DOM的显示和隐藏。

  • v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;

  • v-show 则适用于需要非常频繁切换条件的场景。

说说你对 SPA 单页面的理解,它的优缺点分别是什么?

答案

  • SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

  • 优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;

  • 基于上面一点,SPA 相对对服务器压力小;

  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

  • 缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;

  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;

  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

原理篇

Vue模板渲染的原理是什么?

答案

vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。

模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。

  • parse阶段:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。

  • optimize阶段:遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。

  • generate阶段:将最终的AST转化为render函数字符串。

Vue 模板 template预编译是什么?

答案

对于 Vue 组件来说,模板编译只会在组件实例化的时候编译一次,生成渲染函数之后在也不会进行编译。因此,编译对组件的 runtime 是一种性能损耗。

而模板编译的目的仅仅是将template转化为 render function ,这个过程,正好可以在项目构建的过程中完成,这样可以让实际组件在 runtime 时直接跳过模板渲染,进而提升性能,这个在项目构建的编译template的过程,就是预编译。

vue 是如何实现响应式数据的呢?(响应式数据原理)

答案

Vue2:Object.definProperty 重新定义 data 中所有的属性, Object.definProperty 可以使数据的获取与设置增加一个拦截的功能,拦截属性的获取,进行依赖收集。拦截属性的更新操作,进行通知。具体的过程:首先 Vue 使用 initData 初始化用户传入的参数,然后使用 new Observer 对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value) 对对象进行处理,内部使用 defineReactive 循环对象属性定义响应式变化,核心就是使用 Object.definProperty 重新定义数据。

那 vue 中是如何检测数组变化的呢?

答案

数组就是使用 object.defineProperty 重新定义数组的每一项,那能引起数组变化的方法我们都是知道的, poppushshiftunshiftsplicesortreverse 这七种,只要这些方法执行改了数组内容,我就更新内容就好了,是不是很好理解。

  1. 是用来函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新。

  2. 数组里每一项可能是对象,那么我就是会对数组的每一项进行观测,(且只有数组里的对象才能进行观测,观测过的也不会进行观测)

vue3:改用 proxy ,可直接监听对象数组的变化。

v-model 中的实现原理及如何自定义 v-model ?

答案

v-model 可以看成是 value+input 方法的语法糖(组件)。原生的 v-model ,会根据标签的不同生成不同的事件与属性。解析一个指令来。自定义:自己写 model 属性,里面放上 propevent

说说diff 算法?(重点)

答案

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

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

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

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

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

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

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

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

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

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

描述组件渲染和更新过程?

答案

渲染组件时,会通过 vue.extend() 方法构建子组件的构造函数,并进行实例化。最终手动调用 $mount() 进行挂载。更新组件时会进行 patchVnode 流程,核心就是 diff 算法。

说说 Vue 的事件绑定原理?

答案

  • 原生 DOM 的绑定:Vue 在创建真实 DOM 时会调用 createElm ,默认会调用 invokeCreateHooks 。会遍历当前平台下相对的属性处理代码,其中就有 updateDOMLListeners 方法,内部会传入 add() 方法

  • 组件绑定事件,原生事件,自定义事件;组件绑定之间是通过 Vue 中自定义的 $on 方法实现的。

说说 Vue2.0 和Vue3.0 有什么区别?

答案

  1. 重构响应式系统,使用Proxy替换Object.defineProperty,使用Proxy优势:
  • 可直接监听数组类型的数据变化

  • 监听的目标为对象本身,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升

  • 可拦截apply、ownKeys、has等13种方法,而Object.defineProperty不行

  • 直接实现对象属性的新增/删除

  1. 新增Composition API,更好的逻辑复用和代码组织

  2. 重构 Virtual DOM

  • 模板编译时的优化,将一些静态节点编译成常量

  • slot优化,将slot编译为lazy函数,将slot的渲染的决定权交给子组件

  • 模板中内联事件的提取并重用(原本每次渲染都重新生成内联函数)

  1. 代码结构调整,更便于Tree shaking,使得体积更小

  2. 使用Typescript替换Flow

nextTick的实现原理是什么?

答案

1、在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM。

2、nextTick主要使用了宏任务微任务

根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。

vm.$set 的实现原理?

答案

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

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

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(懒计算)特性。)

聊聊 keep-alive 的实现原理和缓存策略?

答案

export default {  name: "keep-alive",  abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中  props: {    include: patternTypes, // 被缓存组件    exclude: patternTypes, // 不被缓存组件    max: [String, Number] // 指定缓存大小  },​  created() {    this.cache = Object.create(null); // 缓存    this.keys = []; // 缓存的VNode的键  },​  destroyed() {    for (const key in this.cache) {      // 删除所有缓存      pruneCacheEntry(this.cache, key, this.keys);    }  },​  mounted() {    // 监听缓存/不缓存组件    this.$watch("include", val => {      pruneCache(this, name => matches(val, name));    });    this.$watch("exclude", val => {      pruneCache(this, name => !matches(val, name));    });  },​  render() {    // 获取第一个子元素的 vnode    const slot = this.$slots.default;    const vnode: VNode = getFirstComponentChild(slot);    const componentOptions: ?VNodeComponentOptions =      vnode && vnode.componentOptions;    if (componentOptions) {      // name不在inlcude中或者在exlude中 直接返回vnode      // check pattern      const name: ?string = getComponentName(componentOptions);      const { include, exclude } = this;      if (        // not included        (include && (!name || !matches(include, name))) ||        // excluded        (exclude && name && matches(exclude, name))      ) {        return vnode;      }​      const { cache, keys } = this;      // 获取键,优先获取组件的name字段,否则是组件的tag      const key: ?string =        vnode.key == null          ? // same constructor may get registered as different local components            // so cid alone is not enough (#3269)            componentOptions.Ctor.cid +            (componentOptions.tag ? `::${componentOptions.tag}` : "")          : vnode.key;      // 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个      if (cache[key]) {        vnode.componentInstance = cache[key].componentInstance;        // make current key freshest        remove(keys, key);        keys.push(key);      }      // 不命中缓存,把 vnode 设置进缓存      else {        cache[key] = vnode;        keys.push(key);        // prune oldest entry        // 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个        if (this.max && keys.length > parseInt(this.max)) {          pruneCacheEntry(cache, keys[0], keys, this._vnode);        }      }      // keepAlive标记位      vnode.data.keepAlive = true;    }    return vnode || (slot && slot[0]);  }};

原理

  1. 获取 keep-alive 包裹着的第一个子组件对象及其组件名

  2. 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例

  3. 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)

  4. 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)

  5. 最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说

LRU 缓存淘汰算法

LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

Vue 性能优化

答案

编码优化

  • 事件代理

  • keep-alive

  • 拆分组件

  • key 保证唯一性

  • 路由懒加载、异步组件

  • 防抖节流

Vue 加载性能优化

  • 第三方模块按需导入( babel-plugin-component

  • 图片懒加载

用户体验

  • app-skeleton 骨架屏

  • shellap p 壳

  • pwa

SEO 优化

  • 预渲染

最后的话

🚀🚀觉得不错的朋友可以 ⭐️ 关注我 < 微信公众号 前端互助圈 >

参考资料

[1]

GitHub 仓库:

github.com/Chocolate19…

[2]

cn.vuejs.org/index.html:

cn.vuejs.org/index.html

[3]

vuex.vuejs.org/zh/:

vuex.vuejs.org/zh/