Vue.nextTick實(shí)現(xiàn)原理

vue 2.X 深入響應(yīng)式原理的異步更新隊(duì)列中說(shuō)明如下:

只要偵聽(tīng)到數(shù)據(jù)變化,Vue 將開(kāi)啟一個(gè)隊(duì)列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更。如果同一個(gè) watcher 被多次觸發(fā),只會(huì)被推入到隊(duì)列中一次。這種在緩沖時(shí)去除重復(fù)數(shù)據(jù)對(duì)于避免不必要的計(jì)算和 DOM 操作是非常重要的。然后,在下一個(gè)的事件循環(huán)“tick”中,Vue 刷新隊(duì)列并執(zhí)行實(shí)際 (已去重的) 工作。Vue 在內(nèi)部對(duì)異步隊(duì)列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,如果執(zhí)行環(huán)境不支持,則會(huì)采用 setTimeout(fn, 0) 代替。

用法如下:

<div id="example">{{message}}</div>

var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改數(shù)據(jù)
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

盡管MVVM框架并不推薦訪問(wèn)DOM,但有時(shí)候確實(shí)會(huì)有這樣的需求,尤其是和第三方插件進(jìn)行配合的時(shí)候,免不了要進(jìn)行DOM操作。而nextTick就提供了一個(gè)橋梁,確保我們操作的是更新后的DOM。

本文從這樣一個(gè)問(wèn)題開(kāi)始探索:vue如何檢測(cè)到DOM更新完畢呢?

檢索一下自己的前端知識(shí)庫(kù),能監(jiān)聽(tīng)到DOM改動(dòng)的API好像只有MutationObserver了,后面簡(jiǎn)稱(chēng)MO.

源碼如下:

var nextTick = (function () {
        var callbacks = []; // 存儲(chǔ)需要觸發(fā)的回調(diào)函數(shù)
        var pending = false; // 是否正在等待的標(biāo)識(shí)(false:允許觸發(fā)在下次事件循環(huán)觸發(fā)callbacks中的回調(diào), true: 已經(jīng)觸發(fā)過(guò),需要等到下次事件循環(huán))
        var timerFunc; // 設(shè)置在下次事件循環(huán)觸發(fā)callbacks的 觸發(fā)函數(shù)

        //處理callbacks的函數(shù)
        function nextTickHandler () {
            pending = false;// 可以觸發(fā)timeFunc
            var copies = callbacks.slice(0);//復(fù)制callback
            callbacks.length = 0;//清空callback
            for (var i = 0; i < copies.length; i++) {
                copies[i]();//觸發(fā)callback回調(diào)函數(shù)
            }
        }

        //如果支持Promise,使用Promise實(shí)現(xiàn)
        if (typeof Promise !== 'undefined' && isNative(Promise)) {
            var p = Promise.resolve();
            var logError = function (err) { console.error(err); };
            timerFunc = function () {
                p.then(nextTickHandler).catch(logError);
                // ios的webview下,需要強(qiáng)制刷新隊(duì)列,執(zhí)行上面的回調(diào)函數(shù)
                if (isIOS) { setTimeout(noop); }
            };

            //如果Promise不支持,但是支持MutationObserver
        } else if (typeof MutationObserver !== 'undefined' && (
                isNative(MutationObserver) ||
                // PhantomJS and iOS 7.x
                MutationObserver.toString() === '[object MutationObserverConstructor]'
            )) {
            // use MutationObserver where native Promise is not available,
            // e.g. PhantomJS IE11, iOS7, Android 4.4
            var counter = 1;
            var observer = new MutationObserver(nextTickHandler);
            //創(chuàng)建一個(gè)textnode dom節(jié)點(diǎn),并讓MutationObserver 監(jiān)視這個(gè)節(jié)點(diǎn);而 timeFunc正是改變這個(gè)dom節(jié)點(diǎn)的觸發(fā)函數(shù)
            var textNode = document.createTextNode(String(counter));
            observer.observe(textNode, {
                characterData: true
            });
            timerFunc = function () {
                counter = (counter + 1) % 2;
                textNode.data = String(counter);
            };
        } else {// 上面兩種不支持的話,就使用setTimeout

            timerFunc = function () {
                setTimeout(nextTickHandler, 0);
            };
        }
        //nextTick接受的函數(shù), 參數(shù)1:回調(diào)函數(shù)  參數(shù)2:回調(diào)函數(shù)的執(zhí)行上下文
        return function queueNextTick (cb, ctx) {
            var _resolve;//用于接受觸發(fā) promise.then中回調(diào)的函數(shù)
            //向回調(diào)數(shù)據(jù)中pushcallback
            callbacks.push(function () {
                //如果有回調(diào)函數(shù),執(zhí)行回調(diào)函數(shù)
                if (cb) { cb.call(ctx); }
                if (_resolve) { _resolve(ctx); }//觸發(fā)promise的then回調(diào)
            });
            if (!pending) {//是否執(zhí)行刷新callback隊(duì)列
                pending = true;
                timerFunc();
            }
            //如果沒(méi)有傳遞回調(diào)函數(shù),并且當(dāng)前瀏覽器支持promise,使用promise實(shí)現(xiàn)
            if (!cb && typeof Promise !== 'undefined') {
                return new Promise(function (resolve) {
                    _resolve = resolve;
                })
            }
        }
    })();

理解MutationObserver

MutationObserver是HTML5新增的屬性,用于監(jiān)聽(tīng)DOM修改事件,能夠監(jiān)聽(tīng)到節(jié)點(diǎn)的屬性、文本內(nèi)容、子節(jié)點(diǎn)等的改動(dòng),是一個(gè)功能強(qiáng)大的利器,基本用法如下:

//MO基本用法
var observer = new MutationObserver(function(){
  //這里是回調(diào)函數(shù)
  console.log('DOM被修改了!');
});

var article = document.querySelector('article');
observer.observer(article);

MO的使用不是本篇重點(diǎn)。這里我們要思考的是:vue是不是用MO來(lái)監(jiān)聽(tīng)DOM更新完畢的呢?

那就打開(kāi)vue的源碼看看吧,在實(shí)現(xiàn)nextTick的地方,確實(shí)能看到這樣的代碼:

//vue@2.2.5 /src/core/util/env.js
if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) {

  var counter = 1

  var observer = new MutationObserver(nextTickHandler)

  var textNode = document.createTextNode(String(counter))

  observer.observe(textNode, {

      characterData: true
  })

  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
}

簡(jiǎn)單解釋一下,如果檢測(cè)到瀏覽器支持MO,則創(chuàng)建一個(gè)文本節(jié)點(diǎn),監(jiān)聽(tīng)這個(gè)文本節(jié)點(diǎn)的改動(dòng)事件,以此來(lái)觸發(fā)nextTickHandler(也就是DOM更新完畢回調(diào))的執(zhí)行。后面的代碼中,會(huì)執(zhí)行手工修改文本節(jié)點(diǎn)屬性,這樣就能進(jìn)入到回調(diào)函數(shù)了。
大體掃了一眼,似乎可以得到實(shí)錘了:哦!vue是用MutationObserver監(jiān)聽(tīng)DOM更新完畢的!

難道不感覺(jué)哪里不對(duì)勁嗎?讓我們細(xì)細(xì)想一下:

  1. 我們要監(jiān)聽(tīng)的是模板中的DOM更新完畢,vue為什么自己創(chuàng)建了一個(gè)文本節(jié)點(diǎn)來(lái)監(jiān)聽(tīng),這有點(diǎn)說(shuō)不通啊!

  2. 難道自己創(chuàng)建的文本節(jié)點(diǎn)更新完畢,就能代表其他DOM節(jié)點(diǎn)更新完畢嗎?這又是什么道理!

看來(lái)我們上面得出的結(jié)論并不對(duì),這時(shí)候就需要講講js的事件循環(huán)機(jī)制了。

事件循環(huán)(Event Loop)

在js的運(yùn)行環(huán)境中,我們這里光說(shuō)瀏覽器吧,通常伴隨著很多事件的發(fā)生,比如用戶(hù)點(diǎn)擊、頁(yè)面渲染、腳本執(zhí)行、網(wǎng)絡(luò)請(qǐng)求,等等。為了協(xié)調(diào)這些事件的處理,瀏覽器使用事件循環(huán)機(jī)制。

簡(jiǎn)要來(lái)說(shuō),事件循環(huán)會(huì)維護(hù)一個(gè)或多個(gè)任務(wù)隊(duì)列(task queues),以上提到的事件作為任務(wù)源往隊(duì)列中加入任務(wù)。有一個(gè)持續(xù)執(zhí)行的線程來(lái)處理這些任務(wù),每執(zhí)行完一個(gè)就從隊(duì)列中移除它,這就是一次事件循環(huán)了,如下圖所示:


我們平時(shí)用setTimeout來(lái)執(zhí)行異步代碼,其實(shí)就是在任務(wù)隊(duì)列的末尾加入了一個(gè)task,待前面的任務(wù)都執(zhí)行完后再執(zhí)行它。

關(guān)鍵的地方來(lái)了,每次event loop的最后,會(huì)有一個(gè)UI render步驟,也就是更新DOM。標(biāo)準(zhǔn)為什么這樣設(shè)計(jì)呢?考慮下面的代碼:

for(let i=0; i<100; i++){

    dom.style.left = i + 'px';
}

瀏覽器會(huì)進(jìn)行100次DOM更新嗎?顯然不是的,這樣太耗性能了。事實(shí)上,這100次for循環(huán)同屬一個(gè)task,瀏覽器只在該task執(zhí)行完后進(jìn)行一次DOM更新。

那我們的思路就來(lái)了:只要讓nextTick里的代碼放在UI render步驟后面執(zhí)行,豈不就能訪問(wèn)到更新后的DOM了?

vue就是這樣的思路,并不是用MO進(jìn)行DOM變動(dòng)監(jiān)聽(tīng),而是用隊(duì)列控制的方式達(dá)到目的。那么vue又是如何做到隊(duì)列控制的呢?我們可以很自然的想到setTimeout,把nextTick要執(zhí)行的代碼當(dāng)作下一個(gè)task放入隊(duì)列末尾。

然而事情卻沒(méi)這么簡(jiǎn)單,vue的數(shù)據(jù)響應(yīng)過(guò)程包含:數(shù)據(jù)更改->通知Watcher->更新DOM。而數(shù)據(jù)的更改不由我們控制,可能在任何時(shí)候發(fā)生。如果恰巧發(fā)生在repaint之前,就會(huì)發(fā)生多次渲染。這意味著性能浪費(fèi),是vue不愿意看到的。

所以,vue的隊(duì)列控制是經(jīng)過(guò)了深思熟慮的(也經(jīng)過(guò)了多次改動(dòng))。在這之前,我們還需了解event loop的另一個(gè)重要概念,microtask.

microtask

從名字看,我們可以把它稱(chēng)為微任務(wù)。對(duì)應(yīng)的,task隊(duì)列中的任務(wù)也被叫做macrotask。名字相似,性質(zhì)可不一樣了。

每一次事件循環(huán)都包含一個(gè)microtask隊(duì)列,在循環(huán)結(jié)束后會(huì)依次執(zhí)行隊(duì)列中的microtask并移除,然后再開(kāi)始下一次事件循環(huán)。

在執(zhí)行microtask的過(guò)程中后加入microtask隊(duì)列的微任務(wù),也會(huì)在下一次事件循環(huán)之前被執(zhí)行。也就是說(shuō),macrotask總要等到microtask都執(zhí)行完后才能執(zhí)行,microtask有著更高的優(yōu)先級(jí)。

microtask的這一特性,簡(jiǎn)直是做隊(duì)列控制的最佳選擇啊!vue進(jìn)行DOM更新內(nèi)部也是調(diào)用nextTick來(lái)做異步隊(duì)列控制。而當(dāng)我們自己調(diào)用nextTick的時(shí)候,它就在更新DOM的那個(gè)microtask后追加了我們自己的回調(diào)函數(shù),從而確保我們的代碼在DOM更新后執(zhí)行,同時(shí)也避免了setTimeout可能存在的多次執(zhí)行問(wèn)題。

常見(jiàn)的microtask有:Promise、MutationObserver、Object.observe(廢棄),以及nodejs中的process.nextTick.

咦?好像看到了MutationObserver,難道說(shuō)vue用MO是想利用它的microtask特性,而不是想做DOM監(jiān)聽(tīng)?對(duì)嘍,就是這樣的。核心是microtask,用不用MO都行的。事實(shí)上,vue在2.5版本中已經(jīng)刪去了MO相關(guān)的代碼,因?yàn)樗荋TML5新增的特性,在iOS上尚有bug。

那么最優(yōu)的microtask策略就是Promise了,而令人尷尬的是,Promise是ES6新增的東西,也存在兼容問(wèn)題呀~ 所以vue就面臨一個(gè)降級(jí)策略。

vue的降級(jí)策略

上面我們講到了,隊(duì)列控制的最佳選擇是microtask,而microtask的最佳選擇是Promise.但如果當(dāng)前環(huán)境不支持Promise,vue就不得不降級(jí)為macrotask來(lái)做隊(duì)列控制了。

macrotask有哪些可選的方案呢?前面提到了setTimeout是一種,但它不是理想的方案。因?yàn)閟etTimeout執(zhí)行的最小時(shí)間間隔是約4ms的樣子,略微有點(diǎn)延遲。還有其他的方案嗎?

不賣(mài)關(guān)子了,在vue2.5的源碼中,macrotask降級(jí)的方案依次是:setImmediate、MessageChannel、setTimeout.

setImmediate是最理想的方案了,可惜的是只有IE和nodejs支持。

MessageChannel的onmessage回調(diào)也是microtask,但也是個(gè)新API,面臨兼容性的尷尬...

所以最后的兜底方案就是setTimeout了,盡管它有執(zhí)行延遲,可能造成多次渲染,算是沒(méi)有辦法的辦法了。

總結(jié)

以上就是vue的nextTick方法的實(shí)現(xiàn)原理了,總結(jié)一下就是:

  1. vue用異步隊(duì)列的方式來(lái)控制DOM更新和nextTick回調(diào)先后執(zhí)行

  2. microtask因?yàn)槠涓邇?yōu)先級(jí)特性,能確保隊(duì)列中的微任務(wù)在一次事件循環(huán)前被執(zhí)行完畢

  3. 因?yàn)榧嫒菪詥?wèn)題,vue不得不做了microtask向macrotask的降級(jí)方案

相關(guān)資料:

event loop標(biāo)準(zhǔn)

https://html.spec.whatwg.org/multipage/webappapis.html#event-loops

vue2.5的nextTick更改記錄

https://github.com/vuejs/vue/commit/6e41679a96582da3e0a60bdbf123c33ba0e86b31

源碼解析文章

https://github.com/answershuto/learnVue/blob/master/docs/Vue.js%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0DOM%E7%AD%96%E7%95%A5%E5%8F%8AnextTick.MarkDown

選自:
全面解析Vue.nextTick實(shí)現(xiàn)原理
Vuejs中nextTick()異步更新隊(duì)列源碼解析

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

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

  • 找了一個(gè)實(shí)習(xí),去公司做數(shù)據(jù)的可視化,就是用iview-admin,Echarts做一下展示。中間遇到了一個(gè)問(wèn)題數(shù)據(jù)...
    小白小白啦閱讀 8,694評(píng)論 1 8
  • 前言 為何單線程? 因?yàn)槿绻贒OM操作中,有兩個(gè)線程一個(gè)添加節(jié)點(diǎn),一個(gè)刪除節(jié)點(diǎn),瀏覽器并不知道以哪個(gè)為準(zhǔn),所以只...
    nawussika閱讀 4,243評(píng)論 1 8
  • 前言 見(jiàn)解有限,如有描述不當(dāng)之處,請(qǐng)幫忙及時(shí)指出,如有錯(cuò)誤,會(huì)及時(shí)修正。 ----------超長(zhǎng)文+多圖預(yù)警,需...
    hnscdg閱讀 2,375評(píng)論 0 32
  • 瀏覽器相關(guān)理解 1 概念理解 1.1 進(jìn)程和線程 進(jìn)程> 進(jìn)程是一個(gè)工廠,工廠有它的獨(dú)立資源> 工廠之間相互獨(dú)立 ...
    Haiya_32ef閱讀 897評(píng)論 0 0
  • 擁有一個(gè)幸福的童年,用童年修復(fù)自己的一生,擁有不幸的童年,用一生來(lái)修復(fù)童年。 我從出生到現(xiàn)在一直都是在一個(gè)充滿(mǎn)愛(ài)的...
    湖泖子閱讀 129評(píng)論 0 0