Vue实战面试知识点汇总

1. 双向绑定详解

  参考地址:https://juejin.im/post/5f11672fe51d45348e27b3ff

2.computed 的实现原理

  1)为什么需要computed?

    template 使用大量复杂的逻辑表达式处理数据,使得代码维护性差,且相同数据重复计算对性能开销大

  2)计算属性的实现原理? 

    1.初始化 data 和 computed,分别代理其 set 和 get 方法,对 data 中的所有属性生成唯一的 dep 实例

    2.对 computed 中的 属性生成唯一的 watcher,并保存在 vm._computedWatchers 中

    3.访问计算属性时,设置 Dep.target 指向 计算属性的 watcher,调用该属性具体方法

    4.方法中访问 data 的属性,即会调用 data 属性的 get 方法,将 data 属性的 dep 加入到 计算属性的 watcher , 同时该 dep 中的 subs 添加这个 watcher

    5.设置 data 的这个属性时,调用该属性代理的 set 方法,触发 dep 的 notify 方法

    6.因为时 computed 属性,只是将 watcher 中的 dirty 设置为 true

    7.最后,访问计算属性的 get 方法时,得知该属性的 watcher.dirty 为 true,则调用 watcher.evaluate() 方法获取新的值

    综合以上:也可以解释了为什么有些时候当computed没有被访问(或者没有被模板依赖),当修改了this.data值后,通过vue-tools发现其computed中的值没有变化的原因,因为没有触发到其get方法。

3.为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

  1)Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性,导致通过数组下标添加属性无法实时响应

  2) Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象

  3)Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性

  补救:push();pop();shift();unshift();splice();sort();reverse(); Vue重写了数组这个7个方法,称为变异数组,添加了响应式,使得这7个方法添加的属性可以实时响应


4.Vue中的key有什么作用?
  1)
key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位) .diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.

  2)更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug

  3)更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下:

function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key; 
  const map = {}; 
for (i = beginIdx; i <= endIdx; ++i) {    
  key = children[i].key;    
  if (isDef(key)) map[key] = i;  
 } 
 return map;
}

 5.谈一谈nextTick 的原理

  1. vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行

  2. microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕

  3. 考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案

  Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更;Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替

6.vue 是如何对数组方法进行变异的 ?

 1 const arrayProto = Array.prototype;
 2 export const arrayMethods = Object.create(arrayProto);
 3 const methodsToPatch = [
 4     "push",
 5     "pop",
 6     "shift",
 7     "unshift",
 8     "splice",
 9     "sort",
10     "reverse"
11 ];
12 /** * Intercept mutating methods and emit events */
13 methodsToPatch.forEach(function (method) {
14     // cache original method  
15     const original = arrayProto[method];
16     def(arrayMethods, method, function mutator(...args) {
17         const result = original.apply(this, args);
18         const ob = this.__ob__; let inserted;
19         switch (method) {
20             case "push":
21             case "unshift":
22                 inserted = args;
23                 break;
24             case "splice":
25                 inserted = args.slice(2);
26                 break;
27         }
28         if (inserted)
29             ob.observeArray(inserted);
30         // notify change   
31         ob.dep.notify();
32         return result;
33     });
34 });
35 /** * Observe a list of Array items. */
36 Observer.prototype.observeArray = function observeArray(items) {
37     for (var i = 0, l = items.length; i < l; i++) {
38         observe(items[i]);
39     }
40 };
简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

7.Vue 组件 data 为什么必须是函数 ?

new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?

  因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

8.聊聊 keep-alive 的实现原理和缓存策略?
 1 export default {
 2     name: "keep-alive",
 3     abstract: true,
 4     // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中  
 5     props: {
 6         include: patternTypes,
 7         // 被缓存组件   
 8         exclude: patternTypes,
 9         // 不被缓存组件   
10         max: [String, Number] // 指定缓存大小  
11     },
12     created() {
13         this.cache = Object.create(null); // 缓存  
14         this.keys = []; // 缓存的VNode的键 
15     },
16     destroyed() {
17         for (const key in this.cache) {
18             // 删除所有缓存      
19             pruneCacheEntry(this.cache, key, this.keys);
20         }
21     },
22     mounted() {
23         // 监听缓存/不缓存组件   
24         this.$watch("include", val => {
25             pruneCache(this, name => matches(val, name));
26         });
27         this.$watch("exclude", val => {
28             pruneCache(this, name => !matches(val, name));
29         });
30     },
31     render() {
32         // 获取第一个子元素的 vnode 
33         const slot = this.$slots.default;
34         const vnode: VNode = getFirstComponentChild(slot);
35         const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions;
36         if (componentOptions) {
37             // name不在inlcude中或者在exlude中 直接返回vnode     
38             // check pattern     
39             const name: ?string = getComponentName(componentOptions);
40             const { include, exclude } = this; if (
41                 // not included     
42                 (include && (!name || !matches(include, name))) ||
43                 // excluded       
44                 (exclude && name && matches(exclude, name))
45             ) { return vnode; }
46             const { cache, keys } = this;
47             // 获取键,优先获取组件的name字段,否则是组件的tag     
48             const key: ?string = vnode.key == null ?
49                 // same constructor may get registered as different local components       
50                 // so cid alone is not enough (#3269)          
51                 componentOptions.Ctor.cid +
52                 (componentOptions.tag ? `::${componentOptions.tag}` : "") : vnode.key;
53             // 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个     
54             if (cache[key]) {
55                 vnode.componentInstance = cache[key].componentInstance;
56                 // make current key freshest        
57                 remove(keys, key); keys.push(key);
58             }
59             // 不命中缓存,把 vnode 设置进缓存      
60             else {
61                 cache[key] = vnode; keys.push(key);
62                 // prune oldest entry      
63                 // 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个   
64                 if (this.max && keys.length > parseInt(this.max)) {
65                     pruneCacheEntry(cache, keys[0], keys, this._vnode);
66                 }
67             }
68             // keepAlive标记位      
69             vnode.data.keepAlive = true;
70         } return vnode || (slot && slot[0]);
71     }
72 };

原理

  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]

9.vm.$set()实现原理是什么?

受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。

由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。

那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?

 1 export function set(target: Array<any> | Object, key: any, val: any): any {
 2     // target 为数组  
 3     if (Array.isArray(target) && isValidArrayIndex(key)) {
 4         // 修改数组的长度, 避免索引>数组长度导致splice()执行有误   
 5         target.length = Math.max(target.length, key);
 6         // 利用数组的splice变异方法触发响应式    
 7         target.splice(key, 1, val); return val;
 8     }
 9     // target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
10     if (key in target && !(key in Object.prototype)) {
11         target[key] = val; return val;
12     }
13     // 以上都不成立, 即开始给target创建一个全新的属性  
14     // 获取Observer实例  
15     const ob = (target: any).__ob__;
16     // target 本身就不是响应式数据, 直接赋值  
17     if (!ob) { target[key] = val; return val; }
18     // 进行响应式处理 
19     defineReactive(ob.value, key, val);
20     ob.dep.notify();
21     return val;
22 }
  1. 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式

  2. 如果目标是对象,判断属性存在,即为响应式,直接赋值

  3. 如果 target 本身就不是响应式,直接赋值

  4. 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理

 10.Object.defineProperty和Proxy的区别?

  • Object.defineProperty
    • 不能监听到数组length属性的变化;
    • 不能监听对象的添加;
    • 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
  • Proxy
    • 可以监听数组length属性的变化;
    • 可以监听对象的添加;
    • 可代理整个对象,不需要对对象进行遍历,极大提高性能;
    • 多达13种的拦截远超Object.defineProperty只有get和set两种拦截

 11.你认为Vue的核心是什么?

Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统。

  以上是官方原话,从中可以得知Vue的核心是模板语法和数据渲染。

12.Vue为什么要求组件模板只能有一个根元素?

当前的virtualDOM差异和diff算法在很大程度上依赖于每个子组件总是只有一个根元素。

13.ajax、fetch、axios这三都有什么区别?

ajax是最初出现的发送临时请求的技术,属于原生js标准,核心是使用XMLHttpRequest对象,使用并存并有先后顺序的话,容易产生地狱。

fetch号称可以代替ajax的技术,是基于es6中的Promise对象设计的,参数和jQuery中的ajax类似,它并不是对ajax的进一步封装,它属于原生js尺寸。没有使用XMLHttpRequest对象。

axios不是原生js,使用时需要进行进行安装,客户端和服务器端都可以使用,可以在请求和相应阶段进行拦截,基于promise对象。

 14.如果想扩展某个现有的Vue组件时,怎么做呢?

  • 用mixins混入
  • 用extends,比mixins先触发
  • 用高阶组件HOC封装

  注意:extends使得组件能像面向对象变成一样便于拓展

    extends会比mixins先执行。执行顺序:extends > mixins > 组件

    extends只能暴露一个extends对象,暴露多个extends不会执行。

    mixins能暴露多个

15.vue组件和插件的区别

组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue。

插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身。

简单来说,插件就是指对Vue的功能的增强或补充。

比如说,让你在每个单页面的组件里,都可以调用某个方法,或者共享使用某个变量,或者在某个方法之前执行一段代码等

就可以写一个插件,在Vue原型上扩展方法,要实现这个需求绝对没法写成组件。

1 let whatever = {
2   install: function(Vue, options) {
3     Vue.prototype.$whatever = function(){
4     // do something
5     };
6   }
7 }

16.为什么Vue使用异步更新组件?

批量更新 收集当前的改动一次性更新 节省diff开销;关联this.$nextTick使用微任务方式得到更新后的dom

17.用vue怎么实现一个换肤的功能?

  1.base.scss: 一些通用样式文件(使用scss定义颜色变量,通过@include 引入mixin中的变量)

    #content{
      @include bg_color()
    }    

  2.varibale.scss: 颜色,字体,背景的配置文件

$background-color-them1:red
$background-color-them2:blue

  3.mixin.scss: 定义mixin方法的文件(通过判断HTML上属性,识别加载模板)

@import "./variable";/*引入配置*/
@mixin font_size($size){/*通过该函数设置字体大小,后期方便统一管理;*/
  @include font-dpr($size);
}
@mixin bg_color($color){/*通过该函数设置主题颜色,后期方便统一管理;*/
  background-color:$color;
  [data-theme="theme1"] & {
    background-color:$background-color-theme1;
  }
  [data-theme="theme2"] & {
    background-color:$background-color-theme2;
  }
  [data-theme="theme3"] & {
    background-color:$background-color-theme3;
  }
}

  主要原理:

    通过设置html的attribute属性在封装的函数中进行判断,进行相应的设置不同的颜色

    css中 [ ] 可以识别到在html标签上设置的属性,所以在html上对应属性发生变化时,就会执行相应的样式,

    这一步有点类似于平时给div添加一个.active属性,css自动执行相应样式

18.对于 vue3.0 特性你有什么了解的吗?

  1.使用porxy替换object.defineProperty

    • 只能监测属性,不能监测对象
    • 检测属性的添加和删除;
    • 检测数组索引和长度的变更;
    • 支持 Map、Set、WeakMap 和 WeakSet。

  2.模板
  模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能
同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。

  3.对typescript结合使用更容易

  4.其他改变:

    • 支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
    • 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
    • 基于 treeshaking 优化,提供了更多的内置功能

19.key除了在v-for中使用,还有什么作用?

还可以强制替换元素/组件而不是重复使用它。在以下场景可以使用

  • 完整地触发组件的生命周期钩子
  • 触发过渡
<transition>
  <span :key="text">{{ text }}</span>
</transition>

当 text 发生改变时,<span>会随时被更新,因此会触发过渡

  • 不要使用对象或数组之类的非基本类型值作为key,请用字符串或数值类型的值;

  • 不要使用数组的index作为key值,因为在删除数组某一项,index也会随之变化,导致key变化,渲染会出错

原文地址:https://www.cnblogs.com/advanceCabbage/p/13402355.html