vue 源码学习-数据动态响应:观察者 Observer

前言

我们已经大致了解了初始化工作中 vue 到底做了哪些事情,这篇开始,进入第二大块内容 —— 数据响应,逐步探究 vue 是如何实现数据的动态响应?

再次回顾状态初始化 initState 方法:

function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  // ..
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  // ..
}

这里先忽略 props 、methods 、computed 、watcher 的“处理工作”,先选 options.data 属性作为起点,因为 data 使我们开发中最常用的属性,借他了解整个 动态响应的全貌

function initData(vm: Component) {
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  // 校验 options.data 的合法性
  // ...
  // observe data
  observe(data, true /* asRootData */);
}

经过 initData 的初始化处理,发现最终调用了观察方法 observe(data) ,这是数据动态响应的第一部分。

observe 观察数据

代码主体

先看下方法代码定义:

function observe(value: any, asRootData: ?boolean): Observer | void {
  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) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob;
}

条件判断

我们知道目前是如何调用此 observe 方法的:

observe(data, true /* asRootData */);

所以目前参数列表中 value 就是 options.dataasRootData 值为 true

首先,回去判断某些不做观察的条件:

if (!isObject(value) || value instanceof VNode) {
  return;
}

不做观察的条件value 非对象形式,并且不是 VNode 虚拟节点对象。

再是,判断 value 是否已经被观察过了:

hasOwn(value, "__ob__") && value.__ob__ instanceof Observer;

如果 value 对象上含有 __ob__ 属性,并且该属性是 Observer 对象,则直接返回 __ob__ 属性值:

if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
  ob = value.__ob__;
}

接下来是一段很长的判断:

shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue;

shouldObserve 默认为 true ,通过调用 toggleObserving 方法来切换其值:

function toggleObserving(value: boolean) {
  shouldObserve = value;
}

shouldObserve 像是一个开关来控制是否来执行 observe 方法,你们在 vue 中找到几处 toggleObserving 开关的使用。

比如,在对 inject 处理 provide 的传值时的数据封装:

function initInjections(vm: Component) {
  const result = resolveInject(vm.$options.inject, vm);
  toggleObserving(false);
  Object.keys(result).forEach(key => {
    defineReactive(vm, key, result[key]);
  });
  toggleObserving(true);
}

最终 inject 上的第一级属性被定义了响应化,但子级属性因为 toggleObserving(false) 而直接 return ,这就是为何:你更新 inject 相关值时,页面没有更新的原因(有些扯远了)。

isServerRendering 判断是否是 SSR 服务端渲染,本文的运行环境是浏览器中,所以这里判断为 true

后续只要 value 符合是 引用类型对象(对象字面量、数组),并且是属性可扩展,和非 vue 框架对象 (_isVuefalse ),则会创建 Observer 观察者对象:

ob = new Observer(value);

Observer 观察者对象

代码主体

这是 Observer 类的代码主体:

class Observer {
  constructor(value: any) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, "__ob__", this);
    if (Array.isArray(value)) {
      // ...
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }
  walk() {
    defineReactive();
  }
  observeArray() {
    observe();
  }
}

首先我们先看下 Observer 的基本属性:

this.value = value;
this.dep = new Dep();
this.vmCount = 0;

每次新建的 Observer 对象将初始化一个 Dep 对象(这块下篇再看)定义为 this.dep ,并且还会初始化 this.value 待观察对象,this.vmCount

对象属性 __ob__

value 对象上新增一个观察属性 __ob__

def(value, "__ob__", this);

def 方法内部就是通过定义 数据类型属性

Object.defineProperty(obj, key, {
  value: val,
  enumerable: !!enumerable,
  writable: true,
  configurable: true
});

注意这里的 enumerablefalse ,会在遍历 value 是屏蔽掉当前属性 __ob__ 。这个属性 __ob__ 以后有什么用,我们在 Dep 对象中再看。

先跳过 Array.isArray(value) 的判断,直接调用 this.walk 方法:

this.walk(value);
walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i])
    }
}

walk 会遍历 value 上的所有属性(除了 __ob__),并通过 defineReactive 定义 响应式数据

数组拦截

再回到数据类型的判断:

if (Array.isArray(value)) {
  if (hasProto) {
    protoAugment(value, arrayMethods);
  } else {
    copyAugment(value, arrayMethods, arrayKeys);
  }
  this.observeArray(value);
}

首先我们知道 __proto__ 是个有争议的属性,因为他不属于 Web 规范,只是被浏览器厂商实现着,用来访问对象 [[Prototype]] 属性。

所以 vue 会有如下工具函数 hasProto

"__proto__" in {};

判断当前环境是否支持 hasProto 分别调用 protoAugmentcopyAugment 来对 value 数组上的每个元素进行针对数组类型的数据做响应。

根据 Array.prototype 创建数组(方法)对象 arrayMethods ,通过 def 设置 mutator 方法,如果数组数据涉及变化( push、unshift、splice )则再次调用 notify 触发数据响应:

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;
  });
});

上面就是 vue 怎么对原生数组方法进行拦截的代码,有兴趣可以细看。

解析完数组队列后,最后执行 this.observeArray

observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i])
    }
}

内部还是执行 observe 方法。

假设你在 data 上定义了一个数组数据,最后的结果就会长成这样:

数组每个元素都添加了

defineReactive 响应式数据的定义

已经知道,无论是否是 Array 类型的数据, value 最终都会调用 walk 方法,执行 defineReactive

walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i])
    }
}

代码主体

现在来看下 defineReactive 的代码:

function defineReactive(obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) {
  const dep = new Dep();

  // ...

  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;
      // depend ...
      return value;
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val;
      // ...
      // notify ...
    }
  });
}

创建 Dep 对象

已进入该方法,就看到了新建了 Dep 对象,并定义为 dep ,这个是依赖对象,我们目前只需知道每个对象属性都会调用 defineReactive 方法,并且都会创建 Dep 实例。具体下篇再细谈。

const dep = new Dep();

合法验证

对象属性是否可配置

根据对象属性描述特征 configurable 来判断是否要进行数据动态响应定义:

const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
  return;
}

如果特性 configurablefalse ,就没有之后的意义了(将对象属性定义为 访问器属性 )。

预取值

我们通常在 data 选项属性上会定义 {name:'foo'} ,是有值的,但没有定义 getter 函数。所以需要通过某种方式来事先计算一次:

const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
  val = obj[key];
}

那如果 getter 存在呢?那就会在底下的 getter/setter 中执行 getter.call() 进行执行赋值操作。

const value = getter ? getter.call(obj) : val;

判断子元素是否需要观察

let childOb = !shallow && observe(val);

如果我们的数据是这样(对象嵌套):

data:{
    user:{
        name:'eminoda',
        child:{
            name:'sxh'
        }
    }
}

当第一次遍历 user 时,当前的 val 就是 child 对象,需要继续通过 observe 观察。

得到 childOb ,供后续逻辑使用。

对象属性定义访问器属性

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter() {},
  set: function reactiveSetter(newVal) {}
});

getter

首先通过 getter ,来预执行:

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);
    }
  }
}

当然目前没不知道这个 Dep.target 是什么,不过从代码上看,当它存在时,就会调用 depend 来执行依赖相关操作。

setter

第一步还是根据判断 getter 进行预执行操作,获得 value

之后判断 newValvalue 是否有变化?

if (newVal === value || (newVal !== newVal && value !== value)) {
  return;
}

值得一提的是,newVal !== newVal 这样的矛盾判断有什么用?你可以运行下如下代码:

Number("foo") == Number("foo"); //false

然后通过 customSetter 在开发模式打印些 warn 信息:

if (process.env.NODE_ENV !== "production" && customSetter) {
  customSetter();
}

如果 getter && !setter 就没必要继续之后操作了,直接 return

如果定义了 setter 函数,则会执行下:

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

最后将这个新 newVal 进行观察,同时出发 notify 进行依赖更新:

childOb = !shallow && observe(newVal);
dep.notify();

总结

本文篇幅较长,尽可能把 vue 数据动态响应的第一部分原理说明白了。

通过 observe 方法判断对象上,那些属性需要观察,来新建对应的 Observer 观察者对象。Observer 内部循环遍历属性,调用 definedReactive 来定义动态响应方式。

这个动态响应的基本原理还是基于对象的访问属性 getter/setter 。

其内部真正实现数据的响应机制,还是要看之后的 DepWatcher 对象。