美文网首页Web 前端开发 让前端飞
一步一步实现Vue的响应式-数组观测

一步一步实现Vue的响应式-数组观测

作者: xshinei | 来源:发表于2019-10-22 14:38 被阅读0次

    本篇是以一步一步实现Vue的响应式-对象观测为基础,实现Vue中对数组的观测。

    数组响应式区别于对象的点

    const data = {
        age: [1, 2, 3]
    };
    
    data.age = 123;     // 直接修改
    data.age.push(4);   // 方法修改内容
    

    如果是直接修改属性值,那么跟对象是没有什么区别的,但是数组可以调用方法使其自身改变,这种情况,访问器属性setter是拦截不到的。因为改变的是数组的内容,而不是数组本身。

    setter拦截不到,就会导致依赖不能触发。也就是说,关键点在于触发依赖的位置。

    起因都是由于数组的方法,所以我们想的是,数组方法在改变数组内容时,把依赖也触发了。这触发依赖是我们自定义的逻辑,总结起来就是,想要在数组的原生方法中增加自定义逻辑。

    原生方法内容是不可见的,我们也不能直接修改原生方法,因为会对所有数组实例造成影响。但是,我们可以实现一个原生方法的超集,包含原生方法的逻辑与自定义的逻辑。

    const arr = [1, 2, 3];
    arr.push = function(val) {
        console.log('我是自定义内容');
        
        return Array.prototype.push.call(this, val);
    };
    
    image

    拦截数组变异方式

    覆盖原型

    数组实例的方法都是从原型上获取的,数组原型上具有改变原数组能力的方法有7个:

    • unshift
    • shift
    • push
    • pop
    • splice
    • sort
    • reverse

    构造一个具有这7个方法的对象,然后重写这7个方法,在方法内部实现自定义的逻辑,最后调用真正的数组原型上的方法,从而可以实现对这7个方法的拦截。当然,这个对象的原型是真正数组原型,保证其它数组特性不变。

    最后,用这个对象替代需要被变异的数组实例的原型。

    const methods = ['unshift', 'shift', 'push', 'pop', 'splice', 'sort', 'reverse'];
    const arrayProto = Object.create(Array.prototype);
    
    methods.forEach(method => {
        const originMethod = arrayProto[method];
        
        arrayProto[method] = function (...args) {
            // 自定义
            
            return originMethod.apply(this, args);
        };
    });
    

    在数组实例上直接新增变异方法

    连接数组原型与访问器属性getter

    对象的dep是在defineReactive函数与访问器属性getter形成的闭包中,也就是说数组原型方法中是访问不到这个dep的,所以这个dep,对于数组类型来说是不能使用了。

    因此,我们需要构建一个访问器属性与数组原型方法都可以访问到的Dep类实例。所以构建的位置很重要,不过正好有个位置满足这个条件,那就是Observer类型的构造函数中,因为访问器属性与数组原型都是可以访问到数组本身的。

    class Observer {
        constructor(data) {
            ...
            this.dep = new Dep();
            def(data, '__ob__', this);
            ...
        }
        
        ...
    }
    

    在数组本身绑定了一个不可迭代的属性ob,其值为Observer类的实例。现在,数组原型方法中可以访问到dep了,进行依赖触发:

    methods.forEach(method => {
        const originMethod = arrayProto[method];
        
        arrayProto[method] = function (...args) {
            const ob = this.__ob__;
            const result = originMethod.apply(this, args);
            
            // 触发依赖
            ob.dep.notify();
            
            return result;
        };
    });
    

    访问器属性setter中收集依赖:

    function defineReactive(obj, key, val) {
        const dep = new Dep();
        const childOb = observe(val);
        
        Object.defineProperty(obj, key, {
            configurable: true,
            enumerable: true,
            get: function () {
                dep.depend();
    
                if (childOb) {
                    childOb.dep.depend();
                }
    
                return val;
            },
            set: function (newVal) {
                if (newVal === val) {
                    return;
                }
                
                val = newVal;
                
                dep.notify();
            }
        });
    }
    

    dep只能收集到纯对象类型的依赖,如果是数组类型,就用新增的childOb中的dep去收集依赖。也就是说,childOb是Observer类的实例,来看看dep的实现:

    function observe(value) {
        let ob;
    
        if (value.hasOwnProperty('__ob__') && Object.getPrototypeOf(value.__ob__) === Observer.prototype) {
            ob = value.__ob__;
        }
        else if (isPlainObject(value) || Array.isArray(value)) {
            ob = new Observer(value);
        }
    
        return ob;
    }
    

    首先判断value自身是否有ob属性,并且属性值是Observer类的实例,如果有就直接使用这个值并返回,这里说明ob标记了一个值是否被观测。如果没有,在value是纯对象或数组类型的情况下,用value为参数实例化Observer类实例作为返回值。

    完整代码

    // Observer.js
    import Dep from './Dep.js';
    import { protoAugment } from './Array.js';
    
    class Observer {
        constructor(data) {
            this.data = data;
            this.dep = new Dep();
            
            def(data, '__ob__', this);
    
            if (Array.isArray(data)) {
                protoAugment(data);
    
                observeArray(data);
            }
            else if (isPlainObject(data)) {
                this.walk(data);
            }
        }
    
        walk(data) {
            const keys = Object.keys(data);
    
            for (let key of keys) {
                const val = data[key];
    
                defineReactive(data, key, val);
            }
        }
    }
    
    function observe(value) {
        let ob;
    
        if (value.hasOwnProperty('__ob__') && Object.getPrototypeOf(value.__ob__) === Observer.prototype) {
            ob = value.__ob__;
        }
        else if (isPlainObject(value) || Array.isArray(value)) {
            ob = new Observer(value);
        }
    
        return ob;
    }
    
    function observeArray(data) {
        for (let val of data) {
            observe(val);
        }
    }
    
    function defineReactive(obj, key, val) {
        const dep = new Dep();
        let childOb = observe(val);
        
        Object.defineProperty(obj, key, {
            configurable: true,
            enumerable: true,
            get: function () {
                dep.depend();
    
                if (childOb) {
                    childOb.dep.depend();
    
                    if (Array.isArray(val)) {
                        dependArray(val);
                    }
                }
    
                return val;
            },
            set: function (newVal) {
                if (newVal === val) {
                    return;
                }
                
                val = newVal;
                
                dep.notify();
            }
        });
    }
    
    function isPlainObject(o) {
        return ({}).toString.call(o) === '[object Object]';
    }
    
    function def(obj, key, val) {
        Object.defineProperty(obj, key, {
            configruable: true,
            enumerable: false,
            writable: true,
            value: val
        });
    }
    
    // Array.js
    const methods = [
        'unshift',
        'shift',
        'push',
        'pop',
        'splice',
        'sort',
        'reverse'
    ];
    const arrayProto = Object.create(Array.prototype);
    
    methods.forEach(method => {
        const originMethod = arrayProto[method];
    
        arrayProto[method] = function (...args) {
            const ob = this.__ob__;
            const result = originMethod.apply(this, args);
            
            ob.dep.notify();
    
            return result;
        }
    });
    
    export function protoAugment(array) {
        array.__proto__ = arrayProto;
    }
    
    // Dep.js
    let uid = 1;
    Dep.target = null;
    
    class Dep {
        constructor() {
            this.id = uid++;
            this.subs = [];
        }
    
        addSub(sub) {
            this.subs.push(sub);
        }
    
        depend() {
            if (Dep.target) {
                Dep.target.addDep(this);
            }
        }
    
        notify() {
            for (let sub of this.subs) {
                sub.update();
            }
        }
    }
    
    // Watcher.js
    import Dep from './Dep.js';
    
    class Watcher {
        constructor(data, pathOrFn, cb) {
            this.data = data;
    
            if (typeof pathOrFn === 'function') {
                this.getter = pathOrFn;
            }
            else {
                this.getter = parsePath(data, pathOrFn);
            }
    
            this.cb = cb;
            this.deps = [];
            this.depIds = new Set();
    
            this.value = this.get();
        }
    
        get() {
            Dep.target = this;
            const value = this.getter();
            Dep.target = null;
    
            return value;
        }
    
        addDep(dep) {
            const id = dep.id;
    
            if (!this.depIds.has(id)) {
                this.deps.push(dep);
                this.depIds.add(id);
    
                dep.addSub(this);
            }
        }
    
        update() {
            const oldValue = this.value;
            this.value = this.get();
    
            this.cb.call(this.data, this.value, oldValue);
        }
    }
    
    function parsePath(path) {
        if (/.$_/.test(path)) {
            return;
        }
    
        const segments = path.split('.');
    
        return function(obj) {
            for (let segment of segments) {
                obj = obj[segment]
            }
    
            return obj;
        }
    }
    

    总结

    响应式的关键点就在于读取数据->收集依赖,修改数据->触发依赖,由于数组的特殊性,所以要去拦截数组变异的方法,但本质其实并没有变。

    相关文章

      网友评论

        本文标题:一步一步实现Vue的响应式-数组观测

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