首頁  >  文章  >  web前端  >  12個vue高頻原理面試題(附分析)

12個vue高頻原理面試題(附分析)

青灯夜游
青灯夜游轉載
2020-07-13 16:17:2752060瀏覽

12個vue高頻原理面試題(附分析)

本文分享12題高頻vue原理面試題,涵蓋了vue 核心實現原理,其實一個框架的實現原理一篇文章是不可能說完的,希望通過這12道問題,讓讀者對自己的Vue 掌握程度有一定的認識(B 數),從而彌補自己的不足,更好的掌握Vue。

【相關推薦:vue面試題#(2021)】

1. Vue 響應式原則

12個vue高頻原理面試題(附分析)

#核心實作類別:

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. 主執行緒不斷重複上面的第三步。

12個vue高頻原理面試題(附分析)

主執行緒的執行過程就是一個 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 的渲染过程

12個vue高頻原理面試題(附分析)

  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)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

12個vue高頻原理面試題(附分析)

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高頻原理面試題(附分析)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除