9个Vue高频原理面试题(2020新)

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

2、computed 和 watch 有什么区别及运用场景?

区别:
computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。

watch 侦听器 : 更多的是「观察」的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

运用场景:

当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算。

当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

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

Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(Vue 为什么不能检测数组变动 )。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组

push();
pop();
shift();
unshift();
splice();
sort();
reverse();

由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

4、Vue 中的 key 到底有什么用?

key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)

diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.

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

更快速 : 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、vue 是如何对数组方法进行变异的 ?

const arrayProto = Array.prototype;

export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

/**
 * Intercept mutating methods and emit events
 */

methodsToPatch.forEach(function(method) {
     
  // cache original 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) {
     
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // notify change
    ob.dep.notify();
    return result;
  });
});
 
/**
 * Observe a list of Array items.
 */

Observer.prototype.observeArray = function observeArray(items) {
     
  for (var i = 0, l = items.length; i < l; i++) {
     
    observe(items[i]);
  }
};

简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

6、Vue 组件 data 为什么必须是函数 ?

new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?
因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

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

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

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

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

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

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

export function set(target: Array | Object, key: any, val: any): any {
     
  // target 为数组
  if (Array.isArray(target) && isValidArrayIndex(key)) {
     
   // 修改数组的长度, 避免索引>数组长度导致splice()执行有误
    target.length = Math.max(target.length, key);
    // 利用数组的splice变异方法触发响应式
    target.splice(key, 1, val);
    return val;
  }

  // target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
  if (key in target && !(key in Object.prototype)) {
     
    target[key] = val;
    return val;
  }

  // 以上都不成立, 即开始给target创建一个全新的属性
  // 获取Observer实例
  const ob = (target: any).__ob__;
  // target 本身就不是响应式数据, 直接赋值
  if (!ob) {
     
    target[key] = val;
    return val;
  }

  // 进行响应式处理
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val;
}

1、如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
2、如果目标是对象,判断属性存在,即为响应式,直接赋值
3、如果 target 本身就不是响应式,直接赋值
4、如果属性不是响应式,则调用 defineReactive 方法进行响应式处理

8、说说 Vue 的渲染过程

9个Vue高频原理面试题(2020新)_第1张图片

调用 compile 函数,生成 render 函数字符串 ,编译过程如下:

  • parse 函数解析 template,生成 ast(抽象语法树)
  • optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)
  • generate 函数生成 render 函数字符串

调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

9、谈谈 Vue 事件机制,手写$ on,$ off, $ emit, $ once

Vue 事件机制 本质上就是 一个 发布-订阅 模式的实现。

class Vue {
     
  constructor() {
     
    //  事件通道调度中心
    this._events = Object.create(null);
  }

  $on(event, fn) {
     
    if (Array.isArray(event)) {
     
      event.map(item => {
     
        this.$on(item, fn);
      });
    } else {
     
      (this._events[event] || (this._events[event] = [])).push(fn);
    }
    return this;
  }

  $once(event, fn) {
     
    function on() {
     
      this.$off(event, on);
      fn.apply(this, arguments);
    }

    on.fn = fn;
    this.$on(event, on);
    return this;
  }

  $off(event, fn) {
     
    if (!arguments.length) {
     
      this._events = Object.create(null);
      return this;
    }

    if (Array.isArray(event)) {
     
      event.map(item => {
     
        this.$off(item, fn);
      });
      return this;
    }

    const cbs = this._events[event];
    if (!cbs) {
     
      return this;
    }

    if (!fn) {
     
      this._events[event] = null;
      return this;
    }

    let cb;
    let i = cbs.length;
    while (i--) {
     
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
     
        cbs.splice(i, 1);
        break;
      }
    }
    return this;
  }

  $emit(event) {
     
    let cbs = this._events[event];
    if (cbs) {
     
      const args = [].slice.call(arguments, 1);
      cbs.map(item => {
     
        args ? item.apply(this, args) : item.call(this);
      });
    }
    return this;
  }
}

你可能感兴趣的:(vue面试题,vue)