vue 源码分析:数据响应

119 阅读6分钟

vue 响应原理

下面的 vue 响应原理图和总结参考于 vue 官网教程的深入响应式原理章节。本文为了大家能够快速理解 vue 响应式原理的代码实现,对基本原理做了适当的总结。若想详细了解,可以到官网查看教程。

data.png

vue响应式原理图vue 响应式原理图

vue 的响应式基本原理:

  1. vue 会遍历 data 选项中的所有 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。
  2. 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。
  3. 当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

上述图文只是对 vue 的响应式原理做了概括总结(当然,说得并不是很清楚),若想了解其具体实现还是得结合源码来看。为了便于阅读和调试源码,下面简单叙述一下大概实现步骤。

明确核心:

vue 采用数据劫持结合发布者-订阅者的方式,通过Object.defineProperty()来劫持各个属性的setter 和 getter,且在数据发生变动时将消息发布给订阅者,以触发相应的监听回调。

大概步骤:

  • 首先,observe(观察者)会对数据对象进行递归遍历(包括子属性对象的属性),为它们设置 setter 和 getter。这样,当给某个属性赋值时,就会触发 setter,从而能够监听到数据变化(通过Object.defineProperty()实现)。

  • 然后,compile(编译者) 进行模板指令解析。主要工作:

    1. 将模板中的变量替换成数据,然后初始化并渲染页面视图。
    2. 将每个指令对应的节点绑定更新函数并添加监听数据的订阅者。
    3. 一旦数据发生变动,通知订阅者更新视图。
  • 接着,Watcher(订阅者)进行订阅。它是 Observer 和 Compile 之间通信的桥梁,主要工作:

    1. 当自身实例化时,往订阅器(Dep)中添加自己。
    2. 自身要拥有一个 update() 更新方法。
    3. 当属性发生变动,调用 dep.notice() 进行通知,也就是去调用自身的 update() 方法,并触发 Compile 中绑定的回调。

以上步骤最终的实现:就是数据的双向绑定,也就是我们常提到的 MVVM 模式(Model-View-ViewModel)。看下面两张图(此图来源于网上,若侵权,请联系本人,立删)。

mvvm.png

图一:MVVM模式图一:MVVM 模式

132184689-57b310ea1804f_fix732.png

图二:MVVM模式(结合源码所画)图二:MVVM 模式(结合源码所画)

核心代码

本文 demo 参考于 vue 源码(只关注于主要功能实现,对细节部分做了删除),目录结构和函数名基本一致,且已实现模版编译数据劫持。感兴趣的同学们可以点击链接去看看。

下面我们一起看一下核心代码的实现。

observe 观察者

initState(vm) 初始化状态时,会调用 observe 观察者函数,对数据进行观测,以便在其发生改变时,做出响应。也就是说,它会遍历 data 选项中的所有 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。

function initState (vm) {
  vm._watchers = []; // 监听者列表
  const options = vm.$options;

  if (options.data) {
    initData(vm); // 初始化 data
  }
}

function initData (vm) {
  let data = vm.$options.data;
  // Vue 中的 data 可以是函数(Vue 中建议将 data 作为一个函数来使用),也可以是 Object --> {}
  data = vm.$data = typeof data === 'function' ? data.call(vm) : data || {};
  for (var key in data) {
    // proxy 实现数据代理,vm.name --> vm.$data.name
    proxy(vm, '$data', key);
  }
  // observe 观察者,对数据进行观测,以便在其发生改变时,做出响应。
  observe(vm.$data); 
}
import Dep from './dep';
import { arrayMethods } from './array';
import {
  isObject,
  def,
  hasProto,
  hasOwn,
  isPlainObject,
} from '../../shared/util';

// 返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);

// done: 尝试为某个值创建一个观察实例,
// 如果成功地观察到,返回新的观察者,如果值已经有观察者,则返回现有的观察者。
export function observe(value, asRootData) {
  // 检查 value 是否为对象(注意:在 js 中,数组也是对象,isObject 方法并不排除数组)。
  if (!isObject(value)) return;

  let ob;
  // 检查对象是否具有 '__ob__' 属性且是一个观察者实例
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__; // 返回现有的观察实例
  } else if (
    // isPlainObject 判断值是否是普通对象,指其原始类型字符串是不是 [object object]
    (Array.isArray(value) || isPlainObject(value)) &&
    // Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。
    Object.isExtensible(value) &&
    // _isVue 是一个要被观察的标志
    !value._isVue
  ) {
    ob = new Observer(value); // 返回新的观察实例
  }

  if (asRootData && ob) {
    ob.vmCount++; // 记录实例个数
  }

  return ob;
}

// done: 附加到每个被观察对象的观察者类。
// 一旦附加,观察者将目标对象的属性键转换为收集依赖项和分派更新的 getter/setter。
export class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;

    // 为当前 value 定义 __ob__ 属性,其值为 this(即当前 Observer 类)
    def(value, '__ob__', this);

    if (Array.isArray(value)) {
      // 以是否存在 __proto__ 来判断使用何种方法增加扩充目标对象或数组
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }

      // 观察数组(Array)
      this.observeArray(value);
    } else {
      // 观察对象(Object)
      this.walk(value);
    }
  }

  // done: 遍历所有属性并将它们转换为 getter/setter。
  // 仅当值类型为 Object 时才应调用此方法
  walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]; // 属性
      const value = obj[key]; // 属性值
      defineReactive(obj, key, value);
    }
  }

  // done: 观察数组(Array)的每一项
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}
// done: 定义响应式属性
function defineReactive(obj, key, val, customSetter, shallow) {
  // 创建订阅器
  const dep = new Dep();
  // Object.getOwnPropertyDescriptor 方法返回指定对象上一个自有属性对应的属性描述符(自有属性指的是
  // 直接赋予该对象的属性,不需要从原型链上进行查找的属性)。
  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];
  }

  // 递归观察 val, 它可能是一个对象(shallow,控制是否递归)
  let childOb = !shallow && observe(val);

  // Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
  // 它是实现数据劫持的关键所在。
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;
      // Dep.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;
      // 同名属性,不需要重新赋值或观察
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }

      // 自定义setter
      if (customSetter) {
        customSetter();
      }

      // 对于没有 setter 的访问器属性,则阻止运行
      if (getter && !setter) return;

      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }

      // 递归观察 newVal,它可能是一个对象
      childOb = !shallow && observe(newVal);

      dep.notify(); // 通知更新
    },
  });
}

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

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

// 当数组被接触时,收集数组元素上的依赖项,因为我们不能像属性getter那样截取数组元素访问。
function dependArray(value) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i];
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e); // 递归
    }
  }
}

Dep 订阅器

一个存储可观察对象的对象(俗称订阅器)。这些可观察的对象会被 watcher 记录为依赖项,当它们的 setter 触发时,就会通知 watcher,从而使它关联的组件重新渲染。

import { remove } from '../../shared/util';

let uid = 0;

/**
 * dep 是一个存储可观察对象的对象(俗称订阅器)。
 */
export default class Dep {
  static target;

  constructor() {
    this.id = uid++;
    this.subs = [];
  }
  // 添加
  addSub(sub) {
    this.subs.push(sub);
  }
  // 删除
  removeSub(sub) {
    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) {
  targetStack.push(target);
  Dep.target = target;
}

export function popTarget() {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}

watcher 订阅者

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。

// 挂载组件
export function mountComponent(vm) {
  // 更新组件
  updateComponent = () => {
    // 将 vm._render() 返回的 vnode 虚拟节点对象传递给 vm._update,它会调用 patch 函数生成文档树
    vm._update(vm._render());
  };

  new Watcher(vm, updateComponent, noop, {}, true /* isRenderWatcher */);
}
import { isObject, _Set as Set } from '../../shared/util';

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

let uid = 0;

/**
 * 订阅者,收集依赖项,并在表达式值发生变化时触发回调。这用于 $watch() api 和指令。
 */
export default class Watcher {
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm;

    if (isRenderWatcher) {
      vm._watcher = this;
    }

    vm._watchers.push(this);

    this.cb = cb;
    this.id = ++uid;
    this.deps = [];
    this.newDeps = [];
    this.depIds = new Set(); // 用于判断dep是否已存在
    this.newDepIds = new Set();
    this.getter = expOrFn;
    this.value = this.get();
  }

  /**
   * 获取值并收集依赖项。
   */
  get() {
    pushTarget(this); // 添加订阅者到栈中并设置为当前正在处理的订阅者
    let value;
    const vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      throw e;
    } finally {
      popTarget(); // 移除当前订阅者
      this.cleanupDeps(); // 清理依赖项集合。
    }

    return value;
  }

  /**
   * 添加依赖项
   */
  addDep(dep) {
    const id = dep.id;
    // 根据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() {
    queueWatcher(this);
  }

  /**
   * Scheduler(调度器)作业接口。
   * 将被 scheduler 调用。
   */
  run() {
    const value = this.get();
    if (
      value !== this.value ||
      // 即使值相同,对象/数组上的订阅也应该触发,因为值可能已经发生了变化。
      isObject(value)
    ) {
      // 设置新的值
      const oldValue = this.value;
      this.value = value;
      this.cb.call(this.vm, value, oldValue);
    }
  }

  /**
   * 依赖当前观察者收集的所有数据.
   */
  depend() {
    let i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  }
}

小结

核心代码的展示,不仅是为了向大家说明 vue 响应式基本原理实现的关键步骤,更是为了方便大家调试源码。感兴趣的小伙们,可以下载本例 demo 进行调试,毕竟实践出真知。最后,大家在调试时,要结合者响应式基本原理图进行,这能够帮你快速掌握源码。

相关文章链接