Web 前端开发 让前端飞

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

2019-10-22  本文已影响0人  xshinei

本篇是以一步一步实现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个:

构造一个具有这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;
    }
}

总结

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

上一篇下一篇

猜你喜欢

热点阅读