7.批量異步更新策略及 nextTick 原理

批量異步更新策略及 nextTick 原理

為什么要異步更新

通過前面幾個(gè)章節(jié)我們介紹蜂莉,相信大家已經(jīng)明白了 Vue.js 是如何在我們修改 data 中的數(shù)據(jù)后修改視圖了送浊。簡單回顧一下仆救,這里面其實(shí)就是一個(gè)“setter -> Dep -> Watcher -> patch -> 視圖”的過程。

假設(shè)我們有如下這么一種情況速和。

<template>
  <div>
    <div>{{number}}</div>
    <div @click="handleClick">click</div>
  </div>
</template>

export default {
    data () {
        return {
            number: 0
        };
    },
    methods: {
        handleClick () {
            for(let i = 0; i < 1000; i++) {
                this.number++;
            }
        }
    }
}

當(dāng)我們按下 click 按鈕的時(shí)候驶俊,number 會(huì)被循環(huán)增加1000次。

那么按照之前的理解嬉探,每次 number 被 +1 的時(shí)候擦耀,都會(huì)觸發(fā) numbersetter 方法,從而根據(jù)上面的流程一直跑下來最后修改真實(shí) DOM涩堤。那么在這個(gè)過程中眷蜓,DOM 會(huì)被更新 1000 次!太可怕了胎围。

Vue.js 肯定不會(huì)以如此低效的方法來處理吁系。Vue.js在默認(rèn)情況下,每次觸發(fā)某個(gè)數(shù)據(jù)的 setter 方法后白魂,對(duì)應(yīng)的 Watcher 對(duì)象其實(shí)會(huì)被 push 進(jìn)一個(gè)隊(duì)列 queue 中汽纤,在下一個(gè) tick 的時(shí)候?qū)⑦@個(gè)隊(duì)列 queue 全部拿出來 runWatcher 對(duì)象的一個(gè)方法,用來觸發(fā) patch 操作) 一遍福荸。

那么什么是下一個(gè) tick 呢蕴坪?

nextTick

Vue.js 實(shí)現(xiàn)了一個(gè) nextTick 函數(shù),傳入一個(gè) cb 逞姿,這個(gè) cb 會(huì)被存儲(chǔ)到一個(gè)隊(duì)列中辞嗡,在下一個(gè) tick 時(shí)觸發(fā)隊(duì)列中的所有 cb 事件。

因?yàn)槟壳盀g覽器平臺(tái)并沒有實(shí)現(xiàn) nextTick 方法滞造,所以 Vue.js 源碼中分別用 Promise续室、setTimeoutsetImmediate 等方式在 microtask(或是task)中創(chuàng)建一個(gè)事件谒养,目的是在當(dāng)前調(diào)用棧執(zhí)行完畢以后(不一定立即)才會(huì)去執(zhí)行這個(gè)事件挺狰。

筆者用 setTimeout 來模擬這個(gè)方法,當(dāng)然买窟,真實(shí)的源碼中會(huì)更加復(fù)雜丰泊,筆者在小冊(cè)中只講原理,有興趣了解源碼中 nextTick 的具體實(shí)現(xiàn)的同學(xué)可以參考next-tick始绍。

首先定義一個(gè) callbacks 數(shù)組用來存儲(chǔ) nextTick瞳购,在下一個(gè) tick 處理這些回調(diào)函數(shù)之前,所有的 cb 都會(huì)被存在這個(gè) callbacks 數(shù)組中亏推。pending 是一個(gè)標(biāo)記位学赛,代表一個(gè)等待的狀態(tài)年堆。

setTimeout 會(huì)在 task 中創(chuàng)建一個(gè)事件 flushCallbacksflushCallbacks 則會(huì)在執(zhí)行時(shí)將 callbacks 中的所有 cb 依次執(zhí)行盏浇。

let callbacks = [];
let pending = false;

function nextTick (cb) {
    callbacks.push(cb);

    if (!pending) {
        pending = true;
        setTimeout(flushCallbacks, 0);
    }
}

function flushCallbacks () {
    pending = false;
    const copies = callbacks.slice(0);
    callbacks.length = 0;
    for (let i = 0; i < copies.length; i++) {
        copies[i]();
    }
}

再寫 Watcher

第一個(gè)例子中变丧,當(dāng)我們將 number 增加 1000 次時(shí),先將對(duì)應(yīng)的 Watcher 對(duì)象給 push 進(jìn)一個(gè)隊(duì)列 queue 中去绢掰,等下一個(gè) tick 的時(shí)候再去執(zhí)行痒蓬,這樣做是對(duì)的。但是有沒有發(fā)現(xiàn)滴劲,另一個(gè)問題出現(xiàn)了攻晒?

因?yàn)?number 執(zhí)行 ++ 操作以后對(duì)應(yīng)的 Watcher 對(duì)象都是同一個(gè),我們并不需要在下一個(gè) tick 的時(shí)候執(zhí)行 1000 個(gè)同樣的 Watcher 對(duì)象去修改界面哑芹,而是只需要執(zhí)行一個(gè) Watcher 對(duì)象缀去,使其將界面上的 0 變成 1000 即可干茉。

那么唠梨,我們就需要執(zhí)行一個(gè)過濾的操作棵譬,同一個(gè)的 Watcher 在同一個(gè) tick 的時(shí)候應(yīng)該只被執(zhí)行一次,也就是說隊(duì)列 queue 中不應(yīng)該出現(xiàn)重復(fù)的 Watcher 對(duì)象末购。

那么我們給 Watcher 對(duì)象起個(gè)名字吧~用 id 來標(biāo)記每一個(gè) Watcher 對(duì)象破喻,讓他們看起來“不太一樣”。

實(shí)現(xiàn) update 方法盟榴,在修改數(shù)據(jù)后由 Dep 來調(diào)用曹质, 而 run 方法才是真正的觸發(fā) patch 更新視圖的方法。

let uid = 0;

class Watcher {
    constructor () {
        this.id = ++uid;
    }

    update () {
        console.log('watch' + this.id + ' update');
        queueWatcher(this);
    }

    run () {
        console.log('watch' + this.id + '視圖更新啦~');
    }
}

queueWatcher

不知道大家注意到了沒有擎场?筆者已經(jīng)將 Watcherupdate 中的實(shí)現(xiàn)改成了

queueWatcher(this);

Watcher 對(duì)象自身傳遞給 queueWatcher 方法羽德。

我們來實(shí)現(xiàn)一下 queueWatcher 方法。

let has = {};
let queue = [];
let waiting = false;

function queueWatcher(watcher) {
    const id = watcher.id;
    if (has[id] == null) {
        has[id] = true;
        queue.push(watcher);

        if (!waiting) {
            waiting = true;
            nextTick(flushSchedulerQueue);
        }
    }
}

我們使用一個(gè)叫做 has 的 map迅办,里面存放 id -> true ( false ) 的形式宅静,用來判斷是否已經(jīng)存在相同的 Watcher 對(duì)象 (這樣比每次都去遍歷 queue 效率上會(huì)高很多)。

如果目前隊(duì)列 queue 中還沒有這個(gè) Watcher 對(duì)象站欺,則該對(duì)象會(huì)被 push 進(jìn)隊(duì)列 queue 中去姨夹。

waiting 是一個(gè)標(biāo)記位,標(biāo)記是否已經(jīng)向 nextTick 傳遞了 flushSchedulerQueue 方法矾策,在下一個(gè) tick 的時(shí)候執(zhí)行 flushSchedulerQueue 方法來 flush 隊(duì)列 queue磷账,執(zhí)行它里面的所有 Watcher 對(duì)象的 run 方法。

flushSchedulerQueue

function flushSchedulerQueue () {
    let watcher, id;

    for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        id = watcher.id;
        has[id] = null;
        watcher.run();
    }

    waiting  = false;
}

舉個(gè)例子

let watch1 = new Watcher();
let watch2 = new Watcher();

watch1.update();
watch1.update();
watch2.update();

我們現(xiàn)在 new 了兩個(gè) Watcher 對(duì)象贾虽,因?yàn)樾薷牧?data 的數(shù)據(jù)逃糟,所以我們模擬觸發(fā)了兩次 watch1update 以及 一次 watch2update

假設(shè)沒有批量異步更新策略的話,理論上應(yīng)該執(zhí)行 Watcher 對(duì)象的 run绰咽,那么會(huì)打印蛉抓。

watch1 update
watch1視圖更新啦~
watch1 update
watch1視圖更新啦~
watch2 update
watch2視圖更新啦~

實(shí)際上則執(zhí)行

watch1 update
watch1 update
watch2 update
watch1視圖更新啦~
watch2視圖更新啦~

這就是異步更新策略的效果,相同的 Watcher 對(duì)象會(huì)在這個(gè)過程中被剔除剃诅,在下一個(gè) tick 的時(shí)候去更新視圖,從而達(dá)到對(duì)我們第一個(gè)例子的優(yōu)化驶忌。

我們?cè)倩剡^頭聊一下第一個(gè)例子矛辕, number 會(huì)被不停地進(jìn)行 ++ 操作,不斷地觸發(fā)它對(duì)應(yīng)的 Dep 中的 Watcher 對(duì)象的 update 方法付魔。然后最終 queue 中因?yàn)閷?duì)相同 idWatcher 對(duì)象進(jìn)行了篩選聊品,從而 queue 中實(shí)際上只會(huì)存在一個(gè) number 對(duì)應(yīng)的 Watcher 對(duì)象。在下一個(gè) tick 的時(shí)候(此時(shí) number 已經(jīng)變成了 1000)几苍,觸發(fā) Watcher 對(duì)象的 run 方法來更新視圖翻屈,將視圖上的 number 從 0 直接變成 1000。

到這里妻坝,批量異步更新策略及 nextTick 原理已經(jīng)講完了伸眶,接下來讓我們學(xué)習(xí)一下 Vuex 狀態(tài)管理的工作原理。

注:本節(jié)代碼參考《批量異步更新策略及 nextTick 原理》刽宪。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末厘贼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子圣拄,更是在濱河造成了極大的恐慌嘴秸,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件庇谆,死亡現(xiàn)場離奇詭異岳掐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)饭耳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門串述,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人哥攘,你說我怎么就攤上這事剖煌。” “怎么了逝淹?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵耕姊,是天一觀的道長。 經(jīng)常有香客問我栅葡,道長茉兰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任欣簇,我火速辦了婚禮规脸,結(jié)果婚禮上坯约,老公的妹妹穿的比我還像新娘。我一直安慰自己莫鸭,他們只是感情好闹丐,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著被因,像睡著了一般卿拴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上梨与,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天堕花,我揣著相機(jī)與錄音,去河邊找鬼粥鞋。 笑死缘挽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的呻粹。 我是一名探鬼主播壕曼,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼尚猿!你這毒婦竟也來了窝稿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤凿掂,失蹤者是張志新(化名)和其女友劉穎伴榔,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庄萎,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡踪少,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了糠涛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片援奢。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖忍捡,靈堂內(nèi)的尸體忽然破棺而出集漾,到底是詐尸還是另有隱情,我是刑警寧澤砸脊,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布具篇,位于F島的核電站,受9級(jí)特大地震影響凌埂,放射性物質(zhì)發(fā)生泄漏驱显。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望埃疫。 院中可真熱鬧伏恐,春花似錦、人聲如沸栓霜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胳蛮。三九已至秤掌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鹰霍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國打工茵乱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留茂洒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓瓶竭,卻偏偏與公主長得像督勺,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子斤贰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

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