一步一步實(shí)現(xiàn)Vue的響應(yīng)式-數(shù)組觀測(cè)

本篇是以一步一步實(shí)現(xiàn)Vue的響應(yīng)式-對(duì)象觀測(cè)為基礎(chǔ),實(shí)現(xiàn)Vue中對(duì)數(shù)組的觀測(cè)。

數(shù)組響應(yīng)式區(qū)別于對(duì)象的點(diǎn)

const data = {
    age: [1, 2, 3]
};

data.age = 123;     // 直接修改
data.age.push(4);   // 方法修改內(nèi)容

如果是直接修改屬性值,那么跟對(duì)象是沒有什么區(qū)別的,但是數(shù)組可以調(diào)用方法使其自身改變,這種情況,訪問器屬性setter是攔截不到的。因?yàn)楦淖兊氖菙?shù)組的內(nèi)容,而不是數(shù)組本身。

setter攔截不到,就會(huì)導(dǎo)致依賴不能觸發(fā)。也就是說,關(guān)鍵點(diǎn)在于觸發(fā)依賴的位置。

起因都是由于數(shù)組的方法,所以我們想的是,數(shù)組方法在改變數(shù)組內(nèi)容時(shí),把依賴也觸發(fā)了。這觸發(fā)依賴是我們自定義的邏輯,總結(jié)起來就是,想要在數(shù)組的原生方法中增加自定義邏輯。

原生方法內(nèi)容是不可見的,我們也不能直接修改原生方法,因?yàn)闀?huì)對(duì)所有數(shù)組實(shí)例造成影響。但是,我們可以實(shí)現(xiàn)一個(gè)原生方法的超集,包含原生方法的邏輯與自定義的邏輯。

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

攔截?cái)?shù)組變異方式

覆蓋原型

數(shù)組實(shí)例的方法都是從原型上獲取的,數(shù)組原型上具有改變?cè)瓟?shù)組能力的方法有7個(gè):

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

構(gòu)造一個(gè)具有這7個(gè)方法的對(duì)象,然后重寫這7個(gè)方法,在方法內(nèi)部實(shí)現(xiàn)自定義的邏輯,最后調(diào)用真正的數(shù)組原型上的方法,從而可以實(shí)現(xiàn)對(duì)這7個(gè)方法的攔截。當(dāng)然,這個(gè)對(duì)象的原型是真正數(shù)組原型,保證其它數(shù)組特性不變。

最后,用這個(gè)對(duì)象替代需要被變異的數(shù)組實(shí)例的原型。

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);
    };
});

在數(shù)組實(shí)例上直接新增變異方法

連接數(shù)組原型與訪問器屬性getter

對(duì)象的dep是在defineReactive函數(shù)與訪問器屬性getter形成的閉包中,也就是說數(shù)組原型方法中是訪問不到這個(gè)dep的,所以這個(gè)dep,對(duì)于數(shù)組類型來說是不能使用了。

因此,我們需要構(gòu)建一個(gè)訪問器屬性與數(shù)組原型方法都可以訪問到的Dep類實(shí)例。所以構(gòu)建的位置很重要,不過正好有個(gè)位置滿足這個(gè)條件,那就是Observer類型的構(gòu)造函數(shù)中,因?yàn)樵L問器屬性與數(shù)組原型都是可以訪問到數(shù)組本身的。

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

在數(shù)組本身綁定了一個(gè)不可迭代的屬性ob,其值為Observer類的實(shí)例。現(xiàn)在,數(shù)組原型方法中可以訪問到dep了,進(jìn)行依賴觸發(fā):

methods.forEach(method => {
    const originMethod = arrayProto[method];
    
    arrayProto[method] = function (...args) {
        const ob = this.__ob__;
        const result = originMethod.apply(this, args);
        
        // 觸發(fā)依賴
        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只能收集到純對(duì)象類型的依賴,如果是數(shù)組類型,就用新增的childOb中的dep去收集依賴。也就是說,childOb是Observer類的實(shí)例,來看看dep的實(shí)現(xiàn):

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類的實(shí)例,如果有就直接使用這個(gè)值并返回,這里說明ob標(biāo)記了一個(gè)值是否被觀測(cè)。如果沒有,在value是純對(duì)象或數(shù)組類型的情況下,用value為參數(shù)實(shí)例化Observer類實(shí)例作為返回值。

完整代碼

// 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;
    }
}

總結(jié)

響應(yīng)式的關(guān)鍵點(diǎn)就在于讀取數(shù)據(jù)->收集依賴,修改數(shù)據(jù)->觸發(fā)依賴,由于數(shù)組的特殊性,所以要去攔截?cái)?shù)組變異的方法,但本質(zhì)其實(shí)并沒有變。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,125評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,402評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,934評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,168評(píng)論 0 287
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,690評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,596評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評(píng)論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,027評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評(píng)論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,398評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,743評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容