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

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

只要偵聽到數(shù)據(jù)變化般妙,Vue 將開啟一個(gè)隊(duì)列借笙,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更单料。如果同一個(gè) watcher 被多次觸發(fā)艇拍,只會(huì)被推入到隊(duì)列中一次眠蚂。這種在緩沖時(shí)去除重復(fù)數(shù)據(jù)對于避免不必要的計(jì)算和 DOM 操作是非常重要的私爷。然后雾棺,在下一個(gè)的事件循環(huán)“tick”中,Vue 刷新隊(duì)列并執(zhí)行實(shí)際 (已去重的) 工作衬浑。Vue 在內(nèi)部對異步隊(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框架并不推薦訪問DOM进统,但有時(shí)候確實(shí)會(huì)有這樣的需求,尤其是和第三方插件進(jìn)行配合的時(shí)候浪听,免不了要進(jìn)行DOM操作螟碎。而nextTick就提供了一個(gè)橋梁,確保我們操作的是更新后的DOM迹栓。

本文從這樣一個(gè)問題開始探索:vue如何檢測到DOM更新完畢呢抚芦?

檢索一下自己的前端知識庫,能監(jiān)聽到DOM改動(dòng)的API好像只有MutationObserver了迈螟,后面簡稱MO.

源碼如下:

var nextTick = (function () {
        var callbacks = []; // 存儲(chǔ)需要觸發(fā)的回調(diào)函數(shù)
        var pending = false; // 是否正在等待的標(biāo)識(false:允許觸發(fā)在下次事件循環(huán)觸發(fā)callbacks中的回調(diào), true: 已經(jīng)觸發(fā)過,需要等到下次事件循環(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();
            }
            //如果沒有傳遞回調(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)聽DOM修改事件,能夠監(jiān)聽到節(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來監(jiān)聽DOM更新完畢的呢耘拇?

那就打開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)
  }
}

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

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

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

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

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

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

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

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


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

關(guān)鍵的地方來了沪袭,每次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更新。

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

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

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

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

microtask

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

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

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

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

常見的microtask有:Promise、MutationObserver趁舀、Object.observe(廢棄)赖捌,以及nodejs中的process.nextTick.

咦?好像看到了MutationObserver矮烹,難道說vue用MO是想利用它的microtask特性越庇,而不是想做DOM監(jiān)聽奋隶?對嘍,就是這樣的悦荒。核心是microtask唯欣,用不用MO都行的。事實(shí)上搬味,vue在2.5版本中已經(jīng)刪去了MO相關(guān)的代碼境氢,因?yàn)樗荋TML5新增的特性,在iOS上尚有bug碰纬。

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

vue的降級策略

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

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

不賣關(guān)子了最域,在vue2.5的源碼中谴分,macrotask降級的方案依次是:setImmediate、MessageChannel镀脂、setTimeout.

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

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

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

總結(jié)

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

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

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

  3. 因?yàn)榧嫒菪詥栴}伪节,vue不得不做了microtask向macrotask的降級方案

相關(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)容合作請聯(lián)系作者
  • 序言:七十年代末光羞,一起剝皮案震驚了整個(gè)濱河市绩鸣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纱兑,老刑警劉巖呀闻,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異潜慎,居然都是意外死亡捡多,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門铐炫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來垒手,“玉大人,你說我怎么就攤上這事倒信】票幔” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵鳖悠,是天一觀的道長榜掌。 經(jīng)常有香客問我,道長乘综,這世上最難降的妖魔是什么唐责? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮瘾带,結(jié)果婚禮上鼠哥,老公的妹妹穿的比我還像新娘。我一直安慰自己看政,他們只是感情好朴恳,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著允蚣,像睡著了一般于颖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嚷兔,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天森渐,我揣著相機(jī)與錄音,去河邊找鬼冒晰。 笑死同衣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的壶运。 我是一名探鬼主播耐齐,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了埠况?” 一聲冷哼從身側(cè)響起耸携,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辕翰,沒想到半個(gè)月后夺衍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡喜命,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年沟沙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渊抄。...
    茶點(diǎn)故事閱讀 40,505評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡尝胆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出护桦,到底是詐尸還是另有隱情含衔,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布二庵,位于F島的核電站贪染,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏催享。R本人自食惡果不足惜杭隙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望因妙。 院中可真熱鬧痰憎,春花似錦、人聲如沸攀涵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽以故。三九已至蜗细,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間怒详,已是汗流浹背炉媒。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留昆烁,地道東北人吊骤。 一個(gè)月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像善玫,于是被迫代替她去往敵國和親水援。 傳聞我的和親對象是個(gè)殘疾皇子密强,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評論 2 359

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

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