从源码层面解读16道Vue常考面试题

Posted 前端迷

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从源码层面解读16道Vue常考面试题相关的知识,希望对你有一定的参考价值。

本文通过 16 道 vue 常考题来解读 vue 部分实现原理,希望让大家更深层次的理解 vue;

近期自己也实践了几个编码常考题目,希望能够帮助大家加深理解:

  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
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) 具体做了如下事情:

  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 还在生产环境中记录了初始化的时间,用于性能分析;

2. Vue.use 做了什么?

use

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

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(arguments1);
    // 第一个参数塞入 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 做了如下事情:

  1. 检查插件是否注册,若已注册,则直接跳出;
  2. 处理入参,将第一个参数之后的参数归集,并在首部塞入 this 上下文;
  3. 执行注册方法,调用定义好的 install 方法,传入处理的参数,若没有 install 方法并且插件本身为 function 则直接进行注册;

3. vue 的响应式?

Observer

上代码,直接查看 src/core/observer/index.js,class Observer,这个方法使得对象/数组可响应

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 和 setter

// 使对象中的某个属性可响应
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, {
    enumerabletrue,
    configurabletrue,
    getfunction 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;
    },
    setfunction 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,
    writabletrue,
    configurabletrue,
  });
}
小结
  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 = { lazytrue };

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(thisthis);
  };
}
小结

综上代码分析过程,总结 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,输出转换)。
// 此外,在事件处理程序中使用(宏)任务会导致一些奇怪的行为
//不能规避(例如#7109、#7153、#7546、#7834、#8109)。
//因此,我们现在再次在任何地方使用微任务。
//这种权衡的一个主要缺点是存在一些场景
//微任务的优先级过高,并在两者之间被触发
//顺序事件(例如#4521、#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, {
    characterDatatrue,
  });
  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 系列;

欢迎关注,敬请期待。


以上是关于从源码层面解读16道Vue常考面试题的主要内容,如果未能解决你的问题,请参考以下文章

互联网大厂面试揭秘:MySQL查询常考的十道面试题

百度前端常考vue面试题(附答案)

源码解读JDK1.8 中 ConcurrentHashMap 不支持空键值对源码剖析

阿里前端常考vue面试题汇总

前端面试之道 (高清彩图)

2021年互联网企业软件测试面试题(常考)