vue.js響應(yīng)式系統(tǒng)的基本原理

摘自《剖析Vue.js內(nèi)部運(yùn)行機(jī)制》

Vue.js是一款MVVM框架婿奔,數(shù)據(jù)模型僅僅是普通的JavaScript對(duì)象,但是對(duì)這些對(duì)象進(jìn)行操作時(shí),卻能影響對(duì)應(yīng)視圖碗暗,它的核心實(shí)現(xiàn)就是響應(yīng)式系統(tǒng)。盡管我們?cè)谑褂?code>Vue.js 進(jìn)行開發(fā)時(shí)不會(huì)直接修改響應(yīng)式系統(tǒng)梢夯,但是理解它的實(shí)現(xiàn)有助于避開一些常見的坑言疗,也有助于在遇見一些琢磨不透的問題時(shí)可以深入其原理來解決它。

Object.defineProperty

首先我們來介紹一下 Object.defineProperty颂砸,Vue.js 就是基于它實(shí)現(xiàn)響應(yīng)式系統(tǒng)的噪奄。

/*
    obj: 目標(biāo)對(duì)象
    prop: 需要操作的目標(biāo)對(duì)象的屬性名
    descriptor: 描述符
    
    return value 傳入對(duì)象
*/
Object.defineProperty(obj, prop, descriptor)

descriptor的一些屬性:

  • enumerable:屬性是否可枚舉,默認(rèn)false沾凄。
  • configurable:屬性是否可以被修改或者刪除梗醇,默認(rèn) false
  • get:獲取屬性的方法撒蟀。
  • set:設(shè)置屬性的方法叙谨。

實(shí)現(xiàn) observer (可觀察的)

知道了Object.defineProperty 以后,我們來用它使對(duì)象變成可觀察的保屯。

首先我們定義一個(gè)cb函數(shù)手负,這個(gè)函數(shù)用來模擬視圖更新,調(diào)用它即代表更新視圖姑尺,內(nèi)部可以是一些更新視圖的方法竟终。

function cb (val) {
    /* 渲染視圖 */
    console.log("視圖更新啦~");
}

然后我們定義一個(gè)defineReactive,這個(gè)方法通過 Object.defineProperty來實(shí)現(xiàn)對(duì)對(duì)象的響應(yīng)式化切蟋,入?yún)⑹且粋€(gè)obj(需要綁定的對(duì)象)统捶、keyobj 的某一個(gè)屬性),val(具體的值)柄粹。經(jīng)過 defineReactive處理以后喘鸟,我們的objkey屬性在的時(shí)候會(huì)觸發(fā)reactiveGetter方法,而在該屬性被的時(shí)候則會(huì)觸發(fā) reactiveSetter方法驻右。

function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,       /* 屬性可枚舉 */
        configurable: true,     /* 屬性可被修改或刪除 */
        get: function reactiveGetter () {
            return val;         /* 實(shí)際上會(huì)依賴收集什黑,下一小節(jié)會(huì)講 */
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            cb(newVal);
        }
    });
}

當(dāng)然這是不夠的,我們需要在上面再封裝一層 observer堪夭。這個(gè)函數(shù)傳入一個(gè)value(需要響應(yīng)式化的對(duì)象)愕把,通過遍歷所有屬性的方式對(duì)該對(duì)象的每一個(gè)屬性都通過 defineReactive 處理拣凹。

function observer (value) {
    if (!value || (typeof value !== 'object')) {
        return;
    }
    
    Object.keys(value).forEach((key) => {
        defineReactive(value, key, value[key]);
    });
}

最后,讓我們用observer來封裝一個(gè)Vue吧恨豁!

Vue的構(gòu)造函數(shù)中嚣镜,對(duì) optionsdata 進(jìn)行處理,這里的 data想必大家很熟悉圣絮,就是平時(shí)我們?cè)趯?code>Vue 項(xiàng)目時(shí)組件中的data屬性(實(shí)際上是一個(gè)函數(shù)祈惶,這里當(dāng)作一個(gè)對(duì)象來簡單處理)。

class Vue {
    /* Vue 構(gòu)造類 */
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}

這樣我們只要 new一個(gè)Vue對(duì)象扮匠,就會(huì)將 data中的數(shù)據(jù)進(jìn)行響應(yīng)式化捧请。如果我們對(duì)data的屬性進(jìn)行下面的操作,就會(huì)觸發(fā) cb 方法更新視圖棒搜。

let o = new Vue({
    data: {
        test: "I am test."
    }
});
o._data.test = "hello,world.";  /* 視圖更新啦~ */

至此疹蛉,響應(yīng)式原理已經(jīng)介紹完了,接下來讓我們學(xué)習(xí)響應(yīng)式系統(tǒng)的另一部分 ——依賴收集力麸。

響應(yīng)式系統(tǒng)的依賴收集追蹤原理

栗子一:

我們現(xiàn)在有這么一個(gè) Vue對(duì)象可款。

new Vue({
    template: 
        `<div>
            <span>{{text1}}</span> 
            <span>{{text2}}</span> 
        <div>`,
    data: {
        text1: 'text1',
        text2: 'text2',
        text3: 'text3'
    }
});

然后我們做了這么一個(gè)操作。

this.text3 = 'modify text3';

我們修改了 datatext3 的數(shù)據(jù)克蚂,但是因?yàn)橐晥D中并不需要用到 text3闺鲸,所以我們并不需要觸發(fā)上一章所講的cb函數(shù)來更新視圖,調(diào)用cb顯然是不正確的埃叭。

栗子二:

let globalObj = {
    text1: 'text1'
};
let o1 = new Vue({
    template:
        `<div>
            <span>{{text1}}</span> 
        <div>`,
    data: globalObj
});
let o2 = new Vue({
    template:
        `<div>
            <span>{{text1}}</span> 
        <div>`,
    data: globalObj
});

這個(gè)時(shí)候宅广,我們執(zhí)行了如下操作秸侣。

globalObj.text1 = 'hello,text1';

我們應(yīng)該需要通知o1以及o2兩個(gè) vm實(shí)例進(jìn)行視圖的更新逸贾,依賴收集會(huì)讓text1這個(gè)數(shù)據(jù)知道:“有兩個(gè)地方依賴我的數(shù)據(jù)师逸,我變化的時(shí)候需要通知它們”。

接下來我們來介紹一下依賴收集是如何實(shí)現(xiàn)的类早。

訂閱者 Dep

首先我們來實(shí)現(xiàn)一個(gè)訂閱者Dep媚媒,它的主要作用是用來存放Watcher觀察者對(duì)象。

class Dep {
    constructor () {
        /* 用來存放 Watcher 對(duì)象的數(shù)組 */
        this.subs = [];
    }
    /* 在 subs 中添加一個(gè) Watcher 對(duì)象 */
    addSub (sub) {
        this.subs.push(sub);
    }
    /* 通知所有 Watcher 對(duì)象更新視圖 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

為了便于理解我們只實(shí)現(xiàn)了添加的部分代碼涩僻,主要是兩件事情:

  • addSub方法可以在目前的Dep對(duì)象中增加一個(gè)Watcher 的訂閱操作缭召;
  • notify方法通知目前Dep 對(duì)象的subs中的所有 Watcher對(duì)象觸發(fā)更新操作。

觀察者 Watcher

class Watcher {
    constructor () {
        /* 在 new 一個(gè) Watcher 對(duì)象時(shí)將該對(duì)象賦值給 Dep.target逆日,在 get 中會(huì)用到 */
        Dep.target = this;
    }
    /* 更新視圖的方法 */
    update () {
        console.log("視圖更新啦~");
    }
}
Dep.target = null;

依賴收集

接下來我們修改一下defineReactive以及Vue的構(gòu)造函數(shù)恼琼,來完成依賴收集。

我們?cè)陂]包中增加了一個(gè)Dep 類的對(duì)象屏富,用來收集 Watcher對(duì)象。在對(duì)象被的時(shí)候蛙卤,會(huì)觸發(fā)reactiveGetter函數(shù)把當(dāng)前的 Watcher對(duì)象(存放在Dep.target中)收集到Dep 類中去狠半。之后如果當(dāng)該對(duì)象被的時(shí)候噩死,則會(huì)觸發(fā) reactiveSetter方法,通知 Dep類調(diào)用 notify來觸發(fā)所有Watcher 對(duì)象的 update 方法更新對(duì)應(yīng)視圖神年。

function defineReactive (obj, key, val) {
    /* 一個(gè) Dep 類對(duì)象 */
    const dep = new Dep();
    
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            /* 將 Dep.target(即當(dāng)前的 Watcher 對(duì)象存入 dep 的 subs 中) */
            dep.addSub(Dep.target);
            return val;         
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            /* 在 set 的時(shí)候觸發(fā) dep 的 notify 來通知所有的 Watcher 對(duì)象更新視圖 */
            dep.notify();
        }
    });
}
class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一個(gè) Watcher 觀察者對(duì)象已维,這時(shí)候 Dep.target 會(huì)指向這個(gè) Watcher 對(duì)象 */
        new Watcher();
        /* 在這里模擬 render 的過程,為了觸發(fā) test 屬性的 get 函數(shù) */
        console.log('render~', this._data.test);
    }
}

總結(jié)

首先在 observer 的過程中會(huì)注冊(cè)get方法已日,該方法用來進(jìn)行依賴收集垛耳。在它的閉包中會(huì)有一個(gè)Dep 對(duì)象,這個(gè)對(duì)象用來存放Watcher對(duì)象的實(shí)例飘千。其實(shí)依賴收集的過程就是把Watcher實(shí)例存放到對(duì)應(yīng)的 Dep 對(duì)象中去堂鲜。

get方法可以讓當(dāng)前的Watcher對(duì)象(Dep.target)存放到它的subs 中(addSub )方法,在數(shù)據(jù)變化時(shí)护奈,set會(huì)調(diào)用Dep對(duì)象的 notify方法通知它內(nèi)部所有的Watcher 對(duì)象進(jìn)行視圖更新缔莲。這是Object.definePropertyset/get方法處理的事情,那么依賴收集的前提條件還有兩個(gè):

  • 觸發(fā) get方法霉旗;
  • 新建一個(gè)Watcher對(duì)象痴奏。

這個(gè)我們?cè)?code>Vue 的構(gòu)造類中處理。新建一個(gè)Watcher 對(duì)象只需要new出來厌秒,這時(shí)候Dep.target 已經(jīng)指向了這個(gè)new出來的 Watcher 對(duì)象來读拆。

而觸發(fā) get方法也很簡單,實(shí)際上只要把 render function進(jìn)行渲染鸵闪,那么其中的依賴的對(duì)象都會(huì)被讀取檐晕,這里我們通過打印來模擬這個(gè)過程,讀取 test來觸發(fā)get 進(jìn)行依賴收集岛马。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末棉姐,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子啦逆,更是在濱河造成了極大的恐慌伞矩,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件夏志,死亡現(xiàn)場離奇詭異乃坤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)沟蔑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門湿诊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瘦材,你說我怎么就攤上這事厅须。” “怎么了食棕?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵朗和,是天一觀的道長错沽。 經(jīng)常有香客問我,道長眶拉,這世上最難降的妖魔是什么千埃? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮忆植,結(jié)果婚禮上放可,老公的妹妹穿的比我還像新娘。我一直安慰自己朝刊,他們只是感情好耀里,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坞古,像睡著了一般备韧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上痪枫,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天织堂,我揣著相機(jī)與錄音,去河邊找鬼奶陈。 笑死易阳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吃粒。 我是一名探鬼主播潦俺,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼徐勃!你這毒婦竟也來了事示?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤僻肖,失蹤者是張志新(化名)和其女友劉穎肖爵,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臀脏,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡劝堪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了揉稚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秒啦。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖搀玖,靈堂內(nèi)的尸體忽然破棺而出余境,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布葛超,位于F島的核電站暴氏,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏绣张。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一关带、第九天 我趴在偏房一處隱蔽的房頂上張望侥涵。 院中可真熱鬧,春花似錦宋雏、人聲如沸芜飘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嗦明。三九已至,卻和暖如春蚪燕,著一層夾襖步出監(jiān)牢的瞬間娶牌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來泰國打工馆纳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留诗良,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓鲁驶,卻偏偏與公主長得像鉴裹,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子钥弯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361