百分百源码网-让建站变得如此简单! 登录 注册 签到领金币!

主页 | 如何升级VIP | TAG标签

当前位置: 主页>网站教程>JS教程> 12个vue高频道理面试题(附剖析)
分享文章到:

12个vue高频道理面试题(附剖析)

发布时间:09/01 来源:未知 浏览: 关键词:

本文分享12道高频vue道理面试题,覆盖了 vue 中心实现道理,其实一个框架的实现道理一篇文章是不成能说完的,但愿通过这 12 道问题,让读者对本人的 Vue 把握程度有必然的认识(B 数),从而补偿本人的不足,更好的把握 Vue。

【相关引荐:vue面试题(2020)】

1. Vue 响应式道理

1.png

中心实现类:

Observer : 它的作用是给对象的属性增加 getter 和 setter,用于依靠收集和派发更新

Dep : 用于收集当前响应式对象的依靠关系,每个响应式对象包罗子对象都具有一个 Dep 实例(里面 subs 是 Watcher 实例数组),当数据有变动时,会通过 dep.notify()通知各个 watcher。

Watcher : 视察者对象 , 实例分为渲染 watcher (render watcher),运算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

Watcher 和 Dep 的关系

watcher 中实例化了 dep 并向 dep.subs 中增加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

依靠收集

  1. initState 时,对 computed 属性初始化时,触发 computed watcher 依靠收集
  2. initState 时,对侦听属性初始化时,触发 user watcher 依靠收集
  3. render()的历程,触发 render watcher 依靠收集
  4. re-render 时,vm.render()再次施行,会移除所有 subs 中的 watcer 的订阅,从新赋值。

派发更新

  1. 组件中对响应的数据停止了修改,触发 setter 的逻辑
  2. 调取 dep.notify()
  3. 遍历所有的 subs(Watcher 实例),调取每一个 watcher 的 update 办法。

道理

当创立 Vue 实例时,vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性增加 getter 和 setter 对数据的读取停止劫持(getter 用来依靠收集,setter 用来派发更新),并且在内部追踪依靠,在属性被拜访和修改时通知转变。

每个组件实例会有响应的 watcher 实例,会在组件渲染的历程中记载依靠的所有数据属性(停止依靠收集,还有 computed watcher,user watcher 实例),之后依靠项被改动时,setter 办法会通知依靠与此 data 的 watcher 实例从新运算(派发更新),从而使它关联的组件从新渲染。

一句话总结:

vue.js 采纳数据劫持结合公布-订阅模式,通过 Object.defineproperty 来劫持各个属性的 setter,getter,在数据变更时公布新闻给订阅者,触发响应的监听回调

2. 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(懒运算)特性。)

3. computed 和 watch 有什么不同及使用场景?

不同

computed 运算属性 : 依靠其它属性值,并且 computed 的值有缓存,只要它依靠的属性值发生改动,下一次猎取 computed 的值时才会从新运算 computed 的值。

watch 侦听器 : 更多的是「视察」的作用,无缓存性,相似于某些数据的监听回调,每当监听的数据转变时都会施行回调停止后续操纵。

使用场景

使用场景:

当我们需要停止数值运算,并且依靠于其它数据时,应当使用 computed,由于可以利用 computed 的缓存特性,幸免每次猎取值时,都要从新运算。

当我们需要在数据转变时施行异步或开销较大的操纵时,应当使用 watch,使用 watch 选项同意我们施行异步操纵 ( 拜访一个 API ),限制我们施行该操纵的频率,并在我们得到终究结果前,设定中心状态。这些都是运算属性没法做到的。

4. 为什么在 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 不仅可以代理对象,还可以代理数组。还可以代理动态增添的属性。

5. 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;
}

6. 谈一谈 nextTick 的道理

JS 运转机制

JS 施行是单线程的,它是基于事件轮回的。事件轮回大致分为以下几个步骤:

  1. 所有同步任务都在主线程上施行,构成一个施行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运转结果,就在"任务队列"之中放置一个事件。
  3. 一旦"施行栈"中的所有同步任务施行完毕,系统就会读取"任务队列",看看里面是什么事件。那些对应的异步任务,于是完毕等候状态,进入施行栈,开端施行。
  4. 主线程不竭反复上面的第三步。

2.jpg

主线程的施行历程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 新闻队列中存置的是一个个的任务(task)。 标准中规定 task 分为两大类,离别是 macro task 和 micro task,并且每个 macro task 完毕后,都要清空所有的 micro task。

for (macroTask of macroTaskQueue) {
  // 1. Handle current MACRO-TASK
  handleMacroTask();

  // 2. Handle all MICRO-TASK
  for (microTask of microTaskQueue) {
    handleMicroTask(microTask);
  }
}

在阅读器环境中 :

常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate

常见的 micro task 有 MutationObsever 和 Promise.then

异步更新队列

大概你还没有留意到,Vue 在更新 DOM 时是异步施行的。只要侦听到数据转变,Vue 将开启一个队列,并缓冲在统一事件轮回中发生的所有数据变动。

假如统一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除反复数据关于幸免不必要的运算和 DOM 操纵是非常重要的。

然后,鄙人一个的事件轮回“tick”中,Vue 刷新队列并施行实际 (已去重的) 工作。

Vue 在内部对异队伍列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,假如施行环境不支撑,则会采纳 setTimeout(fn, 0) 代替。

在 vue2.5 的源码中,macrotask 落级的方案顺次是:setImmediate、MessageChannel、setTimeout

vue 的 nextTick 办法的实现道理:

  1. vue 用异队伍列的方式来操纵 DOM 更新和 nextTick 回调前后施行
  2. microtask 由于其高优先级特性,能确保队列中的微任务在一次事件轮回前被施行完毕
  3. 思考兼容问题,vue 做了 microtask 向 macrotask 的落级方案

7. 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

8. Vue 组件 data 为什么必需是函数 ?

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

由于组件是可以复用的,JS 里对象是援用关系,假如组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,发生副作用。

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

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;
  }
}

10. 说说 Vue 的渲染历程

3.png

  1. 调取 compile 函数,生成 render 函数字符串 ,编译历程如下:
  • parse 函数解析 template,生成 ast(抽象语法树)
  • optimize 函数优化静态节点 (标志不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比力的历程,优化了 patch 的机能)
  • generate 函数生成 render 函数字符串
  1. 调取 new Watcher 函数,监听数据的转变,当数据发生转变时,Render 函数施行生成 vnode 对象
  2. 调取 patch 办法,对照新旧 vnode 对象,通过 DOM diff 算法,增加、修改、删除真正的 DOM 元素

11. 聊聊 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)算法按照数据的历史拜访记载来停止裁汰数据,其中心思想是“假如数据比来被拜访过,那么未来被拜访的几率也更高”。

4.png

keep-alive 的实现正是用到了 LRU 战略,将比来拜访的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被拜访的组件,当缓存实例超越 max 设定值,删除 this.keys[0]

12. vm.$set()实现道理是啥?

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

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

关于已经创立的实例,Vue 不同意动态增加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 办法向嵌套对象增加响应式属性。

那么 Vue 内部是怎样解决对象新增属性不克不及响应的问题的呢?

export function set(target: Array<any> | 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 办法停止响应式处置

本文转载自:https://segmentfault.com/a/1190000021407782

引荐教程:《JavaScript视频教程》

以上就是12个vue高频道理面试题(附剖析)的具体内容,更多请关注百分百源码网其它相关文章!

打赏

打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

百分百源码网 建议打赏1~10元,土豪随意,感谢您的阅读!

共有151人阅读,期待你的评论!发表评论
昵称: 网址: 验证码: 点击我更换图片
最新评论

本文标签

广告赞助

能出一分力是一分吧!

订阅获得更多模板

本文标签

广告赞助

订阅获得更多模板