「试着读读 Vue 源代码」响应式系统是如何构建的 ❓

611 阅读9分钟

说明

  • 首先这篇文章是读 vue.js 源代码的梳理性文章,文章分块梳理,记录着自己的一些理解及大致过程;更重要的一点是希望在 vue.js 3.0 发布前深入的了解其原理。

  • 如果你从未看过或者接触过 vue.js 源代码,建议你参考以下列出的 vue.js 解析的相关文章,因为这些文章更细致的讲解了这个工程,本文只是以一些 demo 演示某一功能点或 API 实现,力求简要梳理过程。

  • 如果搞清楚了工程目录及入口,建议直接去看代码,这样比较高效 ( 遇到难以理解对应着回来看看别人的讲解,加以理解即可 )

  • 文章所涉及到的代码,基本都是缩减版,具体还请参阅 vue.js - 2.5.17

  • 如有任何疏漏和错误之处欢迎指正、交流。

构建前对 data 选项的预处理

在上文 「试着读读 Vue 源代码」new Vue()发生了什么 ❓, 着重梳理了 new Vue(() 其代码执行的全过程,了解了 Vue 内部到底做了哪些工作,但就响应式系统的构建并没有展开描述,Vue 在哪里开始对Data选项进行响应式体系构建呢?没错,就是上文简单提过的 _init() 内部执行的 initState 函数。

注:这里在对 data 选项初始化时,首先若存在 data 选项,则调用 initData 方法进行对 data 预处理,最终调用 observe(data, true /* asRootData */) 函数将 data 数据对象转换成响应式的;若不存在,简单初始化为空对象处理即可。

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);
  /****** 初始化 data 选项 ******/
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  /****** 初始化 data 选项 ******/
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

initData 代码实现

function initData(vm: Component) {
  /************************** data 提取并预处理 ***************************/
  // 说明: 1. 根据上文,data 选项最终将被合并成一个函数,该函数返回 data 的值。
  let data = vm.$options.data;
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};
  // 检验 data 选项是否是一个纯对象(注:在对data 选项合并处理之后走了一次 beforeCreate 钩子函数,防止 data 在那里被修改)
  if (!isPlainObject(data)) {
    data = {};
    process.env.NODE_ENV !== 'production' &&
      warn('数据函数应该返回一个对象', vm);
  }
  const keys = Object.keys(data);
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  // 为了避免选项属性直接的覆盖,将迭代 data 选项
  while (i--) {
    const key = keys[i];
    // 在非生产环境下 methods 存在:如果 methods 选项中的 key 在 data 中被定义将被警告
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(`方法“${key}”已经被定义为一个data属性。`, vm);
      }
    }
    // 在非生产环境下 props 存在:如果 data 选项中的 key 在 props 中被定义了将被警告
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' &&
        warn(`data的属性“${key}”已经被声明为一个props的属性。`, vm);
    } else if (!isReserved(key)) {
      // isReserved 作用: 检查字符串是否以$或_开头; 剔除这些特征字段,避免与 Vue 自身的属性和方法相冲突。
      //                 注: ① 如果你在data中定义了 `_message/$message` 你可以试一下 `this._message / $message` 能不能访问到?
      // proxy 作用:     data 数据代理, 使你能够:this.message 而不是 this.data.message;
      //                (`this.message <=> this.($data/_data/data).message`)。
      proxy(vm, `_data`, key);
    }
  }
  /************************** data 提取并预处理 ***************************/

  observe(data, true /* asRootData */); // observe 函数将 data 数据对象转换成响应式
}

proxy 代码实现

/**
 * 数据代理
 * @param {Object} target 要在其上定义属性的对象
 * @param {string} sourceKey 资源属性的名称
 * @param {string} key 要定义或修改的属性的名称
 */
export function proxy(target: Object, sourceKey: string, key: string) {
  // getter 函数
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key];
  };
  // setter 函数
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
  • 上述代码思路:
    • 提取 data 选项的值。
    • 判断 data 选项内键名是否和 methods / props 内定义键名冲突。
    • data 选项内属性做一层代理,且剔除特征字符代理。
    • 调用 observe 函数将 data 数据对象转换成响应式。

observe 观察函数

/**
 * 在某些情况下,我们可能希望禁用组件更新计算中的观察。
 */
export let shouldObserve: boolean = true;
export function toggleObserving(value: boolean) {
  shouldObserve = value;
}

/**
 * 观察函数
 * @param {Any} value 观测数据
 * @param {Boolean} asRootData 被观测的数据是否是根级数据
 */
export function observe(value: any, asRootData: ?boolean): Observer | void {
  // 值不是对象 或 值是虚拟DOM 直接退出
  if (!isObject(value) || value instanceof VNode) {
    return;
  }

  let ob: Observer | void;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() && // 判断是否是服务端渲染
    (Array.isArray(value) || isPlainObject(value)) && // 判断是否是数组 或 纯对象
    Object.isExtensible(value) && // 判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)
    !value._isVue // 避免 Vue 实例对象被观测
  ) {
    ob = new Observer(value);
  }

  if (asRootData && ob) {
    ob.vmCount++; // 根数据对象 target.__ob__.vmCount > 0
  }
  return ob;
}
  • 上述代码思路:
    • data 选项的值进行类型判断; 若合法,调用 Observer 类。
    • 若是根数据对象,执行 ob.vmCount++
    • 返回 Observer 实例。

Observer 观察者基类

/**
 * 附加到每个被观察对象的观察者类。
 * 一旦附加,观察者将目标对象的属性键转换为 getter/setter,用于收集依赖项和分派更新。
 */
export class Observer {
  value: any; // 观察对象
  dep: Dep; // Dep 是一个可观察的对象,可以有多个指令订阅它。
  vmCount: number; // 将此属性作为根 $data 的 vm 数量

  constructor(value: any) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;

    // 为 value 添加 __ob__ 不可枚举属性, 值为当前 `Observer` 实例
    def(value, '__ob__', this);

    // 后续的深度监测 data 数据下的二层级的数据可能是数组、对象等...
    if (Array.isArray(value)) {
      // 拦截数组变异方法。
      // 判断是否可以使用 __proto__ 选择不同的执行方法。
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
      // 递归处理,解决嵌套数组。
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }

  /**
   * 遍历每个属性并将它们转换为getter/setter。此方法只应在值类型为Object时调用。
   */
  walk(obj: Object) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }

  /**
   * 观察数组项的列表 - 使嵌套的数组或对象是响应式数据
   */
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}

数组处理

拦截数组变异方法实现

// 创建一个新对象,使用 现有的对象(Array.prototype) 来提供新创建的对象的__proto__
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

// 需要拦截的数组变异方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];

/**
 * 拦截突变方法并发出事件
 */
methodsToPatch.forEach(function(method) {
  // 缓存数组原始变异方法
  const original = arrayProto[method];
  // 在 arrayMethods 上添加这些变异方法并做一些事情。
  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;
    }
    // 若存在将被插入的数组元素,将调用 observeArray 继续进行处理
    if (inserted) ob.observeArray(inserted);

    ob.dep.notify(); // 触发依赖

    return result; // 将值结果返回
  });
});
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);

/**
 * 通过使用 _proto__ 拦截原型链来增加目标对象或数组
 */
function protoAugment(target, src: Object, keys: any) {
  target.__proto__ = src;
}

/**
 * 通过定义隐藏属性来扩充目标对象或数组。
 */
function copyAugment(target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i];
    def(target, key, src[key]);
  }
}

下面是对数据变异拦截后的断点截图:

-

defineReactive

/**
 * 在对象上定义反应性属性。
 */
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep(); // 订阅池

  /******************** 剔除不可配置属性 *********************/
  const property = Object.getOwnPropertyDescriptor(obj, key); // 返回指定对象上一个自有属性对应的属性描述符
  if (property && property.configurable === false) {
    return;
  }

  /******************** 满足预定义的 getter / setter *********************/
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]; // 触发取值函数 - 收集依赖
  }

  let childOb = !shallow && observe(val); // 默认深度观测

  /******************** 劫持属性并配置 *********************/
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    /******************** 返回正确的属性值并收集依赖 *********************/
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;

      // target 保存着要被收集的依赖(观察者)
      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; // 缓存旧值
      // NaN 或 值 相等不处理
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter(); // 用来打印辅助信息
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal); // 深度观测
      dep.notify(); // 触发依赖
    }
  });
}

Dep

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

let uid = 0;

/**
 * Dep 是一个可观察的对象,可以有多个指令订阅它。
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  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) {
      Dep.target.addDep(this);
    }
  }

  notify() {
    // 首先稳定订阅者列表
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

// 正在评估的当前目标监视程序。这是全局惟一的,因为在任何时候都只能评估一个监视程序。
Dep.target = null;
const targetStack = [];

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

export function popTarget() {
  Dep.target = targetStack.pop();
}
  • 上述陈述了响应式系统构建的部分内容,知道如何为属性构建响应式属性,即构造 getter/setter;知道了在 getter 时收集依赖,在 setter 触发依赖。 同时对 Dep 这个基类做了相应的分析。

  • 就以前文例子,断点图简单展示了构建之后的结果:

-

Watcher

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError
} from '../util/index';

import { traverse } from './traverse';
import { queueWatcher } from './scheduler';
import Dep, { pushTarget, popTarget } from './dep';

import type { SimpleSet } from '../util/index';

let uid = 0;

/**
 * 一个观察者解析一个表达式,收集依赖关系,当表达式值改变时触发回调。这用于$watch() api和指令。
 * 通过对“被观测目标”的求值,触发数据属性的 get 拦截器函数从而收集依赖
 */
export default class Watcher {
  vm: Component; // 组件实例
  expression: string; // 被观察的目标表达式
  cb: Function; // 当被观察的表达式的值变化时的回调函数
  id: number; // 观察者实例对象的唯一标识
  deep: boolean; // 当前观察者实例对象是否是深度观测
  user: boolean; // 标识当前观察者实例对象是 开发者定义的 还是 内部定义的
  computed: boolean; // 标识当前观察者实例对象是否是计算属性的观察者
  sync: boolean; // 告诉观察者当数据变化时是否同步求值并执行回调 默认 将需要重新求值并执行回调的观察者放到一个异步队列中,当所有数据的变化结束之后统一求值并执行回调
  dirty: boolean; // for computed watchers, true 代表着还没有求值
  active: boolean; // 观察者是否处于激活状态,或者可用状态
  dep: Dep;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet; // 用来在 多次求值(当数据变化时重新求值的过程) 中避免收集重复依赖
  newDepIds: SimpleSet; // 用来避免在 一次求值 的过程中收集重复的依赖
  before: ?Function; // Watcher 实例的钩子
  getter: Function;
  value: any;

  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, // 当前观察者对象的选项
    isRenderWatcher?: boolean // isRenderWatcher 用来标识该观察者实例是否是渲染函数的观察者
  ) {
    /****************** 初始化一些实例属性 ******************/
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);

    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.computed = !!options.computed;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.computed = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid;
    this.active = true;
    this.dirty = this.computed;
    /****************** 初始化一些实例属性 ******************/

    /****************** 实现避免收集重复依赖 ******************/
    this.deps = [];
    this.newDeps = [];
    this.depIds = new Set();
    this.newDepIds = new Set();
    /****************** 实现避免收集重复依赖 ******************/

    /****************** 解析路径 ******************/
    this.expression =
      process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '';
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = function() {};
        process.env.NODE_ENV !== 'production' &&
          warn(
            `监视路径失败:“${exports}”监视程序只接受简单的点分隔路径。要实现完全控制,可以使用函数。`,
            vm
          );
      }
    }
    /****************** 解析路径 ******************/

    /****************** 求值 - 计算属性的观察者 与 普通属性观察者 处理方式 ******************/
    if (this.computed) {
      this.value = undefined;
      this.dep = new Dep();
    } else {
      this.value = this.get();
    }
  }

  /**
   * 求值:触发访问器属性的 get 拦截器函数,并重新收集依赖项。
   */
  get() {
    pushTarget(this); // 给 Dep.target 赋值
    let value;
    const vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      } else {
        throw e;
      }
    } finally {
      // “触发”每一个属性,因此它们都作为依赖项被跟踪,以便进行深度监视
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value;
  }

  /**
   * 向该指令添加一个依赖项。
   */
  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)) {
        dep.addSub(this); // 移除已经没有关联关系的观察者
      }
    }
  }

  /**
   * 清理依赖项集合.
   */
  cleanupDeps() {
    let i = this.deps.length;
    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() {
    if (this.computed) {
      // 计算属性监视程序有两种模式: 延迟模式 和 激活模式。
      // 默认情况下,它初始化为lazy,只有当至少有一个订阅者(通常是另一个计算属性或组件的呈现函数)依赖于它时才会被激活。
      if (this.dep.subs.length === 0) {
        // 在延迟模式下,除非必要,否则我们不想执行计算,因此我们只需将监视程序标记为dirty。
        // 实际计算是在访问计算属性时在this.evaluate()中即时执行的。
        this.dirty = true;
      } else {
        // 在激活模式下,我们希望主动执行计算,但只在值确实发生更改时通知订阅者。
        this.getAndInvoke(() => {
          this.dep.notify();
        });
      }
    } else if (this.sync) {
      this.run();
    } else {
      // 处于性能考量,异步更新队列,但最终都会执行 watcher.run(),此处不再细说。
      queueWatcher(this);
    }
  }

  /**
   * 调度器的工作界面。将由调度程序调用。
   */
  run() {
    if (this.active) {
      this.getAndInvoke(this.cb);
    }
  }

  getAndInvoke(cb: Function) {
    const value = this.get();
    if (
      value !== this.value ||
      // 即使值是相同的,深度观察者和对象/数组上的观察者也应该触发,因为值可能发生了突变。
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value;
      this.value = value;
      this.dirty = false;
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`);
        }
      } else {
        cb.call(this.vm, value, oldValue);
      }
    }
  }

  /**
   * 计算并返回监视程序的值。这只对计算过的属性观察者调用。
   */
  evaluate() {
    if (this.dirty) {
      this.value = this.get();
      this.dirty = false;
    }
    return this.value;
  }

  /**
   * Depend on this watcher. Only for computed property watchers.
   */
  depend() {
    if (this.dep && Dep.target) {
      this.dep.depend();
    }
  }

  /**
   * 从所有依赖项的订阅服务器列表中删除self。
   */
  teardown() {
    if (this.active) {
      // 从vm的监视者列表中删除self这是一个有点昂贵的操作,所以如果正在销毁vm,我们就跳过它。
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this);
      }
      let i = this.deps.length;
      while (i--) {
        this.deps[i].removeSub(this);
      }
      this.active = false;
    }
  }
}

上文已分析了构建响应式全部的内容,下面就 $watch 函数 渲染函数 的观察者 简单演示整个响应流过程。

渲染函数

上文在谈 new Vue() 最终程序走的是挂载函数,接下来,就看看挂载函数做了哪些处理。(注意:这里的挂载函数在初始化时已经被重写,给运行时版的 $mount 函数增加编译模板的能力)

import { mountComponent } from 'core/instance/lifecycle';

/**
 * 公用的挂载方法
 *
 * @param {String | Element} el 挂载元素
 * @param {Boolean} hydrating 用于 Virtual DOM 的补丁算法
 * @returns {Function} 真正的挂载组件的方法
 */
Vue.prototype.$mount = function(
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};

mountComponent

/**
 * 组件挂载函数
 */
export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el;
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode;
    if (process.env.NODE_ENV !== 'production') {
      if (
        (vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el ||
        el
      ) {
        warn(
          '您正在使用Vue的仅运行时构建,其中模板编译器不可用。要么将模板预编译为呈现函数,要么使用编译器包含的构建。',
          vm
        );
      } else {
        warn('加载组件失败:模板或呈现函数未定义。', vm);
      }
    }
  }
  callHook(vm, 'beforeMount');

  /******************* 把虚拟DOM渲染成真正的DOM ********************/
  let updateComponent;
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name;
      const id = vm._uid;
      const startTag = `vue-perf-start:${id}`;
      const endTag = `vue-perf-end:${id}`;

      mark(startTag);
      const vnode = vm._render(); // 调用 vm.$options.render 函数并返回生成的虚拟节点(vnode)
      mark(endTag);
      measure(`vue ${name} render`, startTag, endTag);

      mark(startTag);
      vm._update(vnode, hydrating); // 渲染虚拟节点为真正的 DOM
      mark(endTag);
      measure(`vue ${name} patch`, startTag, endTag);
    };
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating);
    };
  }
  /******************* 把虚拟DOM渲染成真正的DOM ********************/

  /******************* 实例化观察者 ********************/
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted) {
          callHook(vm, 'beforeUpdate');
        }
      }
    },
    true
  );

  hydrating = false;

  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, 'mounted');
  }
  return vm;
}

$watch 函数

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

在上文初始化过程,谈到 $watch 的初始化,下面是代码实现。

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

  ...

  Vue.prototype.$watch = function(
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this;
    // 这个里就是为了规范化 watch 参数,这里不细说。
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options);
    }
    options = options || {};
    options.user = true; // 用户自定义回调
    const watcher = new Watcher(vm, expOrFn, cb, options); // 实例化观察者
    // 如果立即触发,则立即执行回调。否则放入异步队列中
    if (options.immediate) {
      cb.call(vm, watcher.value); // 这里注意,第二个参数(newVal)未传,所以你在回调拿不到
    }
    // 返回一个取消观察函数,用来停止触发回调
    return function unwatchFn() {
      watcher.teardown();
    };
  };

  ...
}

演示 demo

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>vue.js DEMO</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <p>数据属性:{{ message }}</p>
      <button @click="update">更新</button>
    </div>

    <script>
      new Vue({
        el: '#app',
        data: {
          message: 'hello vue.js'
        },

        mounted() {
          this.$watch('message', function(newVal, oldVal) {
            console.log(`message: __新值__${newVal}___旧值___${oldVal}`);
          });
        },

        methods: {
          update() {
            this.message = `${this.message} ---- ${Math.random()}`;
          }
        }
      });
    </script>
  </body>
</html>

演示效果 及步骤梳理

  • 点击更新按钮,设置 message 属性,触发 message 更新(setter
    • -
  • dep.notify() 触发依赖,依次执行 update (这里包含渲染函数的观察者: 渲染函数 => watch )
    • -
  • 经过异步队列处理,统一调用更新程序 run
    • 渲染函数(经过其处理,最新更新的值已经被渲染到 DOM 上):
      • -
    • $watch:
      • -
      • get 求值,执行回调,更新新值
        • -
      • 返回新值,缓存旧值,调用回调,传入新旧值。
        • -