美文网首页
Vue 响应式系统(二)- observe 工厂函数

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

作者: 前端老司机 | 来源:发表于2020-06-08 21:29 被阅读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 分支的条件才会被观测,我们看看需要满足什么条件:

    • shouldObserve 为 true ( 默认值 true )
    • !isServerRendering() 函数的返回值是一个布尔值,用来判断是否是服务端渲染。Vue SSR 数据预取和状态
    • Array.isArray(value) || isPlainObject(value) 被观测的数据对象必须是数组或者纯对象。
    • value._isVue Vue实例才拥有_isVue 属性,在此是避免观测Vue实例对象。
    • Object.isExtensible(value) 观测的数据对象必须是可扩展的。(对象默认可扩展)

    阻止对象扩展方法有: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
        });
    });
    

    推荐:

    • 020 持续更新,精品小圈子每日都有新内容,干货浓度极高。
    • 结实人脉、讨论技术 你想要的这里都有!
    • 抢先入群,跑赢同龄人!(入群无需任何费用)
    • 群号:779186871
    • 点击此处,与前端开发大牛一起交流学习

    申请即送:

    • BAT大厂面试题、独家面试工具包,

    • 资料免费领取,包括 各类面试题以及答案整理,各大厂面试真题分享!

    相关文章

      网友评论

          本文标题:Vue 响应式系统(二)- observe 工厂函数

          本文链接:https://www.haomeiwen.com/subject/damdtktx.html