Vue 响应式系统(二)- observe 工厂函数

2020-06-08  本文已影响0人  前端老司机

接上篇文章回到 initData 函数的最后一句代码:

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

调用了 observe 函数观测数据, observe 源码如下:

function observe(value, asRootData) {
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    var ob;
    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函数开始就是一个if 判断。

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

如果要观测的数据不是一个对象或者是 VNode 实例,则直接 return 。

关于VNode实例可以去了解之前写的编译器相关的文章。

接下来又是一个if else 语句。

var ob;
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 分支判断value 是否有"ob" 属性, 如果有是否为 Observer 实例,两者满足把 value.ob 值赋值给ob。 但为什么要这么做 ob 又是什么呢?

原因是当一个数据对象被观测之后将会在该对象上定义 ob 属性,换句话说不管哪个数据对象被观测了此对象上会扩展一个ob 属性。在此 if 分支的作用是用来避免重复观测一个数据对象。

再来看看 else...if 分支,如果数据对象上没有定义 ob 属性,那么说明该对象没有被观测过,进而会判断 else...if 分支,那么会执行 ob = new Observer(value) 对数据对象进行观测。 前提是数据对象满足所有 else...if 分支的条件才会被观测,我们看看需要满足什么条件:

阻止对象扩展方法有:Object.preventExtensions() 、Object.freeze() 、Object.seal()

当一个对象满足了以上五个条件时,就会执行 else...if 语句块的代码,创建一个Observer实例:

ob = new Observer(value);

真正将数据对象转换成响应式数据对象的是 Observer 函数 。

Observer 构造函数源码:

var Observer = function Observer(value) {
    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.
 */
Observer.prototype.walk = function walk(obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
        defineReactive$$1(obj, keys[i]);
    }
};

/**
 * Observe a list of Array items.
 */
Observer.prototype.observeArray = function observeArray(items) {
    for (var i = 0, l = items.length; i < l; i++) {
        observe(items[i]);
    }
};

Observer构造函数的实例对象将拥有三个实例属性,分别是 value、dep 和 vmCount 以及两个实例方法 walk 和 observeArray。来看下实例化 Observer 构造函数对象都做了什么。

实例对象的 value 属性引用了数据对象:

this.value = value;

实例对象的 dep 属性获取Dep构造函数实例的引用,用于依赖收集。

this.dep =new Dep();

实例对象的 vmCount 属性初始设置为0:

this.vmCount = 0;

接下来:

def(value, '__ob__', this);

使用 def 函数,为数据对象定义了一个 ob 属性,这个属性的值就是当前 Observer 实例对象。

def 源码:

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

有意思的是这里监听你对 obj.key 属性访问, 值为val。 但是把obj.key 设置为不可枚举的属性,之所以这么做的原因是后面遍历数据对象的时候防止遍历到 ob 属性。

假设我们的数据对象如下:

var data = {count: 1};

那么经过 def 函数处理之后,data 对象应该变成如下这个样子:

var data = {
  count: 1,
  // __ob__ 为不可枚举的属性
  __ob__: {
    value: data, 
    dep: new Dep(), 
    vmCount: 0
  }
}

在看接下来的代码:

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

该判断用来区分数据对象到底是数组还是一个纯对象,因为对于数组和纯对象的处理方式是不同的,先来看下如果数据对象是数组对象的处理方式。

通过以上代码了解到在数据对象是数组时,还会在做一个if...else 的判断,根据变量hasProto决定是去调用protoAugment函数 还是 copyAugment 函数,hasProto 是一个布尔值,它用来检测当前环境是否可以使用proto属性,再此我们只考虑正常情况不去扩展边界默认都为真。所以接下来进入到 protoAugment 函数中去。

protoAugment函数源码

function protoAugment(target, src) {
    /* eslint-disable no-proto */
    target.__proto__ = src;
    /* eslint-enable no-proto */
}

这里做了什么事情?

它把我们即将要观测的数组原型链指向了src, src的值是调用protoAugment函数传过来的arrayMethods对象。

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);

arrayMethods 对象的原型链又指向了Array.prototype,画个图来理解下。

如图所示,在这使用了一种叫"代理原型"的方式,在被观测的数组对象与Array.prototype之间插入一个纯对象。此时value. __ proto . proto __ === Array.prototype。

为什么要这么做?

因为数组是一个特殊的数据结构,它有很多实例方法,并且有些方法会改变数组自身的值,我们称其为变异方法,这些方法有:push、pop、shift、unshift、splice、sort以及reverse等。 这个时候我们就要考虑一件事,即当用户调用这些变异方法改变数组时需要触发依赖。并且需要知道何时调用了这些变异方法,这样才能在这些方法被调用时做对应的处理。而这步的操作就是通过代理原型的方式实现的。

var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function(method) {
    // cache original method
    var original = arrayProto[method];
    def(arrayMethods, method, function mutator() {
        var args = [],
            len = arguments.length;
        while (len--) args[len] = arguments[len];

        var result = original.apply(this, args);
        var ob = this.__ob__;
        var 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
    });
});

推荐:

申请即送:

上一篇下一篇

猜你喜欢

热点阅读