Vue2.x 源码剖析之响应式原理

Study Notes

本博主会持续更新各种前端的技术,如果各位道友喜欢,可以关注、收藏、点赞下本博主的文章。

Vue.js 源码剖析-响应式原理

响应式处理的入口

/* @flow */

import ...;

const sharedPropertyDefinition = {...};

export function proxy(target: Object, sourceKey: string, key: string) {...}

export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts.methods);
  // 判断options中是否存在data属性
  if (opts.data) {
    // 如果存在data属性,则给其添加响应式
    initData(vm);
  } else {
    // 如果不存在data属性,则给给vue对象,添加_data属性,初始化为空对象,并给其添加响应式
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

function initProps(vm: Component, propsOptions: Object) {...}

function initData(vm: Component) {
  let data = vm.$options.data;
  // 判断data是否是函数,如果是一个函数,则是组件中的data,将其this指向vue
  // 不是函数,则判断是否存在。不存在则赋值为空对象
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};
  // 判断data是否是[object Object],如果不是,则将data赋值为空对象
  // 并在非production环境下,抛出警告
  if (!isPlainObject(data)) {
    data = {};
    process.env.NODE_ENV !== 'production' &&
      warn(
        'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm,
      );
  }
  // proxy data on instance
  // 获取data的所有key值
  const keys = Object.keys(data);
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  while (i--) {
    const key = keys[i];
    // 在非production环境下,判断是否与methods里的方法存在同名,存在则抛出警告
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm,
        );
      }
    }
    // 在非production环境下,判断是否与props里的属性存在同名,存在则抛出警告
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' &&
        warn(
          `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm,
        );
    } else if (!isReserved(key)) {
      // 如果key的第一个字符不是$或者_,为其设置代理
      proxy(vm, `_data`, key);
    }
  }
  // observe data
  // 将data转换为响应式数据
  // true:告诉observe函数,其为根属性data
  observe(data, true /* asRootData */);
}

function getData(data: Function, vm: Component): any {...}

const computedWatcherOptions = { lazy: true };

function initComputed(vm: Component, computed: Object) {...}

export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function,
) {...}

function createComputedGetter(key) {...}

function initMethods(vm: Component, methods: Object) {...}

function initWatch(vm: Component, watch: Object) {...}

function createWatcher(
  vm: Component,
  keyOrFn: string | Function,
  handler: any,
  options?: Object,
) {...}

export function stateMixin(Vue: Class<Component>) {...}

src/core/observer/index.js

  • observe(value, asRootData)
  • 负责为每一个 Object 类型的 value 创建一个 observer 实例
/* @flow */

import ...;

const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
export const observerState = {
  shouldConvert: true,
};
export class Observer {...}

function protoAugment(target, src: Object, keys: any) {...}

/* istanbul ignore next */
function copyAugment(target: Object, src: Object, keys: Array<string>) {...}

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
// 如果不存在观察者实例,则创建观察者实例,并返回
// 存在,则直接返回现有的观察者实例
export function observe(value: any, asRootData: ?boolean): Observer | void {
  // 如果value不是一个对象或者value是一个虚拟dom,则不往下执行,即不需为其转换为响应式数据
  if (!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob: Observer | void;
  // 如果value中存在__ob__属性,并且__ob__是Observer,则直接将value.__ob__赋值给ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // observerState.shouldConvert为true
    // 并且当前是浏览器环境
    // 并且value是一个数组或者是'[object Object]'
    // 并且value是可扩展的(可以在它上面添加新的属性)
    // 并且value不是Vue实例
    // 创建一个Observer对象,并赋值给ob
    ob = new Observer(value);
  }
  // 如果处理的是根数据并且存在ob,则ob.vmCount++
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob;
}
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean,
) {...}

export function set(target: Array<any> | Object, key: any, val: any): any {...}
export function del(target: Array<any> | Object, key: any) {...}
function dependArray(value: Array<any>) {...}

Observer

src/core/observer/index.js

  • Observer 类
    • constructor()方法,初始化数据,并改变数组当前对象的原型属性
    • walk()方法为对象做响应式处理
    • observeArray()方法为数组做响应式处理
export class Observer {
  // 数据对象
  value: any;
  // 依赖对象
  dep: Dep;
  // 实例计数器
  // 以该对象在根$data的vms数
  vmCount: number; // number of vms that has this object as root $data

  // 构造函数,初始化数据
  constructor(value: any) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    // 将实例挂载到观察对象的__ob__属性
    def(value, '__ob__', this);
    // 如果观察对象是一个数组,将数组转换为响应式数据
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment // 改变数组当前对象的原型属性
        : copyAugment; // 改变数组当前对象的原型属性
      // arrayKeys是对象的所有自身属性的属性名组成的数组
      augment(value, arrayMethods, arrayKeys);
      // 为数组中的每一个对象元素创建一个observer实例
      this.observeArray(value);
    } else {
      // 遍历对象中的每一个属性,转换成getter/setter
      this.walk(value);
    }
  }

  /**
   * Walk through each property 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], 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
    • 为一个对象定义一个响应式的属性,每一个属性对应一个 dep 对象
    • 如果该属性的值是对象,继续调用 observe
    • 如果给属性赋的新值是一个对象,继续调用 observe
    • 如果数据更新发送通知
// 在对象上定义响应式属性
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean,
) {
  // 创建依赖对象实例,其作用是为当前属性(key)收集依赖
  const dep = new Dep();

  // 获取obj对象的属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key);
  // 当且仅当该属性的 configurable 键值为 true 时
  // 该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
  // 如果存在属性描述符,并且configurable为false,即不可被转换为响应式数据,则不继续往下执行
  if (property && property.configurable === false) {
    return;
  }

  // cater for pre-defined getter/setters
  // 提供预定义的存取器函数
  const getter = property && property.get;
  const setter = property && property.set;

  // 当shallow为false或者不存在时
  // observe会判断val是否是递归观察子对象
  // 并将对象属性都转换为getter/setter,返回子观察对象
  let childOb = !shallow && observe(val);
  // 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 如果存在用户设置的getter,将getter的this指向obj,并赋值给value
      // 否则直接将传入的val赋值给value
      const value = getter ? getter.call(obj) : val;
      // 如果存在当前依赖目标,即watcher对象,则建立依赖
      // 在src/core/observer/index.js中的mountComponent方法,创建了watcher对象
      // 在watcher对象的构造函数中,创建了Dep.target
      if (Dep.target) {
        // 为当前属性收集依赖
        dep.depend();
        // 如果存在子观察对象,则建立子对象的依赖关系
        if (childOb) {
          // 为当前子对象收集依赖
          childOb.dep.depend();
          // 如果value是数组,则特殊处理收集数组对象依赖
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      // 返回属性值
      return value;
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val;
      // 如果新值等于旧值或者新值和旧值都为NaN,则不继续往下执行
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      // 如果传入customSetter参数时,并且在非production环境时,调用customSetter方法
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter();
      }
      // 如果存在用户设置的setter,则将setter的this指向obj,并将newVal传入
      // 否则直接newVal赋值给val
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      // 当shallow为false或者不存在时
      // observe会判断newVal是否是是递归观察子对象,返回子观察对象
      childOb = !shallow && observe(newVal);
      // 派发更新(发布更新通知)
      dep.notify();
    },
  });
}

数组响应式处理

在 observer 构造函数中

// 如果观察对象是一个数组,将数组转换为响应式数据
if (Array.isArray(value)) {
  const augment = hasProto
    ? protoAugment // 改变数组当前对象的原型属性
    : copyAugment; // 改变数组当前对象的原型属性
  // arrayKeys是对象的所有自身属性的属性名组成的数组
  augment(value, arrayMethods, arrayKeys);
  // 为数组中的每一个对象元素创建一个observer实例
  this.observeArray(value);
} else {
  // 遍历对象中的每一个属性,转换成getter/setter
  this.walk(value);
}
  • 改变数组当前对象的原型属性

src/core/observer/array.js

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

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

const arrayProto = Array.prototype;
// 使用数组的原型创建一个新的对象
export const arrayMethods = Object.create(arrayProto);

/**
 * Intercept mutating methods and emit events
 */
// 修改数组元素的方法
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(
  function (method) {
    // cache original method
    // 缓存数组的原方法
    const original = arrayProto[method];
    // 调用Object.defineProperty()方法,重写数组的方法
    def(arrayMethods, method, function mutator(...args) {
      // 执行原数组的原方法,并改变其this指向
      const result = original.apply(this, args);
      // 获取数组对象的__ob__属性
      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对象发送更新通知
      ob.dep.notify();
      return result;
    });
  },
);

Dep 类

  1. 在 defineReactive() 的 getter 中创建 dep 对象,当存在 Dep.target , 调用 dep.depend()
  2. dep.depend() 内部调用 Dep.target.addDep(this),也就是 watcher 的 addDep() 方法,它内部最终调用 dep.addSub(this),把订阅者(Watcher)添加到 dep 的 subs 数组中,当数据变化的时候调用 watcher 对象的 update() 方法
  3. 什么时候设置的 Dep.target? 通过简单的案例调试观察。调用 mountComponent() 方法的时候,创建了渲染 watcher 对象,执行 watcher 中的 get() 方法
  4. get() 方法内部调用 pushTarget(this),把当前 Dep.target = watcher,同时把当前 watcher 入栈,因为有父子组件嵌套的时候先把父组件对应的 watcher 入栈,再去处理子组件的 watcher,子组件的处理完毕后,再把父组件对应的 watcher 出栈,继续操作
  5. Dep.target 用来存放目前正在使用的 watcher。全局唯一,并且一次也只能有一个 watcher 被使用
/* @flow */

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

let uid = 0;

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  // 静态属性,Watcher对象
  static target: ?Watcher;
  // dep实例id
  id: number;
  // dep实例对应的Watcher对象/订阅者数组
  subs: Array<Watcher>;

  // 构造函数,初始化数据
  constructor() {
    this.id = uid++;
    this.subs = [];
  }

  // 添加新的订阅者Watcher对象
  addSub(sub: Watcher) {
    this.subs.push(sub);
  }

  // 移除订阅者Watcher对象
  removeSub(sub: Watcher) {
    remove(this.subs, sub);
  }

  // 将观察对象和Watcher对象建立依赖
  depend() {
    // 如果存在target,则把dep对象添加到watcher的依赖中
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }

  // 发布通知
  notify() {
    // stabilize the subscriber list first
    // 克隆数组
    const subs = this.subs.slice();
    // 调用每个订阅者的update方法实现更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

// Dep.target 用来存放目前正在使用的watcher
// 全局唯一,并且一次也只能有一个watcher被使用
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null;
const targetStack = [];

// 入栈并将当前 watcher 赋值给Dep.target
export function pushTarget(_target: Watcher) {
  // 每一个组件都有一个mountComponent函数
  // mountComponent函数创建了watcher对象
  // 所以每一个组件对应一个watcher对象
  // 如果A组件嵌套B组件,当我们渲染A组件时,发现存在子组件,则先渲染子组件,将A组件渲染挂起
  // 所以这里需要将A组件的Dep.target入栈
  // 当子组件渲染结束后,将其弹出栈,并继续执行A组件渲染
  // 总结:
  // 父子组件嵌套的时候先把父组件对应的watcher入栈,再去处理子组件的watcher。
  // 子组件处理完毕后,再把父组件对应的watcher出栈,继续操作
  if (Dep.target) targetStack.push(Dep.target);
  Dep.target = _target;
}

// 出栈操作
export function popTarget() {
  Dep.target = targetStack.pop();
}

Watcher 类

  • Watcher 分为三种,Computed Watcher、用户 Watcher (侦听器)、渲染 Watcher
  • 什么时候渲染 Watcher?在 src/core/instance/lifecycle.js 中的 mountComponent 方法中创建
  • Watcher 的构造函数初始化,处理 expOrFn (渲染 watcher 和侦听器处理不同)
  • 调用 this.get() ,它里面调用 pushTarget() 方法,接下来执行 this.getter.call(vm, vm) (对于渲染 watcher 调用 updateComponent),如果是用户 watcher 会获取属性的值(触发 get 操作)
  • 当数据更新的时候,dep 中调用 notify() 方法,notify() 中调用 watcher 的 update() 方法
  • update() 中调用 queueWatcher()
  • queueWatcher() 是一个核心方法,去除重复操作,调用 flushSchedulerQueue() 刷新队列并执行 watcher
  • flushSchedulerQueue() 中对 watcher 排序,遍历所有 watcher ,如果存在 before,触发生命周期的钩子函数 beforeUpdate,执行 watcher.run(),它内部调用 this.get(),然后调用 this.cb() (渲染 watcher 的 cb 是 noop)

哪些数组操作可以触发视图更新

const vm = new Vue({
  el: '#app',
  data: {
    arr: [2, 3, 5],
  },
});

// 在浏览器控制台分别执行下面的代码
// 会更新视图
// vm.arr.push(8)
// 数据已改变,但不会更新视图
// vm.arr[0] = 100
// 将数组清空,也不会更新视图
// vm.arr.length = 0
// 将数组第一位删除,并重新赋值为100,这时会更新视图
// vm.arr.splice(0,1,100)

总结

实例方法/数据

Vue.set(target, propertyName/index, value)

  • 参数:

    • {Object | Array} target
    • {string | number} propertyName/index
    • {any} value
  • 返回值:设置的值。

  • 用法:

    向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi')

注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。

  • 示例
Vue.set(obj, 'foo', 'test');

vm.$set(target, propertyName/index, value)

  • 参数:

    • {Object | Array} target
    • {string | number} propertyName/index
    • {any} value
  • 返回值:设置的值。

  • 用法:

    这是全局 Vue.set 的别名。

  • 示例

vm.$set(obj, 'foo', 'test');

源码分析

export function set(target: Array<any> | Object, key: any, val: any): any {
  // 如果目标是数组,并且key 是合法的索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 通过Math.max返回最大值,并赋值给target.length
    target.length = Math.max(target.length, key);
    // 通过 splice 对key位置的元素进行替换
    // splice 在 src/core/observer/array.js中进行了响应式的处理
    target.splice(key, 1, val);
    return val;
  }
  // 如果target中已存在key属性,则直接赋值
  if (hasOwn(target, key)) {
    target[key] = val;
    return val;
  }
  // 获取 target 中的 observer 对象
  const ob = (target: any).__ob__;
  // 如果target是vue实例或者是$data,则直接返回
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' &&
      warn(
        'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.',
      );
    return val;
  }
  // 如果不存在ob,那么target 不是一个响应式对象,则直接赋值并返回
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 把 key 设置为响应式属性
  defineReactive(ob.value, key, val);
  // 发送更新通知
  ob.dep.notify();
  return val;
}

Vue.delete(target, propertyName/index)

  • 参数:

    • {Object | Array} target
    • {string | number} propertyName/index

仅在 2.2.0+ 版本中支持 Array + index 用法。

  • 用法:

    删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到 property 被删除的限制,但是你应该很少会使用它。

在 2.2.0+ 中同样支持在数组上工作。

目标对象不能是一个 Vue 实例或 Vue 实例的根数据对象。

vm.$delete(target, propertyName/index)

  • 参数:

    • {Object | Array} target
    • {string | number} propertyName/index
  • 用法:

    这是全局 Vue.delete 的别名。

源码分析

export function del(target: Array<any> | Object, key: any) {
  // 如果目标是数组,并且key 是合法的索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 通过 splice 对key位置的元素进行删除
    // splice 在 src/core/observer/array.js中进行了响应式的处理
    target.splice(key, 1);
    return;
  }
  // 获取 target 中的 observer 对象
  const ob = (target: any).__ob__;
  // 如果target是vue实例或者是$data,则直接返回
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' &&
      warn(
        'Avoid deleting properties on a Vue instance or its root $data ' +
          '- just set it to null.',
      );
    return;
  }
  // 如果target不存在key属性,则直接返回
  if (!hasOwn(target, key)) {
    return;
  }
  // 删除target的key属性
  delete target[key];
  // 如果不存在ob,那么target 不是一个响应式对象,则直接返回
  if (!ob) {
    return;
  }
  // 发送更新通知
  ob.dep.notify();
}

vm.$watch(expOrFn, callback, [options])

  • 功能

    • 观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。
  • 参数

    • {string | Function} expOrFn:要监视的 $data 中的属性,可以是表达式或函数

    • {Function | Object} callback:数据变化后执行的函数

      • 函数:回调函数

      • 对象:具有 handler 属性(字符串或者函数),如果该属性为字符串则 methods 中相应的定义

    • {Object} [options]:可选的选项

      • {boolean} deep:布尔类型,深度监听

      • {boolean} immediate:布尔类型,是否立即执行一次回调函数

    • 返回值:{Function} unwatch

    注意:在变更 (不是替换) 对象或数组时,旧值将与新值相同,因为它们的引用指向同一个对象/数组。Vue 不会保留变更之前值的副本。

  • 示例

const vm = new Vue({
  el: '#app',
  data: {
    a: '1',
    b: '2',
    msg: 'Hello Vue',
    user: {
      firstName: '诸葛',
      lastName: '亮',
    },
  },
});
// expOrFn 是表达式
vm.$watch('msg', function (newVal, oldVal) {
  console.log(newVal, oldVal);
});
vm.$watch('user.firstName', function (newVal, oldVal) {
  console.log(newVal);
});
// expOrFn 是函数
vm.$watch(
  function () {
    return this.a + this.b;
  },
  function (newVal, oldVal) {
    console.log(newVal);
  },
);
// deep 是 true,消耗性能
vm.$watch(
  'user',
  function (newVal, oldVal) {
    // 此时的 newVal 是 user 对象
    console.log(newVal === vm.user);
  },
  {
    deep: true,
  },
);
// immediate 是 true
vm.$watch(
  'msg',
  function (newVal, oldVal) {
    console.log(newVal);
  },
  {
    immediate: true,
  },
);

三种类型的 Watcher 对象

  • 没有静态方法,因为 $watch 方法中要使用 Vue 的实例

  • Watcher 分三种:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher

  • 创建顺序:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher

  • 源码分析

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    // 获取 Vue 实例 this
    const vm: Component = this
    if (isPlainObject(cb)) {
      // 如果 cb 是对象,则执行 createWatcher
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    // 标记为用户 watcher
    options.user = true
    // 创建用户 watcher 对象
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      // immediate 如果为 true,则立即执行一次 cb 回调,把this指向vue,并且当前值传入
      cb.call(vm, watcher.value)
    }
    // 返回取消监听的方法
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

Vue.nextTick([callback, context])

  • 参数:

  • {Function} [callback]

  • {Object} [context]

  • 用法:

    在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

// 修改数据
vm.msg = 'Hello';
// DOM 还没有更新
Vue.nextTick(function () {
  // DOM 更新了
});

// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick().then(function () {
  // DOM 更新了
});

2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promise 的 polyfill,所以如果你的目标浏览器不原生支持 Promise (IE:你们都看我干嘛),你得自己提供 polyfill。

vm.$nextTick([callback])

  • 参数:

    • {Function} [callback]
  • 用法:

    将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。

    2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promise 的 polyfill,所以如果你的目标浏览器不是原生支持 Promise (IE:你们都看我干嘛),你得自行 polyfill。

  • 示例

new Vue({
  // ...
  methods: {
    // ...
    example: function () {
      // 修改数据
      this.message = 'changed';
      // DOM 还没有更新
      this.$nextTick(function () {
        // DOM 现在更新了
        // `this` 绑定到当前实例
        this.doSomethingElse();
      });
    },
  },
});

源码分析

src/core/instance/index.js 中调用 src/core/instance/render.js 中的 renderMixin 方法

  • 手动调用 vm.$nextTick()
  • 在 Watcher 的 queueWatcher 中执行 nextTick()
  • src/core/util/env.js
export const nextTick = (function () {
  const callbacks = [];
  let pending = false;
  let timerFunc;

  function nextTickHandler() {
    pending = false;
    // 克隆回调函数数组
    const copies = callbacks.slice(0);
    // 将callbacks数组置为空数组
    callbacks.length = 0;
    // 遍历执行回调函数数组中的每一个回调函数
    for (let i = 0; i < copies.length; i++) {
      copies[i]();
    }
  }

  // An asynchronous deferring mechanism.
  // In pre 2.4, we used to use microtasks (Promise/MutationObserver)
  // but microtasks actually has too high a priority and fires in between
  // supposedly sequential events (e.g. #4521, #6690) or even between
  // bubbling of the same event (#6566). Technically setImmediate should be
  // the ideal choice, but it's not available everywhere; and the only polyfill
  // that consistently queues the callback after all DOM events triggered in the
  // same loop is by using MessageChannel.
  /* istanbul ignore if */
  if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = () => {
      setImmediate(nextTickHandler);
    };
  } else if (
    typeof MessageChannel !== 'undefined' &&
    (isNative(MessageChannel) ||
      // PhantomJS
      MessageChannel.toString() === '[object MessageChannelConstructor]')
  ) {
    const channel = new MessageChannel();
    const port = channel.port2;
    channel.port1.onmessage = nextTickHandler;
    timerFunc = () => {
      port.postMessage(1);
    };
  } else if (typeof Promise !== 'undefined' && isNative(Promise)) {
  /* istanbul ignore next */
    // use microtask in non-DOM environments, e.g. Weex
    const p = Promise.resolve();
    timerFunc = () => {
      p.then(nextTickHandler);
    };
  } else {
    // fallback to setTimeout
    timerFunc = () => {
      setTimeout(nextTickHandler, 0);
    };
  }

  return function queueNextTick(cb?: Function, ctx?: Object) {
    let _resolve;
    // 把 cb 加上异常处理存入 callbacks 数组中
    callbacks.push(() => {
      if (cb) {
        try {
          // 如果存在cb
          // 将cb的this指向ctx,并调用 cb()
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick');
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    if (!pending) {
      pending = true;
      timerFunc();
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
      // 返回 promise 对象
      return new Promise((resolve, reject) => {
        _resolve = resolve;
      });
    }
  };
})();
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容