从源码层面解读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 系列;

卑微求个赞,谢谢各位大佬。

前端艺匠
相关文章
相关标签/搜索