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

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

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容