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

从源码层面解读16道Vue常考面试题_第1张图片

本文通过 16 道 vue 常考题来解读 vue 部分实现原理,希望让大家更深层次的理解 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) {
  // 增加原型链 _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(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 做了如下事情:

  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) {
    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, {
    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;

  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;
  newDeps: Array;
  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

其实这个问题需要承接上述第三题,再结合下图

从源码层面解读16道Vue常考面试题_第2张图片

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 时给每项元素绑定事件需要用事件代理吗?为什么?
]( https://github.com/Advanced-F...

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 
  // 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, {
    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

从源码层面解读16道Vue常考面试题_第3张图片

你可能感兴趣的