從源碼層面解讀16道Vue常考面試題

致本身

本文經過 16 道 vue 常考題來解讀 vue 部分實現原理,但願讓你們更深層次的理解 vue;javascript

近期本身也實踐了幾個編碼常考題目,但願可以幫助你們加深理解:html

  1. ES5 實現 new
  2. ES5 實現 let/const
  3. ES5 實現 call/apply/bind
  4. ES5 實現 防抖和節流函數
  5. 如何實現一個經過 Promise/A+ 規範的 Promise
  6. 基於 Proxy 實現簡易版 Vue

題目概覽

  1. new Vue() 都作了什麼?
  2. Vue.use 作了什麼?
  3. vue 的響應式?
  4. vue3 爲什麼用 proxy 替代了 Object.defineProperty?
  5. vue 雙向綁定,model 怎麼改變 viewview 怎麼改變 vue
  6. vue 如何對數組方法進行變異?例如 pushpopslice 等;
  7. computed 如何實現?
  8. computedwatch 的區別在哪裏?
  9. 計算屬性和普通屬性的區別?
  10. v-if/v-show/v-html 的原理是什麼,它是如何封裝的?
  11. v-for 給每一個元素綁定事件須要事件代理嗎?
  12. 你知道 key 的做⽤嗎?
  13. 說一下 vue 中全部帶$的方法?
  14. 你知道 nextTick 嗎?
  15. 子組件爲何不能修改父組件傳遞的 props,若是修改了,vue 是如何監聽到並給出警告的?
  16. 父組件和子組件生命週期鉤子的順序?

題目詳解

1. new Vue() 都作了什麼?

構造函數

這裏咱們直接查看源碼 src/core/instance/index.js 查看入口:前端

  1. 首先 new 關鍵字在 JavaScript 中是實例化一個對象;
  2. 這裏 Vuefunction 形式實現的類,new Vue(options) 聲明一個實例對象;
  3. 而後執行 Vue 構造函數,this._init(options) 初始化入參;
import { initMixin } from "./init";
import { stateMixin } from "./state";
import { renderMixin } from "./render";
import { eventsMixin } from "./events";
import { lifecycleMixin } from "./lifecycle";
import { warn } from "../util/index";

function Vue(options) {
  // 構造函數
  if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  // 初始化參數
  this._init(options);
}

// 初始化方法混入
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);

export default Vue;
複製代碼
_init

深刻往下,在 src/core/instance/init.js 中找到 this._init 的聲明vue

// 這裏的混入方法入參 Vue
export function initMixin(Vue: Class<Component>) {
  // 增長原型鏈 _init 即上面構造函數中調用該方法
  Vue.prototype._init = function (options?: Object) {
    // 上下文轉移到 vm
    const vm: Component = this;
    // a uid
    vm._uid = uid++;

    let startTag, endTag;
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`;
      endTag = `vue-perf-end:${vm._uid}`;
      mark(startTag);
    }

    // a flag to avoid this being observed
    vm._isVue = true;
    // 合併配置 options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      // 初始化內部組件實例
      initInternalComponent(vm, options);
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") {
      // 初始化代理 vm
      initProxy(vm);
    } else {
      vm._renderProxy = vm;
    }
    // expose real self
    vm._self = vm;

    // 初始化生命週期函數
    initLifecycle(vm);
    // 初始化自定義事件
    initEvents(vm);
    // 初始化渲染
    initRender(vm);
    // 執行 beforeCreate 生命週期
    callHook(vm, "beforeCreate");
    // 在初始化 state/props 以前初始化注入 inject
    initInjections(vm); // resolve injections before data/props
    // 初始化 state/props 的數據雙向綁定
    initState(vm);
    // 在初始化 state/props 以後初始化 provide
    initProvide(vm); // resolve provide after data/props
    // 執行 created 生命週期
    callHook(vm, "created");

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== "production" && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(`vue ${vm._name} init`, startTag, endTag);
    }

    // 掛載到 dom 元素
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}
複製代碼
小結

綜上,可總結出,new Vue(options) 具體作了以下事情:java

  1. 執行構造函數;
  2. 上下文轉移到 vm;
  3. 若是 options._isComponent 爲 true,則初始化內部組件實例;不然合併配置參數,並掛載到 vm.$options 上面;
  4. 初始化生命週期函數、初始化事件相關、初始化渲染相關;
  5. 執行 beforeCreate 生命週期函數;
  6. 在初始化 state/props 以前初始化注入 inject
  7. 初始化 state/props 的數據雙向綁定;
  8. 在初始化 state/props 以後初始化 provide
  9. 執行 created 生命週期函數;
  10. 掛載到 dom 元素

其實 vue 還在生產環境中記錄了初始化的時間,用於性能分析;node

2. Vue.use 作了什麼?

use

直接查看 src/core/global-api/use.js, 以下react

import { toArray } from "../util/index";

export function initUse(Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 插件緩存數組
    const installedPlugins =
      this._installedPlugins || (this._installedPlugins = []);
    // 已註冊則跳出
    if (installedPlugins.indexOf(plugin) > -1) {
      return this;
    }

    // 附加參數處理,截取第1個參數以後的參數
    const args = toArray(arguments, 1);
    // 第一個參數塞入 this 上下文
    args.unshift(this);
    // 執行 plugin 這裏遵循定義規則
    if (typeof plugin.install === "function") {
      // 插件暴露 install 方法
      plugin.install.apply(plugin, args);
    } else if (typeof plugin === "function") {
      // 插件自己若沒有 install 方法,則直接執行
      plugin.apply(null, args);
    }
    // 添加到緩存數組中
    installedPlugins.push(plugin);
    return this;
  };
}
複製代碼
小結

綜上,能夠總結 Vue.use 作了以下事情:android

  1. 檢查插件是否註冊,若已註冊,則直接跳出;
  2. 處理入參,將第一個參數以後的參數歸集,並在首部塞入 this 上下文;
  3. 執行註冊方法,調用定義好的 install 方法,傳入處理的參數,若沒有 install 方法而且插件自己爲 function 則直接進行註冊;

3. vue 的響應式?

Observer

上代碼,直接查看 src/core/observer/index.js,class Observer,這個方法使得對象/數組可響應git

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor(value: any) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, "__ob__", this);
    if (Array.isArray(value)) {
      // 數組則經過擴展原生方法形式使其可響應
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }

  /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */
  walk(obj: Object) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }

  /** * Observe a list of Array items. */
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}
複製代碼
defineReactive

上代碼,直接查看 src/core/observer/index.js,核心方法 defineReactive,這個方法使得對象可響應,給對象動態添加 getter 和 setteres6

// 使對象中的某個屬性可響應
export function defineReactive( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
  // 初始化 Dep 對象,用做依賴收集
  const dep = new Dep();

  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  let childOb = !shallow && observe(val);
  // 響應式對象核心,定義對象某個屬性的 get 和 set 監聽
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;
      // 監測 watcher 是否存在
      if (Dep.target) {
        // 依賴收集
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== "production" && customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return;
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      // 通知更新
      dep.notify();
    },
  });
}
複製代碼
Dep

依賴收集,咱們須要看一下 Dep 的代碼,它依賴收集的核心,在 src/core/observer/dep.js 中:

import type Watcher from "./watcher";
import { remove } from "../util/index";
import config from "../config";

let uid = 0;

/** * A dep is an observable that can have multiple * directives subscribing to it. */
export default class Dep {
  // 靜態屬性,全局惟一 Watcher
  // 這裏比較巧妙,由於在同一時間只能有一個全局的 Watcher 被計算
  static target: ?Watcher;
  id: number;
  // watcher 數組
  subs: Array<Watcher>;

  constructor() {
    this.id = uid++;
    this.subs = [];
  }

  addSub(sub: Watcher) {
    this.subs.push(sub);
  }

  removeSub(sub: Watcher) {
    remove(this.subs, sub);
  }

  depend() {
    if (Dep.target) {
      // Watcher 中收集依賴
      Dep.target.addDep(this);
    }
  }

  notify() {
    // stabilize the subscriber list first
    const subs = this.subs.slice();
    if (process.env.NODE_ENV !== "production" && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id);
    }
    // 遍歷全部的 subs,也就是 Watcher 的實例數組,而後調用每個 watcher 的 update 方法
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
// 全局惟一的 Watcher
Dep.target = null;
const targetStack = [];

export function pushTarget(target: ?Watcher) {
  targetStack.push(target);
  Dep.target = target;
}

export function popTarget() {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}
複製代碼
Watcher

Dep 是對 Watcher 的一種管理,下面咱們來看一下 Watcher, 在 src/core/observer/watcher.js

let uid = 0;

/** * 一個 Watcher 分析一個表達式,收集依賴項, 並在表達式值更改時觸發回調。 * 用於 $watch() api 和指令 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new Set();
    this.newDepIds = new Set();
    this.expression =
      process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
    // parse expression for getter
    if (typeof expOrFn === "function") {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        process.env.NODE_ENV !== "production" &&
          warn(
            `Failed watching path: "${expOrFn}" ` +
              "Watcher only accepts simple dot-delimited paths. " +
              "For full control, use a function instead.",
            vm
          );
      }
    }
    this.value = this.lazy ? undefined : this.get();
  }

  // 評估getter,並從新收集依賴項。
  get() {
    // 實際上就是把 Dep.target 賦值爲當前的渲染 watcher 並壓棧(爲了恢復用)。
    pushTarget(this);
    let value;
    const vm = this.vm;
    try {
      // this.getter 對應就是 updateComponent 函數,這實際上就是在執行:
      // 這裏須要追溯 new Watcher 執行的地方,是在
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      } else {
        throw e;
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      // 遞歸深度遍歷每個屬性,使其均可以被依賴收集
      if (this.deep) {
        traverse(value);
      }
      // 出棧
      popTarget();
      // 清理依賴收集
      this.cleanupDeps();
    }
    return value;
  }

  // 添加依賴
  // 在 Dep 中會調用
  addDep(dep: Dep) {
    const id = dep.id;
    // 避免重複收集
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        // 把當前的 watcher 訂閱到這個數據持有的 dep 的 subs 中
        // 目的是爲後續數據變化時候能通知到哪些 subs 作準備
        dep.addSub(this);
      }
    }
  }

  // 清理依賴
  // 每次添加完新的訂閱,會移除掉舊的訂閱,因此不會有任何浪費
  cleanupDeps() {
    let i = this.deps.length;
    // 首先遍歷 deps,移除對 dep.subs 數組中 Wathcer 的訂閱
    while (i--) {
      const dep = this.deps[i];
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this);
      }
    }
    let tmp = this.depIds;
    this.depIds = this.newDepIds;
    this.newDepIds = tmp;
    this.newDepIds.clear();
    tmp = this.deps;
    this.deps = this.newDeps;
    this.newDeps = tmp;
    this.newDeps.length = 0;
  }

  // 發佈接口
  // 依賴更新的時候觸發
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      // computed 數據
      this.dirty = true;
    } else if (this.sync) {
      // 同步數據更新
      this.run();
    } else {
      // 正常數據會通過這裏
      // 派發更新
      queueWatcher(this);
    }
  }

  // 調度接口,用於執行更新
  run() {
    if (this.active) {
      const value = this.get();
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // 設置新的值
        const oldValue = this.value;
        this.value = value;
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue);
          } catch (e) {
            handleError(
              e,
              this.vm,
              `callback for watcher "${this.expression}"`
            );
          }
        } else {
          this.cb.call(this.vm, value, oldValue);
        }
      }
    }
  }

  /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */
  evaluate() {
    this.value = this.get();
    this.dirty = false;
  }

  /** * Depend on all deps collected by this watcher. */
  depend() {
    let i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  }

  /** * Remove self from all dependencies' subscriber list. */
  teardown() {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this);
      }
      let i = this.deps.length;
      while (i--) {
        this.deps[i].removeSub(this);
      }
      this.active = false;
    }
  }
}
複製代碼
小結

綜上響應式核心代碼,咱們能夠描述響應式的執行過程:

  1. 根據數據類型來作不一樣處理,若是是對象則 Object.defineProperty() 監聽數據屬性的 get 來進行數據依賴收集,再經過 get 來完成數據更新的派發;若是是數組若是是數組則經過覆蓋 該數組原型的⽅法,擴展它的 7 個變動⽅法(push/pop/shift/unshift/splice/reverse/sort),經過監聽這些方法能夠作到依賴收集和派發更新;
  2. Dep 是主要作依賴收集,收集的是當前上下文做爲 Watcher,全局有且僅有一個 Dep.target,經過 Dep 能夠作到控制當前上下文的依賴收集和通知 Watcher 派發更新;
  3. Watcher 鏈接表達式和值,說白了就是 watcher 鏈接視圖層的依賴,並能夠觸發視圖層的更新,與 Dep 緊密結合,經過 Dep 來控制其對視圖層的監聽

4. vue3 爲什麼用 proxy 替代了 Object.defineProperty?

traverse

截取上面 Watcher 中部分代碼

if (this.deep) {
  // 這裏其實遞歸遍歷屬性用做依賴收集
  traverse(value);
}
複製代碼

再查看 src/core/observer/traverse.jstraverse 的實現,以下:

const seenObjects = new Set();

// 遞歸遍歷對象,將全部屬性轉換爲 getter
// 使每一個對象內嵌套屬性做爲依賴收集項
export function traverse(val: any) {
  _traverse(val, seenObjects);
  seenObjects.clear();
}

function _traverse(val: any, seen: SimpleSet) {
  let i, keys;
  const isA = Array.isArray(val);
  if (
    (!isA && !isObject(val)) ||
    Object.isFrozen(val) ||
    val instanceof VNode
  ) {
    return;
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id;
    if (seen.has(depId)) {
      return;
    }
    seen.add(depId);
  }
  if (isA) {
    i = val.length;
    while (i--) _traverse(val[i], seen);
  } else {
    keys = Object.keys(val);
    i = keys.length;
    while (i--) _traverse(val[keys[i]], seen);
  }
}
複製代碼
小結

再綜上一題代碼實際瞭解,其實咱們看到一些弊端:

  1. Watcher 監聽 對屬性作了遞歸遍歷,這裏可能會形成性能損失;
  2. defineReactive 遍歷屬性對當前存在的屬性 Object.defineProperty() 做依賴收集,可是對於不存在,或者刪除屬性,則監聽不到;從而會形成 對新增或者刪除的屬性沒法作到響應式,只能經過 Vue.set/delete 這類 api 才能夠作到;
  3. 對於 es6 中新產⽣的 MapSet 這些數據結構不⽀持

5. vue 雙向綁定,Model 怎麼改變 ViewView 怎麼改變 Model

其實這個問題須要承接上述第三題,再結合下圖

響應式原理

Model 改變 View:

  1. defineReactive 中經過 Object.defineProperty 使 data 可響應;
  2. Dep 在 getter 中做依賴收集,在 setter 中做派發更新;
  3. dep.notify() 通知 Watcher 更新,最終調用 vm._render() 更新 UI;

View 改變 Model: 其實同上理,View 與 data 的數據關聯在了一塊兒,View 經過事件觸發 data 的變化,從而觸發了 setter,這就構成了一個雙向循環綁定了;

6. vue 如何對數組方法進行變異?例如 pushpopslice 等;

這個問題,咱們直接從源碼找答案,這裏咱們截取上面 Observer 部分源碼,先來追溯一下,Vue 怎麼實現數組的響應:

constructor(value: any) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, "__ob__", this);
  if (Array.isArray(value)) {
    // 數組則經過擴展原生方法形式使其可響應
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value);
  }
}
複製代碼
arrayMethods

這裏須要查看一下 arrayMethods 這個對象,在 src/core/observer/array.js

import { def } from "../util/index";

const arrayProto = Array.prototype;
// 複製數組原型鏈,並建立一個空對象
// 這裏使用 Object.create 是爲了避免污染 Array 的原型
export const arrayMethods = Object.create(arrayProto);

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

// 攔截突變方法併發出事件
// 攔截了數組的 7 個方法
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;
  });
});
複製代碼
def

def 使對象可響應,在 src/core/util/lang.js

export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
  });
}
複製代碼
小結
  1. Object.create(Array.prototype) 複製 Array 原型鏈爲新的對象;
  2. 攔截了數組的 7 個方法的執行,並使其可響應,7 個方法分別爲:push, pop, shift, unshift, splice, sort, reverse
  3. 當數組調用到這 7 個方法的時候,執行 ob.dep.notify() 進行派發通知 Watcher 更新;
附加思考

不過,vue 對數組的監聽仍是有限制的,以下:

  1. 數組經過索引改變值的時候監聽不到,好比:array[2] = newObj
  2. 數組長度變化沒法監聽

這些操做都須要經過 Vue.set/del 去操做才行;

7. computed 如何實現?

initComputed

這個方法用於初始化 options.computed 對象, 這裏仍是上源碼,在 src/core/instance/state.js 中,這個方法是在 initState 中調用的

const computedWatcherOptions = { lazy: true };

function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  // 建立一個空對象
  const watchers = (vm._computedWatchers = Object.create(null));
  // computed properties are just getters during SSR
  const isSSR = isServerRendering();

  for (const key in computed) {
    // 遍歷拿到每一個定義的 userDef
    const userDef = computed[key];
    const getter = typeof userDef === "function" ? userDef : userDef.get;
    // 沒有 getter 則 warn
    if (process.env.NODE_ENV !== "production" && getter == null) {
      warn(`Getter is missing for computed property "${key}".`, vm);
    }

    if (!isSSR) {
      // 爲每一個 computed 屬性建立 watcher
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions // {lazy: true}
      );
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      // 定義 vm 中未定義的計算屬性
      defineComputed(vm, key, userDef);
    } else if (process.env.NODE_ENV !== "production") {
      if (key in vm.$data) {
        // 判斷 key 是否是在 data
        warn(`The computed property "${key}" is already defined in data.`, vm);
      } else if (vm.$options.props && key in vm.$options.props) {
        // 判斷 key 是否是在 props 中
        warn(
          `The computed property "${key}" is already defined as a prop.`,
          vm
        );
      }
    }
  }
}
複製代碼
defineComputed

這個方法用做定義 computed 中的屬性,繼續看代碼:

export function defineComputed( target: any, key: string, userDef: Object | Function ) {
  const shouldCache = !isServerRendering();
  if (typeof userDef === "function") {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef);
    sharedPropertyDefinition.set = noop;
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop;
    sharedPropertyDefinition.set = userDef.set || noop;
  }
  if (
    process.env.NODE_ENV !== "production" &&
    sharedPropertyDefinition.set === noop
  ) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      );
    };
  }
  // 定義計算屬性的 get / set
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

// 返回計算屬性對應的 getter
function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        // watcher 檢查是 computed 屬性的時候 會標記 dirty 爲 true
        // 這裏是 computed 的取值邏輯, 執行 evaluate 以後 則 dirty false,直至下次觸發
        // 其實這裏就能夠說明 computed 屬性實際上是觸發了 getter 屬性以後才進行計算的,而觸發的媒介即是 computed 引用的其餘屬性觸發 getter,再觸發 dep.update(), 繼而 觸發 watcher 的 update
        watcher.evaluate();
        // --------------------------- Watcher --------------------------------
        // 這裏截取部分 Watcher 的定義
        // update 定義
        // update () {
        // /* istanbul ignore else */
        // if (this.lazy) {
        // // 觸發更新的時候標記計算屬性
        // this.dirty = true
        // } else if (this.sync) {
        // this.run()
        // } else {
        // queueWatcher(this)
        // }
        // }
        // evaluate 定義
        // evaluate () {
        // this.value = this.get()
        // // 取值後標記 取消
        // this.dirty = false
        // }
        // ------------------------- Watcher ----------------------------------
      }
      if (Dep.target) {
        // 收集依賴
        watcher.depend();
      }
      return watcher.value;
    }
  };
}

function createGetterInvoker(fn) {
  return function computedGetter() {
    return fn.call(this, this);
  };
}
複製代碼
小結

綜上代碼分析過程,總結 computed 屬性的實現過程以下(如下分析過程均忽略了 ssr 狀況):

  1. Object.create(null) 建立一個空對象用做緩存 computed 屬性的 watchers,並緩存在 vm._computedWatchers 中;
  2. 遍歷計算屬性,拿到用戶定義的 userDef,爲每一個屬性定義 Watcher,標記 Watcher 屬性 lazy: true;
  3. 定義 vm 中未定義過的 computed 屬性,defineComputed(vm, key, userDef),已存在則判斷是在 data 或者 props 中已定義並相應警告;
  4. 接下來就是定義 computed 屬性的 gettersetter,這裏主要是看 createComputedGetter 裏面的定義:當觸發更新則檢測 watcher 的 dirty 標記,則執行 watcher.evaluate() 方法執行計算,而後依賴收集;
  5. 這裏再追溯 watcher.dirty 屬性邏輯,在 watcher.update 中 當遇到 computed 屬性時候被標記爲 dirty:false,這裏其實能夠看出 computed 屬性的計算前提必須是引用的正常屬性的更新觸發了 Dep.update(),繼而觸發對應 watcher.update 進行標記 dirty:true,繼而在計算屬性 getter 的時候纔會觸發更新,不然不更新;

以上即是計算屬性的實現邏輯,部分代碼邏輯須要追溯上面第三題響應式的部分 Dep/Watcher 的觸發邏輯;

8. computedwatch 的區別在哪裏?

initWatch

這裏仍是老樣子,上代碼,在 src/core/instance/state.js 中:

function initWatch(vm: Component, watch: Object) {
  // 遍歷 watch 對象屬性
  for (const key in watch) {
    const handler = watch[key];
    // 數組則進行遍歷建立 watcher
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}

// 建立 watcher 監聽
function createWatcher( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) {
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  // handler 傳入字符串,則直接從 vm 中獲取函數方法
  if (typeof handler === "string") {
    handler = vm[handler];
  }
  // 建立 watcher 監聽
  return vm.$watch(expOrFn, handler, options);
}
複製代碼
$watch

咱們還須要看一下 $watch 的邏輯,在 src/core/instance/state.js 中:

Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // 建立 watch 屬性的 Watcher 實例
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }

    // 用做銷燬
    return function unwatchFn () {
      // 移除 watcher 的依賴
      watcher.teardown()
    }
  }
}
複製代碼
小結

綜上代碼分析,先看來看一下 watch 屬性的實現邏輯:

  1. 遍歷 watch 屬性分別建立屬性的 Watcher 監聽,這裏能夠看出其實該屬性並未被 Dep 收集依賴;
  2. 能夠分析 watch 監聽的屬性 必然是已經被 Dep 收集依賴的屬性了(data/props 中的屬性),進行對應屬性觸發更新的時候纔會觸發 watch 屬性的監聽回調;

這裏就能夠分析 computed 與 watch 的異同:

  1. computed 屬性的更新須要依賴於其引用屬性的更新觸發標記 dirty: true,進而觸發 computed 屬性 getter 的時候纔會觸發其自己的更新,不然其不更新;
  2. watch 屬性則是依賴於自己已被 Dep 收集依賴的部分屬性,即做爲 data/props 中的某個屬性的尾隨 watcher,在監聽屬性更新時觸發 watcher 的回調;不然監聽則無心義;

這裏再引伸一下使用場景:

  1. 若是一個數據依賴於其餘數據,那麼就使用 computed 屬性;
  2. 若是你須要在某個數據變化時作一些事情,使用 watch 來觀察這個數據變化;

9. 計算屬性和普通屬性的區別?

這個題目跟上題相似,區別以下:

  1. 普通屬性都是基於 gettersetter 的正常取值和更新;
  2. computed 屬性是依賴於內部引用普通屬性的 setter 變動從而標記 watcherdirty 標記爲 true,此時纔會觸發更新;

10. v-if/v-show/v-html 的原理是什麼,它是如何封裝的?

v-if

先來看一下 v-if 的實現,首先 vue 編譯 template 模板的時候會先生成 ast 靜態語法樹,而後進行標記靜態節點,再以後生成對應的 render 函數,這裏就直接看下 genIf 的代碼,在src/compiler/codegen/index.js中:

export function genIf( el: any, state: CodegenState, altGen?: Function, altEmpty?: string ): string {
  el.ifProcessed = true; // 標記避免遞歸,標記已經處理過
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty);
}

function genIfConditions( conditions: ASTIfConditions, state: CodegenState, altGen?: Function, altEmpty?: string ): string {
  if (!conditions.length) {
    return altEmpty || "_e()";
  }

  const condition = conditions.shift();
  // 這裏返回的是一個三元表達式
  if (condition.exp) {
    return `(${condition.exp})?${genTernaryExp( condition.block )}:${genIfConditions(conditions, state, altGen, altEmpty)}`;
  } else {
    return `${genTernaryExp(condition.block)}`;
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp(el) {
    return altGen
      ? altGen(el, state)
      : el.once
      ? genOnce(el, state)
      : genElement(el, state);
  }
}
複製代碼

v-if 在 template 生成 ast 以後 genIf 返回三元表達式,在渲染的時候僅渲染表達式生效部分;

v-show

這裏截取 v-show 指令的實現邏輯,在 src/platforms/web/runtime/directives/show.js 中:

export default {
  bind(el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
    vnode = locateNode(vnode);
    const transition = vnode.data && vnode.data.transition;
    const originalDisplay = (el.__vOriginalDisplay =
      el.style.display === "none" ? "" : el.style.display);
    if (value && transition) {
      vnode.data.show = true;
      enter(vnode, () => {
        el.style.display = originalDisplay;
      });
    } else {
      el.style.display = value ? originalDisplay : "none";
    }
  },

  update(el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) {
    /* istanbul ignore if */
    if (!value === !oldValue) return;
    vnode = locateNode(vnode);
    const transition = vnode.data && vnode.data.transition;
    if (transition) {
      vnode.data.show = true;
      if (value) {
        enter(vnode, () => {
          el.style.display = el.__vOriginalDisplay;
        });
      } else {
        leave(vnode, () => {
          el.style.display = "none";
        });
      }
    } else {
      el.style.display = value ? el.__vOriginalDisplay : "none";
    }
  },

  unbind(
    el: any,
    binding: VNodeDirective,
    vnode: VNodeWithData,
    oldVnode: VNodeWithData,
    isDestroy: boolean
  ) {
    if (!isDestroy) {
      el.style.display = el.__vOriginalDisplay;
    }
  },
};
複製代碼

這裏其實比較明顯了,v-show 根據表達式的值最終操做的是 style.display

v-html

v-html 比較簡單,最終操做的是 innerHTML,咱們仍是看代碼,在 src/platforms/compiler/directives/html.js 中:

import { addProp } from "compiler/helpers";

export default function html(el: ASTElement, dir: ASTDirective) {
  if (dir.value) {
    addProp(el, "innerHTML", `_s(${dir.value})`, dir);
  }
}
複製代碼
小結

綜上代碼證實:

  1. v-iftemplate 生成 ast 以後 genIf 返回三元表達式,在渲染的時候僅渲染表達式生效部分;
  2. v-show 根據表達式的值最終操做的是 style.display,並標記當前 vnode.data.show 屬性;
  3. v-html 最終操做的是 innerHTML,將當前值 innerHTML 到當前標籤;

11. v-for 給每一個元素綁定事件須要事件代理嗎?

首先,咱們先來看一下 v-for 的實現,同上面 v-if,在模板渲染過程當中由genFor 處理,在 src/compiler/codegen/index.js 中:

export function genFor( el: any, state: CodegenState, altGen?: Function, altHelper?: string ): string {
  const exp = el.for;
  const alias = el.alias;
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : "";
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : "";

  if (
    process.env.NODE_ENV !== "production" &&
    state.maybeComponent(el) &&
    el.tag !== "slot" &&
    el.tag !== "template" &&
    !el.key
  ) {
    state.warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
        `v-for should have explicit keys. ` +
        `See https://vuejs.org/guide/list.html#key for more info.`,
      el.rawAttrsMap["v-for"],
      true /* tip */
    );
  }

  el.forProcessed = true; // 標記避免遞歸,標記已經處理過
  return (
    `${altHelper || "_l"}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
    `return ${(altGen || genElement)(el, state)}` +
    "})"
  );
  // 僞代碼解析後大體以下
  // _l(data, function (item, index) {
  // return genElement(el, state);
  // });
}
複製代碼

這裏其實能夠看出,genFor 最終返回了一串僞代碼(見註釋)最終每一個循環返回 genElement(el, state),其實這裏能夠大膽推測,vue 並無單獨在 v-for 對事件作委託處理,只是單獨處理了每次循環的處理;
能夠確認的是,vue 在 v-for 中並無處理事件委託,處於性能考慮,最好本身加上事件委託,這裏有個帖子有分析對比,第 94 題:vue 在 v-for 時給每項元素綁定事件須要用事件代理嗎?爲何?

12. 你知道 key 的做⽤嗎?

key 可預想的是 vue 拿來給 vnode 做惟一標識的,下面咱們先來看下 key 到底被拿來作啥事,在 src/core/vdom/patch.js 中:

updateChildren
function updateChildren( parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly ) {
  let oldStartIdx = 0;
  let newStartIdx = 0;
  let oldEndIdx = oldCh.length - 1;
  let oldStartVnode = oldCh[0];
  let oldEndVnode = oldCh[oldEndIdx];
  let newEndIdx = newCh.length - 1;
  let newStartVnode = newCh[0];
  let newEndVnode = newCh[newEndIdx];
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly;

  if (process.env.NODE_ENV !== "production") {
    checkDuplicateKeys(newCh);
  }

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(
        oldStartVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      );
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(
        oldEndVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      patchVnode(
        oldStartVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
      canMove &&
        nodeOps.insertBefore(
          parentElm,
          oldStartVnode.elm,
          nodeOps.nextSibling(oldEndVnode.elm)
        );
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Vnode moved left
      patchVnode(
        oldEndVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      );
      canMove &&
        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      if (isUndef(oldKeyToIdx))
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
      if (isUndef(idxInOld)) {
        // New element
        createElm(
          newStartVnode,
          insertedVnodeQueue,
          parentElm,
          oldStartVnode.elm,
          false,
          newCh,
          newStartIdx
        );
      } else {
        vnodeToMove = oldCh[idxInOld];
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(
            vnodeToMove,
            newStartVnode,
            insertedVnodeQueue,
            newCh,
            newStartIdx
          );
          oldCh[idxInOld] = undefined;
          canMove &&
            nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
        } else {
          // same key but different element. treat as new element
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          );
        }
      }
      newStartVnode = newCh[++newStartIdx];
    }
  }
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(
      parentElm,
      refElm,
      newCh,
      newStartIdx,
      newEndIdx,
      insertedVnodeQueue
    );
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
  }
}
複製代碼

這段代碼是 vue diff 算法的核心代碼了,用做比較同級節點是否相同,批量更新的,可謂是性能核心了,以上能夠看下 sameVnode 比較節點被用了屢次,下面咱們來看下是怎麼比較兩個相同節點的

sameVnode
function sameVnode(a, b) {
  return (
    // 首先就是比較 key,key 相同是必要條件
    a.key === b.key &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)))
  );
}
複製代碼

能夠看到 key 是 diff 算法用來比較節點的必要條件,可想而知 key 的重要性;

小結

以上,咱們瞭解到 key 的關鍵性,這裏能夠總結下:

key 在 diff 算法比較中用做比較兩個節點是否相同的重要標識,相同則複用,不相同則刪除舊的建立新的;

  1. 相同上下文的 key 最好是惟一的;
  2. 別用 index 來做爲 key,index 相對於列表元素來講是可變的,沒法標記原有節點,好比我新增和插入一個元素,index 對於原來節點就發生了位移,就沒法 diff 了;

13. 說一下 vue 中全部帶$的方法?

實例 property
  • vm.$data: Vue 實例觀察的數據對象。Vue 實例代理了對其 data 對象 property 的訪問。
  • vm.$props: 當前組件接收到的 props 對象。Vue 實例代理了對其 props 對象 property 的訪問。
  • vm.$el: Vue 實例使用的根 DOM 元素。
  • vm.$options: 用於當前 Vue 實例的初始化選項。
  • vm.$parent: 父實例,若是當前實例有的話。
  • vm.$root: 當前組件樹的根 Vue 實例。若是當前實例沒有父實例,此實例將會是其本身。
  • vm.$children: 當前實例的直接子組件。須要注意 $children 並不保證順序,也不是響應式的。若是你發現本身正在嘗試使用 $children 來進行數據綁定,考慮使用一個數組配合 v-for 來生成子組件,而且使用 Array 做爲真正的來源。
  • vm.$slots: 用來訪問被插槽分發的內容。每一個具名插槽有其相應的 property (例如:v-slot:foo 中的內容將會在 vm.$slots.foo 中被找到)。default property 包括了全部沒有被包含在具名插槽中的節點,或 v-slot:default 的內容。
  • vm.$scopedSlots: 用來訪問做用域插槽。對於包括 默認 slot 在內的每個插槽,該對象都包含一個返回相應 VNode 的函數。
  • vm.$refs: 一個對象,持有註冊過 ref attribute 的全部 DOM 元素和組件實例。
  • vm.$isServer: 當前 Vue 實例是否運行於服務器。
  • vm.$attrs: 包含了父做用域中不做爲 prop 被識別 (且獲取) 的 attribute 綁定 (class 和 style 除外)。當一個組件沒有聲明任何 prop 時,這裏會包含全部父做用域的綁定 (class 和 style 除外),而且能夠經過 v-bind="$attrs" 傳入內部組件——在建立高級別的組件時很是有用。
  • vm.$listeners: 包含了父做用域中的 (不含 .native 修飾器的) v-on 事件監聽器。它能夠經過 v-on="$listeners" 傳入內部組件——在建立更高層次的組件時很是有用。
實例方法 / 數據
  • vm.$watch( expOrFn, callback, [options] ): 觀察 Vue 實例上的一個表達式或者一個函數計算結果的變化。回調函數獲得的參數爲新值和舊值。表達式只接受監督的鍵路徑。對於更復雜的表達式,用一個函數取代。
  • vm.$set( target, propertyName/index, value ): 這是全局 Vue.set 的別名。
  • vm.$delete( target, propertyName/index ): 這是全局 Vue.delete 的別名。
實例方法 / 事件
  • vm.$on( event, callback ): 監聽當前實例上的自定義事件。事件能夠由 vm.$emit 觸發。回調函數會接收全部傳入事件觸發函數的額外參數。
  • vm.$once( event, callback ): 監聽一個自定義事件,可是隻觸發一次。一旦觸發以後,監聽器就會被移除。
  • vm.$off( [event, callback] ): 移除自定義事件監聽器。
    • 若是沒有提供參數,則移除全部的事件監聽器;
    • 若是隻提供了事件,則移除該事件全部的監聽器;
    • 若是同時提供了事件與回調,則只移除這個回調的監聽器。
  • vm.$emit( eventName, […args] ): 觸發當前實例上的事件。附加參數都會傳給監聽器回調。
實例方法 / 生命週期
  • vm.$mount( [elementOrSelector] )

    • 若是 Vue 實例在實例化時沒有收到 el 選項,則它處於「未掛載」狀態,沒有關聯的 DOM 元素。可使用 vm.$mount() 手動地掛載一個未掛載的實例。
    • 若是沒有提供 elementOrSelector 參數,模板將被渲染爲文檔以外的的元素,而且你必須使用原生 DOM API 把它插入文檔中。
    • 這個方法返回實例自身,於是能夠鏈式調用其它實例方法。
  • vm.$forceUpdate(): 迫使 Vue 實例從新渲染。注意它僅僅影響實例自己和插入插槽內容的子組件,而不是全部子組件。

  • vm.$nextTick( [callback] ): 將回調延遲到下次 DOM 更新循環以後執行。在修改數據以後當即使用它,而後等待 DOM 更新。它跟全局方法 Vue.nextTick 同樣,不一樣的是回調的 this 自動綁定到調用它的實例上。

  • vm.$destroy(): 徹底銷燬一個實例。清理它與其它實例的鏈接,解綁它的所有指令及事件監聽器。

    • 觸發 beforeDestroy 和 destroyed 的鉤子。

14. 你知道 nextTick 嗎?

直接上代碼,在 src/core/util/next-tick.js 中:

import { noop } from "shared/util";
import { handleError } from "./error";
import { isIE, isIOS, isNative } from "./env";

export let isUsingMicroTask = false;

const callbacks = [];
let pending = false;

function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

//這裏咱們使用微任務使用異步延遲包裝器。
//在2.5中,咱們使用(宏)任務(與微任務結合使用)。
//可是,當狀態在從新繪製以前被更改時,它會有一些微妙的問題
//(例如#6813,輸出轉換)。
// 此外,在事件處理程序中使用(宏)任務會致使一些奇怪的行爲
//不能規避(例如#710九、#715三、#754六、#783四、#8109)。
//所以,咱們如今再次在任何地方使用微任務。
//這種權衡的一個主要缺點是存在一些場景
//微任務的優先級太高,並在二者之間被觸發
//順序事件(例如#452一、#6690,它們有解決方案)
//甚至在同一事件的冒泡(#6566)之間。
let timerFunc;

// nextTick行爲利用了能夠訪問的微任務隊列
//經過任何一個原生承諾。而後或MutationObserver。
// MutationObserver得到了更普遍的支持,但它受到了嚴重的干擾
// UIWebView在iOS >= 9.3.3時觸發的觸摸事件處理程序。它
//觸發幾回後徹底中止工做…因此,若是本地
// Promise可用,咱們將使用:
if (typeof Promise !== "undefined" && isNative(Promise)) {
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);

    //在有問題的UIWebViews中,承諾。而後不徹底打破,可是
    //它可能陷入一種奇怪的狀態,即回調被推入
    // 可是隊列不會被刷新,直到瀏覽器刷新
    //須要作一些其餘的工做,例如處理定時器。所以,咱們能夠
    //經過添加空計時器來「強制」刷新微任務隊列。
    if (isIOS) setTimeout(noop);
  };
  isUsingMicroTask = true;
} else if (
  !isIE &&
  typeof MutationObserver !== "undefined" &&
  (isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
  //在原生 Promise 不可用的狀況下使用MutationObserver,
  //例如PhantomJS, iOS7, android4.4
  // (#6466 MutationObserver在IE11中不可靠)
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () => {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
  //退回到setimmediation。
  //技術上它利用了(宏)任務隊列,
  //但它仍然是比setTimeout更好的選擇。
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve;
  // 入隊列
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, "nextTick");
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    timerFunc();
  }

  // 這是當 nextTick 不傳 cb 參數的時候,提供一個 Promise 化的調用
  if (!cb && typeof Promise !== "undefined") {
    return new Promise((resolve) => {
      _resolve = resolve;
    });
  }
}
複製代碼
小結

結合以上代碼,總結以下:

  1. 回調函數先入隊列,等待;
  2. 執行 timerFunc,Promise 支持則使用 Promise 微隊列形式,不然,再非 IE 狀況下,若支持 MutationObserver,則使用 MutationObserver 一樣以 微隊列的形式,再不支持則使用 setImmediate,再不濟就使用 setTimeout;
  3. 執行 flushCallbacks,標記 pending 完成,而後先複製 callback,再清理 callback;

以上即是 vue 異步隊列的一個實現,主要是優先以(promise/MutationObserver)微任務的形式去實現(其次纔是(setImmediate、setTimeout)宏任務去實現),等待當前宏任務完成後,便執行當下全部的微任務

15. 子組件爲何不能修改父組件傳遞的 props,若是修改了,vue 是如何監聽到並給出警告的?

initProps

這裏能夠看一下 initProps 的實現邏輯,先看一下 props 的初始化流程:

function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {};
  const props = (vm._props = {});
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = (vm.$options._propKeys = []);
  const isRoot = !vm.$parent;
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false);
  }
  // props 屬性遍歷監聽
  for (const key in propsOptions) {
    keys.push(key);
    const value = validateProp(key, propsOptions, propsData, vm);
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") {
      const hyphenatedKey = hyphenate(key);
      if (
        isReservedAttribute(hyphenatedKey) ||
        config.isReservedAttr(hyphenatedKey)
      ) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        );
      }
      // props 數據綁定監聽
      defineReactive(props, key, value, () => {
        // 開發環境下會提示 warn
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
              `overwritten whenever the parent component re-renders. ` +
              `Instead, use a data or computed property based on the prop's ` +
              `value. Prop being mutated: "${key}"`,
            vm
          );
        }
      });
    } else {
      // props 數據綁定監聽
      defineReactive(props, key, value);
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key);
    }
  }
  toggleObserving(true);
}
複製代碼

分析代碼發現 props 單純作了數據淺綁定監聽,提示是在開發環境中作的校驗

小結

如上可知,props 初始化時對 props 屬性遍歷 defineReactive(props, key, value) 作了數據淺綁定監聽:

  1. 若是 value 爲基本屬性(開發環境中),當更改 props 的時候則會 warn,可是這裏修改並不會改變父級的屬性,由於這裏的基礎數據是值拷貝;
  2. 若是 value 爲對象或者數組時,則更改父級對象值的時候也會 warn(可是不會影響父級 props),可是當修改其 屬性的時候則不會 warn,而且會直接修改父級的 props 對應屬性值;
  3. 注意這裏父級的 props 在組件建立時是數據拷貝過來的;

繼續分析,若是 vue 容許子組件修改父組件的狀況下,這裏 props 將須要在父組件以及子組件中都進行數據綁定,這樣講致使屢次監聽,並且不利於維護,而且可想而知,容易邏輯交叉,不容易維護;
因此 vue 在父子組件的數據中是以單向數據流來作的處理,這樣父子的業務數據邏輯不易交叉,而且易於定位問題源頭;

16. 父組件和子組件生命週期鉤子的順序?

渲染過程

從父到子,再由子到父;(由外到內再由內到外)

  • 父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
子組件更新過程
  • 父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
父組件更新過程
  • 父 beforeUpdate->父 updated
銷燬過程
  • 父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

展望

感謝閱讀,但願對你們有所幫助,後續打算:

  1. 解讀 vuex 源碼常考題;
  2. 解讀 react-router 源碼常考題;
  3. 實現本身的 vue/vuex/react-router 系列;

卑微求個贊,謝謝各位大佬。

前端藝匠
相關文章
相關標籤/搜索