JS Event Loop(VUE nextTick)

前言

js是一個單線程的語言(非阻塞),最初的目的是為了和瀏覽器交互纷妆,也就是事件的輸入輸出流亦镶,計算機(jī)根據(jù)人類的指令做出不同的反應(yīng)結(jié)果,但是在JS執(zhí)行的過程環(huán)境中 我們有 幾個特殊的 “單詞” setTimeout 灼狰、setInterval宛瞄、 Promise、另外在 Node中還有 process.nextTick交胚。那么他們的執(zhí)行順序到底是怎么樣的呢份汗,瀏覽器不應(yīng)該是按照他們書寫的順序從上往下執(zhí)行嗎?

那你又有疑問了蝴簇,既然是單線程的杯活,在某個特定的時刻只有特定的代碼能夠被執(zhí)行,并阻塞其它的代碼熬词。

那不行啊旁钧,我們總不能一直等著啊,前端需要調(diào)用后端接口取數(shù)據(jù)互拾,這個過程是需要響應(yīng)時間的歪今,那執(zhí)行這個代碼的時候瀏覽器也等著?答案是否定的颜矿。

其實還有其他很多類線程(應(yīng)該叫做任務(wù)隊列)寄猩,比如進(jìn)行ajax請求、監(jiān)控用戶事件骑疆、定時器田篇、讀寫文件的線程(例如在NodeJS中)等等。

這些我們稱之為異步事件封断,當(dāng)異步事件發(fā)生時斯辰,將他們放入執(zhí)行隊列,等待當(dāng)前代碼執(zhí)行完成坡疼。就不會長時間阻塞主線程彬呻。

等主線程的代碼執(zhí)行完畢,然后再讀取任務(wù)隊列柄瑰,返回主線程繼續(xù)處理闸氮。如此循環(huán)這就是事件循環(huán)機(jī)制。

JS 在執(zhí)行的過程中會產(chǎn)生執(zhí)行環(huán)境教沾,這些執(zhí)行環(huán)境會被順序的加入到執(zhí)行棧中蒲跨。如果遇到異步的代碼,會被掛起并加入到 Task(有多種 task) 隊列中授翻。一旦執(zhí)行棧為空或悲,Event Loop 就會從 Task 隊列中拿出需要執(zhí)行的代碼并放入執(zhí)行棧中執(zhí)行孙咪,所以本質(zhì)上來說 JS 中的異步還是同步行為

舉個栗子

console.log('0');
setTimeout(() => {
  console.log('1');
}, 0);
console.log('2');
//輸出 0 , 2 ,1

看起來是setTimeout 設(shè)置了時間為0 但是 setTimeout 是一個“異步”的操作巡语,其實真是的情況是 setTimeout 的0 參數(shù)是無效的翎蹈, JS會給他默認(rèn)一個值為4毫秒。所以結(jié)果是 0 2 1 男公。


image.png

我們剛剛說到了“異步” 那么JS是怎么異步的呢 荤堪,其實在JS執(zhí)行的時候 不同的任務(wù)會分配到不同的隊列中,每個任務(wù)在制定的時候 已經(jīng)規(guī)定了他的基礎(chǔ)要素 也就是他屬于哪個隊列的 枢赔,任務(wù)源可以分為2類 微任務(wù) microtask宏任務(wù) macrotask,微任務(wù)又稱之為JOBS澄阳,宏任務(wù)稱為TASK。
我們在來看看下面這個例子

setTimeout(function() {
    console.log(1)
}, 0);
new Promise((resolve)=>{
    console.log(2);
    for(var i = 0; i < 10000; i++) {
        i == 9999 && resolve();
    }
    console.log(3);
}).then(function() {
    console.log(4);
});
console.log(5)
// 2 3 5 4  1

為什么是這個結(jié)果呢 踏拜。這就是 Jobs 和 Task的區(qū)別 我們下面仔細(xì)梳理下

微任務(wù)(Jobs)包括

process.nextTick

Promise

Object.observe(已廢棄)

MutationObserver (html5 新特性)

宏任務(wù)(Task)包括

setTimeout/setInterval

setImmediate

I/O操作

UI rendering

瀏覽器中新標(biāo)準(zhǔn)中的事件循環(huán)機(jī)制與 node.js 類似碎赢,其中會介紹到幾個nodejs有但是瀏覽器中沒有的 API,大家只需要了解就好执隧。

比如process.nextTick揩抡,setImmediate我們稱他們?yōu)槭录矗?事件源作為任務(wù)分發(fā)器,他們的回調(diào)函數(shù)才是被分發(fā)到任務(wù)隊列镀琉,而本身會立即執(zhí)行峦嗤。

例如,setTimeout第一個參數(shù)被分發(fā)到任務(wù)隊列屋摔,Promise 的 then 方法的回調(diào)函數(shù)被分發(fā)到任務(wù)隊列(catch方法同理)烁设。
不同源的事件被分發(fā)到不同的任務(wù)隊列,其中 setTimeoutsetInterval 屬于同源

整體代碼開始第一次循環(huán)钓试。全局上下文進(jìn)入函數(shù)調(diào)用棧装黑。直到調(diào)用棧清空(只剩全局),然后執(zhí)行所有的job弓熏。

當(dāng)所有可執(zhí)行的 job 執(zhí)行完畢之后恋谭。循環(huán)再次從task開始,找到其中一個任務(wù)隊列執(zhí)行完畢挽鞠,然后再執(zhí)行所有的 job疚颊,這樣一直循環(huán)下去。

無論是 task 還是 job信认,都是通過函數(shù)調(diào)用棧來完成材义。

這個時候我們是不是有一個大發(fā)現(xiàn),除了首次整體代碼的執(zhí)行嫁赏,其他的都有規(guī)律其掂,先執(zhí)行task任務(wù)隊列,再執(zhí)行所有的 job 并清空 job 隊列潦蝇。

再執(zhí)行 task—job—task—job……款熬,往復(fù)循環(huán)直到?jīng)]有可執(zhí)行代碼深寥。

那我們可不可以這么理解,第一次 script 代碼的執(zhí)行也算是一個task任務(wù)呢贤牛,如果這么理解那整個事件循環(huán)就很容易理解了翩迈。

UI rendering是在Task執(zhí)行之后就運(yùn)行的 那么我們只要把DOM操作放入Job中就可以提高渲染的性能了

下面我們說說 vue的 nextTick
還是舉個栗子

        <div id="app">
            <ul ref="list">
                <li  v-for="li in list">
                    {{li.name}}
                </li>
            </ul>
        </div>
new Vue({
    el: '#app',
    data: {
        list: []
    },
    mounted() {
        this.init()
    },
    methods: {
        init() {
            this.list = [{name:"lxl",age:18},{name:"kobe",age:19}]
            this.$refs.list.getElementsByTagName('li')[0].style.color = 'red'
                    
        },
    }
})

我們會發(fā)現(xiàn) 這樣會報錯


error.png

如下修改:

new Vue({
    el: '#app',
    data: {
        list: []
    },
    mounted() {
        this.init()
    },
    methods: {
        init() {
                this.list = [{name:"lxl",age:18},{name:"kobe",age:19}]
                this.$nextTick(()=>{
                    this.$refs.list.getElementsByTagName('li')[0].style.color = 'red'
                })
        },
    }
})
image.png

我在獲取到數(shù)據(jù)后賦值給 data 對象的 list 屬性,然后我想引用ul元素找到第一個li把它的顏色變?yōu)榧t色盔夜,但是事實上,這個要報錯的堤魁。

我們知道喂链,在執(zhí)行這句話時,ul 下面并沒有 li妥泉,也就是說剛剛進(jìn)行的賦值操作椭微,當(dāng)前并沒有引起視圖層的更新。

因為 Vue 的數(shù)據(jù)驅(qū)動視圖更新盲链,是異步的蝇率,即修改數(shù)據(jù)的當(dāng)下,視圖不會立刻更新刽沾,而是等同一事件循環(huán)中的所有數(shù)據(jù)變化完成之后本慕,再統(tǒng)一進(jìn)行視圖更新。

因此侧漓,在這樣的情況下锅尘,vue 給我們提供了 nextTick 方法,如果我們想對未來更新后的視圖進(jìn)行操作布蔗,我們只需要把要執(zhí)行的函數(shù)傳遞給 this.nextTick 方法藤违,vue 在更新完視圖后就會執(zhí)行我們的函數(shù)幫我們做事情。

nextTick 可以讓我們在下次 DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào)纵揍,用于獲得更新后的 DOM顿乒。

var callbacks = [];
var pending = false;

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

// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
var microTimerFunc;
var macroTimerFunc;
var useMacroTask = false;

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = function () {
    setImmediate(flushCallbacks);
  };
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  var channel = new MessageChannel();
  var port = channel.port2;
  channel.port1.onmessage = flushCallbacks;
  macroTimerFunc = function () {
    port.postMessage(1);
  };
} else {
  /* istanbul ignore next */
  macroTimerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  microTimerFunc = function () {
    p.then(flushCallbacks);
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) { setTimeout(noop); }
  };
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc;
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a (macro) task instead of a microtask.
 */
function withMacroTask (fn) {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true;
    var res = fn.apply(null, arguments);
    useMacroTask = false;
    return res
  })
}
function nextTick (cb, ctx) {
  var _resolve;
  callbacks.push(function () {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    if (useMacroTask) {
      macroTimerFunc();
    } else {
      microTimerFunc();
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(function (resolve) {
      _resolve = resolve;
    })
  }
}

綜合上面的代碼我們可以知道
在 Vue 2.4 之前都是使用的 microtasks,但是 microtasks 的優(yōu)先級過高泽谨,在某些情況下可能會出現(xiàn)比事件冒泡更快的情況璧榄,但如果都使用 macrotasks 又可能會出現(xiàn)渲染的性能問題。所以在新版本中隔盛,會默認(rèn)使用 microtasks犹菱,但在特殊情況下會使用 macrotasks,比如 v-on吮炕。

對于實現(xiàn) macrotasks 腊脱,會先判斷是否能使用 setImmediate ,不能的話降級為 MessageChannel 龙亲,以上都不行的話就使用 setTimeout
setImmediate傳送門
MessageChannel傳送門
event-loops傳送門

總結(jié)一下今天的知識

(1)所有同步任務(wù)都在主線程上執(zhí)行陕凹,形成一個執(zhí)行棧(execution context stack)悍抑。

(2)主線程之外,還存在一個"任務(wù)隊列"(task queue)杜耙。只要異步任務(wù)有了運(yùn)行結(jié)果搜骡,就在"任務(wù)隊列"之中放置一個事件。

(3)一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢佑女,系統(tǒng)就會讀取"任務(wù)隊列"记靡,看看里面有哪些事件。那些對應(yīng)的異步任務(wù)团驱,于是結(jié)束等待狀態(tài)摸吠,進(jìn)入執(zhí)行棧,開始執(zhí)行嚎花。

(4)主線程不斷重復(fù)上面的第三步

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寸痢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子紊选,更是在濱河造成了極大的恐慌啼止,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兵罢,死亡現(xiàn)場離奇詭異献烦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)趣些,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門仿荆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人坏平,你說我怎么就攤上這事拢操。” “怎么了舶替?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵令境,是天一觀的道長。 經(jīng)常有香客問我顾瞪,道長舔庶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任陈醒,我火速辦了婚禮惕橙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钉跷。我一直安慰自己弥鹦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著彬坏,像睡著了一般朦促。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上栓始,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天务冕,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播枉长,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了腹侣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤撤奸,失蹤者是張志新(化名)和其女友劉穎吠昭,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胧瓜,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡矢棚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了府喳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒲肋。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖钝满,靈堂內(nèi)的尸體忽然破棺而出兜粘,到底是詐尸還是另有隱情,我是刑警寧澤弯蚜,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布孔轴,位于F島的核電站,受9級特大地震影響碎捺,放射性物質(zhì)發(fā)生泄漏路鹰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一收厨、第九天 我趴在偏房一處隱蔽的房頂上張望晋柱。 院中可真熱鬧,春花似錦诵叁、人聲如沸雁竞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浓领。三九已至玉凯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間联贩,已是汗流浹背漫仆。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泪幌,地道東北人盲厌。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像祸泪,于是被迫代替她去往敵國和親吗浩。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

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