.3-Vue源码之数据劫持(1)

写了一半关机了,又得重新写,好气。

 

  上一节讲到initData函数,其中包含格式化、代理、监听。

    // Line-3011
    function initData(vm) {
        var data = vm.$options.data;
        //data = vm._data = ... 格式化data
        // ...proxy(vm, "_data", keys[i]); 代理
        // 监听
        observe(data, true /* asRootData */ );
    }

  这一节重点开始跑observe函数,该函数接受2个参数,一个是数据,一个布尔值,代表是否是顶层根数据。

    // Line-899
    function observe(value, asRootData) {
        if (!isObject(value)) {
            return
        }
        var ob;
        // 判断是否有__ob__属性 即是否已被监听
        if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
            ob = value.__ob__;
        }
        // 若无进行条件判断 
        else if (
            observerState.shouldConvert && // 是否应该被监听 默认为true
            !isServerRendering() && // 是否是服务器渲染
            (Array.isArray(value) || isPlainObject(value)) && // 数据必须为数组或对象
            Object.isExtensible(value) && // 是否可扩展 => 能添加新属性
            !value._isVue // vue实例才有的属性
        ) {
            // 生成一个观察者
            ob = new Observer(value);
        }
        // 根数据记数属性++
        if (asRootData && ob) {
            ob.vmCount++;
        }
        return ob
    }

  observe函数除去大量的判断,关键部分就是new了一个观察者来进行数据监听,所以直接跳进该构造函数:

    // Line-833
    var Observer = function Observer(value) {
        // data
        this.value = value;
        // 依赖收集
        this.dep = new Dep();
        this.vmCount = 0;
        // 通过Object.defineProperty定义__ob__属性 this指向Observer实例
        def(value, '__ob__', this);
        // 根据类型调用不同的遍历方法
        if (Array.isArray(value)) {
            var augment = hasProto ?
                protoAugment :
                copyAugment;
            augment(value, arrayMethods, arrayKeys);
            this.observeArray(value);
        } else {
            this.walk(value);
        }
    };

  这个构造函数给实例绑了3个属性,分别为data对象的value、记数用的vmCount、依赖dep,接着根据数据类型调用不同的遍历方法进行依赖收集。  

  Dep对象比较简单,包含2个属性和4个对应的原型方法,如下:

    // Line-720
    // 超级简单
    var Dep = function Dep() {
        this.id = uid++;
        this.subs = [];
    };
    // 原型方法
    Dep.prototype.addSub = function addSub(sub) {
        this.subs.push(sub);
    };
    Dep.prototype.removeSub = function removeSub(sub) {
        remove(this.subs, sub);
    };
    Dep.prototype.depend = function depend() {
        if (Dep.target) {
            Dep.target.addDep(this);
        }
    };
    Dep.prototype.notify = function notify() {
        // stabilize the subscriber list first
        var subs = this.subs.slice();
        for (var i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    };

  可见,new出来的dep实例只有2个属性,一个是每次+1的计数uid,还有一个是依赖收集数组。

  原型上的4个方法分别对应增、删、添加依赖、广播,由于暂时用不到update函数,所以先放着。

  

  接着,用自定义的def方法把__ob__属性绑到了生成的observe实例上,该属性引用了自身。

 

  最后,根据value是类型是数组还是对象,调用不同的方法进行处理。案例中传进来的value是一个object,所以会跳到walk方法中。

  这里不妨看看如果是数组会怎样。

    // Line-838
    if (Array.isArray(value)) {
        // 判断是否支持__proto__属性
        var augment = hasProto ?
            protoAugment :
            copyAugment;
        // 原型扩展
        augment(value, arrayMethods, arrayKeys);
        // 数组监听方法
        this.observeArray(value);
    }

  其中,根据环境是否支持__proto__分别调用protoAugment或copyAugment,这两个方法比较简单,上代码就能明白。

    // Line-876
    function protoAugment(target, src) {
        // 直接指定原型
        target.__proto__ = src;
    }
    
    // Line-887
    function copyAugment(target, src, keys) {
        // 遍历keys
        // 调用def(tar,key,value) => (tar[key] = (value => src[key]))
        for (var i = 0, l = keys.length; i < l; i++) {
            var key = keys[i];
            def(target, key, src[key]);
        }
    }

  选择了对应的方法就开始调用,传进的参数除了value还是两个奇怪的值:arrayMethods、arrayKeys。

    // Line-767
    // 创建一个对象 原型为数组对象的原型
    var arrayProto = Array.prototype;
    var arrayMethods = Object.create(arrayProto);
    console.log(Array.isArray(arrayMethods)); //false
    console.log('push' in arrayMethods); //true
    
    // Line-814
    var arrayKeys = Object.getOwnPropertyNames(arrayMethods);

  简单来讲,arrayMethods就是一个对象,拥有数组的方法但不是数组。

  Object.getOwnPropertyNames方法以数组形式返回对象所有可枚举与不可枚举方法,所以arrayKeys直接在控制台打印可以看到:

 

  最后,不管选择哪个方法,都会将“改造过”的数组方法添加到value对象上,由于代码跑不到,等下次给出具体值吧。这里接着会调用observeArray方法,将数组value穿进去。

    // Line-865
    Observer.prototype.observeArray = function observeArray(items) {
        // 遍历分别调用observe方法
        for (var i = 0, l = items.length; i < l; i++) {
            observe(items[i]);
        }
    };

  绕了一圈,最后还是遍历value,挨个调用observe方法,并指向了walk方法。

    // Line-855
    Observer.prototype.walk = function walk(obj) {
        // 获取对象的键
        var keys = Object.keys(obj);
        for (var i = 0; i < keys.length; i++) {
            // 核心方法
            defineReactive$$1(obj, keys[i], obj[keys[i]]);
        }
    };

  这个方法比较简单,获取传进来的对象键,遍历后调用defineReactive$$1方法。看名字也就差不多明白了,这是响应式的核心函数,双绑爸爸。

  下节再来说这个,完结完结!  

 

 

补充tips:

  之前有一段代码,我说将改造过的数组方法添加到数组value上,这个改造是什么意思呢?其实关于arrayMethods代码没有全部贴出来,这里做简单的解释。

    // Line-767
    var arrayProto = Array.prototype;
    var arrayMethods = Object.create(arrayProto);
    // 以下数组方法均会造成破坏性操作
    [
        'push',
        'pop',
        'shift',
        'unshift',
        'splice',
        'sort',
        'reverse'
    ]
    .forEach(function(method) {
        // 缓存原生方法
        var original = arrayProto[method];
        // 修改arrayMethods上的数组方法
        def(arrayMethods, method, function mutator() {
            // 将argument转换为真数组
            var arguments$1 = arguments;
            var i = arguments.length;
            var args = new Array(i);
            while (i--) {
                args[i] = arguments$1[i];
            }
            // 首先执行原生方法
            var result = original.apply(this, args);
            var ob = this.__ob__;
            var inserted;
            // 值添加方法
            // push和unshift会添加一个值 即args
            // splice(a,b,c,..)方法只有c后面是添加的值 所以用slice排除前两个参数
            switch (method) {
                case 'push':
                    inserted = args;
                    break
                case 'unshift':
                    inserted = args;
                    break
                case 'splice':
                    inserted = args.slice(2);
                    break
            }
            // 对添加的值调用数组监听方法
            if (inserted) {
                ob.observeArray(inserted);
            }
            // 广播变化 提示DOM更新及其他操作
            ob.dep.notify();
            return result
        });
    });

  完整的arrayMethods如上所述,解释大部分都写出来了,这也是vue通过对数组方法的劫持来达到变化监听的原理,对象的劫持下节再来分析。

 

  惯例,来一张图:

 

 

  快撒花!

posted @ 2017-05-23 15:59  书生小龙  阅读(1800)  评论(2编辑  收藏  举报