Array的變化偵測(cè)(一)

如何追蹤變化

為什么對(duì)于Array的偵測(cè)方式和Object的不同?如下一句push操作,調(diào)用的是數(shù)組原型上的方法改變數(shù)組,不會(huì)觸發(fā)getter/setter。

this.list.push(1);

在ES6之前,JavaScript并沒有提供元編程的能力,足以攔截原型方法。Vue的做法是寫自定義方法覆蓋原型方法。


使用攔截器覆蓋原生原型方法.png

用一個(gè)攔截器覆蓋Array.prototype,每當(dāng)我們調(diào)用原型方法操作數(shù)組時(shí),調(diào)用的都是自定義方法,就可以跟蹤到變化了。

攔截器

攔截器和Array.prototype一樣也是一個(gè)對(duì)象,包含的屬性也一樣,只是一些能改變數(shù)組的方法是處理過的。
整理一下,發(fā)現(xiàn)數(shù)組原型可以改變數(shù)組自身內(nèi)容的方法有七個(gè):push、pop、shift、unshift、splice、sorte和reverse。

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sorte',
    'reverse'
].forEach(function(method){
    // 緩存原始方法
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args){
            return original.apply(this, args);
        },
        enumerable: false,
        writeable: ture,
        configurable: true
    })
})

這樣我們就可以在mutator函數(shù)中做一些事情了,比如發(fā)送變化的通知。

使用攔截器覆蓋Array原型
export class Observer{
    constructor(value){
        this.value = value;
        if(Array.isArray(value)){
            value.__proto__ = arrayMethods;
        } else {
            this.walk(value);
        }
    }
}

__proto__其實(shí)是Object.getPrototypeOf和Object.setPrototypeOf的早期實(shí)現(xiàn),只是ES6的瀏覽器支持度不理想。

使用__proto__覆蓋原型.png

將攔截器方法掛載到數(shù)組屬性上

并不是所有瀏覽器都支持通過__proto__訪問原型,所以還要處理不能使用這個(gè)非標(biāo)準(zhǔn)屬性的情況。
Vue的做法非常粗暴,直接將arrayMethods身上的方法設(shè)置到被偵測(cè)數(shù)組上。

const hasProto = '__proto__' in {};
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);

export class Observer{
    constructor(value){
        this.value = value;
        if(Array.isArray(value)){
            const augment = hasProto ? protoAugment : copyAugment;
            augment(value, arrayMethods, arrayKeys);
        } else {
            this.walk(value);
        }
    }
}
function protoAugment(target, src, keys){
    target.__proto__ = src;
}
function copyAugment(target, src, keys){
    for(let i = 0, l = keys.length;i < l;i++){
        const key = keys[i];
        def(target, key, src[key]);
    }
}
如何收集依賴

我們創(chuàng)建攔截器實(shí)際上是為了獲得一種能力,一種感知數(shù)組內(nèi)容發(fā)生變化的能力?,F(xiàn)在具備了這個(gè)能力,要通知誰(shuí)呢?根據(jù)前面對(duì)Object的處理,通知Dep中的依賴(Watcher)。
怎么收集依賴呢?還用getter。

function defineReactive(data, key, val){
    if(typeof val = 'object'){
        new Observer(val);
    }
    let dep = new Dep();
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function(){
            dep.depend();
            // 這里收集依賴
            return val;
        },
        set: function(newVal){
            if(val === newVal){
                return;
            }
            dep.notify();
            val = newVal;
        }
    })
}

新增了一段注釋,也就是說(shuō)Array在getter中收集依賴,在攔截器觸發(fā)依賴

依賴收集在哪
export class Observer{
    constructor(value){
        this.value = value;
        this.dep = new Dep(); // 新增dep
        if(Array.isArray(value)){
            const augment = hasProto ? protoAugment : copyAugment;
            augment(value, arrayMethods, arrayKeys);
        } else {
            this.walk(value);
        }
    }
}

Vue將依賴列表存在了Observer,為什么是這里?
前面說(shuō)Array在getter中收集依賴,在攔截器觸發(fā)依賴,所以依賴的位置很關(guān)鍵,保證getter要訪問的到,攔截器也訪問的到。

收集依賴

Dep實(shí)例保存在Observer的屬性上后,我們開始收集依賴。

function defineReactive(data, key, val){
    let childOb = observe(val); // 修改
    let dep = new Dep();
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function(){
            dep.depend();
            
            // 新增
            if(childOb){
                childOb.dep.depend();
            }
            return val;
        },
        set: function(newVal){
            if(val === newVal){
                return;
            }
            dep.notify();
            val = newVal;
        }
    })
}

export function observe(value, asRootData){
    if(!isObject(value)){
        return;
    }
    let ob;
    if(hasOwn(value, '__ob__')&&value.__ob__ instanceof Observer) {
        ob = value.__ob__;
    } else {
        ob = observe(val);
    }
    return ob;
}

增加一個(gè)childOb 的意義到底是啥?在于搭建了從getter把依賴收集到Observer的dep中的橋梁。

在攔截器中獲取Observer

因?yàn)閿r截器是對(duì)數(shù)組原型的封裝,所以攔截器可以訪問到this(正在被操作的數(shù)組)。而dep在Observer中,所以需要在this上讀到Observer實(shí)例。

// 工具函數(shù)
function def(obj, key, val, enumerable){
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writeable: true,
        configurable: true
    })
}
export class Observer{
    constructor(value){
        this.value = value;

        def(value, '__ob__', this); // 新增
        if(Array.isArray(value)){
            const augment = hasProto ? protoAugment : copyAugment;
            augment(value, arrayMethods, arrayKeys);
        } else {
            this.walk(value);
        }
    }
}

現(xiàn)在Observer實(shí)例已經(jīng)存入數(shù)組中__ob__屬性,下一步就是在攔截器中獲取。

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sorte',
    'reverse'
].forEach(function(method){
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args){
            const ob = this.__ob__; // 新增
            return original.apply(this, args);
        },
        enumerable: false,
        writeable: ture,
        configurable: true
    })
})
向數(shù)組的依賴發(fā)通知
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sorte',
    'reverse'
].forEach(function(method){
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args){
            const ob = this.__ob__;
            ob.dep.notify(); // 向依賴發(fā)通知
            return original.apply(this, args);
        },
        enumerable: false,
        writeable: ture,
        configurable: true
    })
})

既然能獲取到Observer實(shí)例和里面的依賴列表了,就直接調(diào)用notify。

剩下的內(nèi)容就是獲取數(shù)組元素變化,以及Vue的處理方式的弊端,另開一篇寫吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容