ホームページ  >  記事  >  ウェブフロントエンド  >  Vue で頻繁に聞かれる主な面接の質問 12 個 (分析付き)

Vue で頻繁に聞かれる主な面接の質問 12 個 (分析付き)

青灯夜游
青灯夜游転載
2020-07-13 16:17:2751870ブラウズ

Vue で頻繁に聞かれる主な面接の質問 12 個 (分析付き)

この記事では、Vue の主要な実装原則をカバーする、頻度の高い 12 の Vue 原則インタビューの質問を共有します。実際、フレームワークの実装原則を 1 つの記事で説明することは不可能です。この 12 の質問を通じて、読者が自分自身の Vue の習熟度 (番号 B) をある程度理解し、自分の欠点を補い、Vue をより良くマスターできることを願っています。

【関連する推奨事項: vue 面接の質問(2021)】

1. Vue の応答性の原則

Vue で頻繁に聞かれる主な面接の質問 12 個 (分析付き)

##コア実装クラス:

オブザーバー: その関数は次のとおりです。依存関係を収集し、更新をディスパッチするためにゲッターとセッターをオブジェクトのプロパティに追加します。

Dep: 現在の応答オブジェクトの依存関係を収集するために使用されます。サブオブジェクトを含む各応答オブジェクトには Dep インスタンスがあります。(内部の subs は Watcher インスタンスの配列です) データが変更されると、各 Watcher は dep.notify() を通じて通知されます。

Watcher: Observer オブジェクト。インスタンスは 3 つのタイプに分かれています: レンダリング ウォッチャー (render watcher)、計算された属性ウォッチャー (computed watcher)、リスナー ウォッチャー (user watcher)

Watcher と Dep Dep関係

watcher でインスタンス化され、サブスクライバを dep.subs に追加します。dep は、notify を介して dep.subs をトラバースして、各ウォッチャーの更新を通知します。

Dependency collection

    InitState、計算された属性が初期化されると、計算されたウォッチャーがトリガーされますDependency collection
  1. InitState、listen 属性が初期化されると、ユーザーウォッチャーがトリガーされる 依存関係コレクション
  2. render() のプロセスにより、レンダー ウォッチャーがトリガーされます。依存関係コレクション
  3. が再レンダリングされると、vm.render() が再度実行され、すべてのウォッチャー サブスクリプションが削除されます。サブで再レンダリングします。
更新のディスパッチ

    コンポーネント内の応答データとセッターをトリガーするロジックが変更されました
  1. Call dep.notify()
  2. すべてのサブ (ウォッチャー インスタンス) をトラバースし、各ウォッチャーの更新メソッドを呼び出します。
原則

Vue インスタンスを作成するとき、vue はデータ オプションのプロパティを走査し、Object.defineProperty を使用してプロパティにゲッターとセッターを追加し、データの読み取りをハイジャックします。データ (ゲッターは依存関係を収集するために使用され、セッターは更新をディスパッチするために使用されます)、依存関係を内部的に追跡し、プロパティがアクセスおよび変更されたときに変更を通知します。

各コンポーネント インスタンスには、対応するウォッチャー インスタンスがあり、コンポーネントのレンダリング プロセス中に依存関係のすべてのデータ属性 (依存関係コレクション、計算されたウォッチャー インスタンスおよびユーザー ウォッチャー インスタンス) が記録され、依存関係は次のようになります。変更されました ウォッチャー インスタンスがこのデータに依存している場合、セッター メソッドはウォッチャー インスタンスに再計算 (更新のディスパッチ) するよう通知し、それによって関連付けられたコンポーネントを再レンダリングします。

要約を一文でまとめると:

vue.js は、パブリッシュ/サブスクライブ モードと組み合わせたデータ ハイジャックを使用し、Object.defineproperty を通じて各プロパティのセッターとゲッターをハイジャックし、サブスクライバーにメッセージをパブリッシュします。データ変更、応答リスニング コールバックをトリガーします

2. computed の実装原理

Computed は本質的に遅延評価オブザーバーです。

Computed は内部的に遅延ウォッチャー、つまり computed watcher を実装します。computed watcher はすぐには評価されず、dep インスタンスも保持します。

計算されたプロパティを再評価する必要があるかどうかをマークするために、内部で this.dirty 属性が使用されます。

計算されたものの依存関係のステータスが変更されると、この遅延ウォッチャーに通知されます。はいの場合は再計算され、新しい値と古い値が比較され、変更された場合は再レンダリングされます。 (

Vue は、計算されたプロパティが依存する値が変更されるだけでなく、計算されたプロパティの最終計算値が変更されたときに、レンダリング ウォッチャーがトリガーされて再レンダリングされることを保証したいと考えています。これは本質的に最適化です。

)

そうでない場合は、this.dirty = true を設定してください。 (

計算された属性が他のデータに依存している場合、その属性はすぐには再計算されません。後で他の場所でその属性を読み取る必要がある場合にのみ実際に計算されます。つまり、遅延 (遅延計算) 特性があります。 )

3.computed と watch、およびそのアプリケーション シナリオの違いは何ですか?

違いcomputed計算されたプロパティ: 他の属性値に依存し、計算された値がキャッシュされます。依存する属性値が変更された場合にのみ、次回計算された値が取得されたときに計算された値が再計算されます。

監視リスナー: より「監視」関数です。

キャッシュなし

。特定のデータの監視コールバックと同様、監視対象データが変更されるたびにコールバックが実行されます。

アプリケーション シナリオ

アプリケーション シナリオ:

数値計算を実行する必要があり、他のデータに依存する場合は、computed のキャッシュ機能を使用できるため、computed を使用する必要があります。値を取得するたびに、値が再計算されます。

データ変更時に非同期操作や高コストの操作を実行する必要がある場合は、watch を使用する必要があります。watch オプションを使用すると、非同期操作 (API にアクセス) を実行し、操作を実行する頻度を制限できます。最終結果の前に、中間状態を設定します。これらは、計算されたプロパティでは実行できないことです。

4. Vue3.0 で Proxy が採用され、Object.defineProperty が廃止されたのはなぜですか?

Object.defineProperty 自体には配列添字の変更を監視する機能がありますが、Vue ではパフォーマンス/エクスペリエンスの費用対効果を考慮して、この機能を放棄しました ( Why) Vue は配列の変更を検出できません)。この問題を解決するには、Vue の内部処理後、次のメソッドを使用して配列を監視します。
push();
pop();
shift();
unshift();
splice();
sort();
reverse();

上記 7 つのメソッドのみがハッキングされるため、他の配列のプロパティを検出することはできません。特定の制限。

Object.defineProperty はオブジェクトのプロパティのみをハイジャックできるため、各オブジェクトの各プロパティをトラバースする必要があります。 Vue 2.x では、データ モニタリングはデータ オブジェクトを再帰的に走査することによって実現されます。属性値もオブジェクトである場合は、深い走査が必要です。完全なオブジェクトをハイジャックできる場合は、明らかにそれがより良い選択です。

プロキシはオブジェクト全体をハイジャックして新しいオブジェクトを返すことができます。プロキシはオブジェクトをプロキシするだけでなく、配列をプロキシすることもできます。動的に追加された属性をプロキシすることもできます。

#5. Vue でのキーの使用法は何ですか?

key は、各 vnode に与えられる一意の ID です。キーに応じて、diff 操作はより正確かつ高速になります (単純なリスト ページのレンダリングでは diff ノードも高速になりますが、トランジション効果が生成されない可能性や、一部のノードにバインドされたデータ (フォーム) 状態があり、状態の不整合が発生するなど、隠れた副作用がいくつかあります。)

Diff アルゴリズムが最初に実行されます。古いノードと新しいノードの先頭と末尾を比較します。一致しない場合は、新しいノードのキーを使用して古いノードと比較し、対応する古いノードを見つけます。

の方が正確です。キーは、その場で再利用されません。同じNode関数のa.key === b.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. すべての同期タスクはメイン スレッドで実行され、実行コンテキスト スタックを形成します。
  2. メインスレッドの他に「タスクキュー」もあります。非同期タスクに実行結果がある限り、イベントは「タスク キュー」に配置されます。
  3. 「実行スタック」内のすべての同期タスクが実行されると、システムは「タスク キュー」を読み取り、その中にどのようなイベントがあるかを確認します。これらの対応する非同期タスクは待機状態を終了し、実行スタックに入り、実行を開始します。
  4. メインスレッドは引き続き上記の 3 番目のステップを繰り返します。

Vue で頻繁に聞かれる主な面接の質問 12 個 (分析付き)

メインスレッドの実行プロセスは 1 ティックであり、すべての非同期結果は「タスク キュー」を通じてスケジュールされます。メッセージキューにはタスクが一つずつ格納されます。仕様ではタスクをマクロタスクとマイクロタスクの2つに分け、各マクロタスク終了後は全てのマイクロタスクをクリアする必要があると定められている。

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

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

ブラウザ環境の場合:

一般的なマクロ タスクには、setTimeout、MessageChannel、postMessage、setImmediate

一般的なマイクロ タスクには、MutationObsever およびPromise.then

非同期更新キュー

まだ気づいていないかもしれませんが、DOM を更新するときに Vue が ## されます。非同期実行されました。データ変更をリッスンしている限り、Vue はキューを開き、同じイベント ループ内で発生するすべてのデータ変更をバッファーに入れます。

同じウォッチャーが複数回トリガーされた場合、キューにプッシュされるのは 1 回だけです。バッファリング中のこの重複排除は、不必要な計算や DOM 操作を回避するために重要です。

次に、次のイベント ループ「ティック」で、Vue はキューをフラッシュし、実際の (重複排除された) 作業を実行します。

Vue は内部的にネイティブの Promise.then、MutationObserver、setImmediate を非同期キューに使用しようとします。実行環境がサポートしていない場合は、代わりに setTimeout(fn, 0) が使用されます。

vue2.5 のソース コードでは、マクロタスクのダウングレード スキームは次のとおりです: 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 的渲染过程

Vue で頻繁に聞かれる主な面接の質問 12 個 (分析付き)

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

Vue で頻繁に聞かれる主な面接の質問 12 個 (分析付き)

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视频教程

以上がVue で頻繁に聞かれる主な面接の質問 12 個 (分析付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はsegmentfault.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。