「硬核JS」一次搞懂JS運(yùn)行機(jī)制

前言

從開(kāi)始做前端到目前為止名秀,陸續(xù)看了很多帖子講JS運(yùn)行機(jī)制然想,看過(guò)不久就忘了捶牢,還是自己理一遍好些

通過(guò)碼字使自己對(duì)JS運(yùn)行機(jī)制相關(guān)內(nèi)容更加深刻(自己用心寫過(guò)的貼子哟沫,內(nèi)容也會(huì)牢記于心)

順道給大家看看(我太難了饺蔑,深夜碼字,反復(fù)修改嗜诀,說(shuō)這么多就是想請(qǐng)你點(diǎn)個(gè)贊在看)

參考了很多資料(帖子)猾警,取其精華,去其糟糠隆敢,都在文末发皿,可自行了解

是時(shí)候搞一波我大js了

從零到一百再到一,從多方面了解JS的運(yùn)行機(jī)制拂蝎,體會(huì)更深刻穴墅,請(qǐng)認(rèn)真讀下去

本文大致分為以下這樣的步驟來(lái)幫助我們由廣入深更加清晰的了解JS運(yùn)行機(jī)制

  • 首先我們要了解進(jìn)程和線程的概念
  • 其次我們要知道瀏覽器的進(jìn)程線程常識(shí)
  • 再然后通過(guò)Event Loop、宏任務(wù)(macrotask)微任務(wù)(microtask)來(lái)看瀏覽器的幾個(gè)線程間是怎樣配合的
  • 再然后通過(guò)例子來(lái)印證我們的猜想
  • 最后提下NodeJS的運(yùn)行機(jī)制

靈魂一問(wèn)

JS運(yùn)行機(jī)制在平常前端面試時(shí)不管是筆試題還是面試題命中率都極高

說(shuō)到JS運(yùn)行機(jī)制温自,你知道多少

看到這大家可能會(huì)說(shuō):JS運(yùn)行機(jī)制嘛玄货,很簡(jiǎn)單,事件循環(huán)捣作、宏微任務(wù)那點(diǎn)東西

是的誉结,作為一名前端我們都了解,但是如果這真的面試問(wèn)到了這個(gè)地方券躁,你真的可以答好嗎(靈魂一問(wèn)???)

不管你對(duì)JS了解多少惩坑,到這里大家不防先停止一下閱讀,假設(shè)你目前在面試也拜,面試官讓你闡述下JS運(yùn)行機(jī)制以舒,思考下你的答案,用20秒的時(shí)間(面試時(shí)20s已經(jīng)很長(zhǎng)了)慢哈,然后帶著答案再接著往下看蔓钟,有人曾經(jīng)說(shuō)過(guò):沒(méi)有思考的閱讀純粹是消磨時(shí)間罷了,這話很好(因?yàn)槭俏艺f(shuō)的卵贱,皮一下??)

也有很多剛開(kāi)始接觸JS的同學(xué)會(huì)被任務(wù)隊(duì)列 執(zhí)行棧 微任務(wù) 宏任務(wù)這些高大上點(diǎn)的名字搞的很懵

接下來(lái)滥沫,我們來(lái)細(xì)致的梳理一遍你就可以清晰的了解它們了

進(jìn)程與線程

什么是進(jìn)程

我們都知道侣集,CPU是計(jì)算機(jī)的核心,承擔(dān)所有的計(jì)算任務(wù)

官網(wǎng)說(shuō)法兰绣,進(jìn)程CPU資源分配的最小單位

字面意思就是進(jìn)行中的程序世分,我將它理解為一個(gè)可以獨(dú)立運(yùn)行且擁有自己的資源空間的任務(wù)程序

進(jìn)程包括運(yùn)行中的程序和程序所使用到的內(nèi)存和系統(tǒng)資源

CPU可以有很多進(jìn)程,我們的電腦每打開(kāi)一個(gè)軟件就會(huì)產(chǎn)生一個(gè)或多個(gè)進(jìn)程缀辩,為什么電腦運(yùn)行的軟件多就會(huì)卡臭埋,是因?yàn)?code>CPU給每個(gè)進(jìn)程分配資源空間,但是一個(gè)CPU一共就那么多資源臀玄,分出去越多瓢阴,越卡,每個(gè)進(jìn)程之間是相互獨(dú)立的健无,CPU在運(yùn)行一個(gè)進(jìn)程時(shí)荣恐,其他的進(jìn)程處于非運(yùn)行狀態(tài),CPU使用 時(shí)間片輪轉(zhuǎn)調(diào)度算法 來(lái)實(shí)現(xiàn)同時(shí)運(yùn)行多個(gè)進(jìn)程

什么是線程

線程CPU調(diào)度的最小單位

線程是建立在進(jìn)程的基礎(chǔ)上的一次程序運(yùn)行單位睬涧,通俗點(diǎn)解釋線程就是程序中的一個(gè)執(zhí)行流募胃,一個(gè)進(jìn)程可以有多個(gè)線程

一個(gè)進(jìn)程中只有一個(gè)執(zhí)行流稱作單線程,即程序執(zhí)行時(shí)畦浓,所走的程序路徑按照連續(xù)順序排下來(lái)痹束,前面的必須處理好,后面的才會(huì)執(zhí)行

一個(gè)進(jìn)程中有多個(gè)執(zhí)行流稱作多線程讶请,即在一個(gè)程序中可以同時(shí)運(yùn)行多個(gè)不同的線程來(lái)執(zhí)行不同的任務(wù)祷嘶, 也就是說(shuō)允許單個(gè)程序創(chuàng)建多個(gè)并行執(zhí)行的線程來(lái)完成各自的任務(wù)

進(jìn)程和線程的區(qū)別

進(jìn)程是操作系統(tǒng)分配資源的最小單位,線程是程序執(zhí)行的最小單位

一個(gè)進(jìn)程由一個(gè)或多個(gè)線程組成夺溢,線程可以理解為是一個(gè)進(jìn)程中代碼的不同執(zhí)行路線

進(jìn)程之間相互獨(dú)立论巍,但同一進(jìn)程下的各個(gè)線程間共享程序的內(nèi)存空間(包括代碼段、數(shù)據(jù)集风响、堆等)及一些進(jìn)程級(jí)的資源(如打開(kāi)文件和信號(hào))

調(diào)度和切換:線程上下文切換比進(jìn)程上下文切換要快得多

多進(jìn)程和多線程

多進(jìn)程:多進(jìn)程指的是在同一個(gè)時(shí)間里嘉汰,同一個(gè)計(jì)算機(jī)系統(tǒng)中如果允許兩個(gè)或兩個(gè)以上的進(jìn)程處于運(yùn)行狀態(tài)。多進(jìn)程帶來(lái)的好處是明顯的状勤,比如大家可以在網(wǎng)易云聽(tīng)歌的同時(shí)打開(kāi)編輯器敲代碼鞋怀,編輯器和網(wǎng)易云的進(jìn)程之間不會(huì)相互干擾

多線程:多線程是指程序中包含多個(gè)執(zhí)行流,即在一個(gè)程序中可以同時(shí)運(yùn)行多個(gè)不同的線程來(lái)執(zhí)行不同的任務(wù)持搜,也就是說(shuō)允許單個(gè)程序創(chuàng)建多個(gè)并行執(zhí)行的線程來(lái)完成各自的任務(wù)

JS為什么是單線程

JS的單線程密似,與它的用途有關(guān)。作為瀏覽器腳本語(yǔ)言葫盼,JavaScript的主要用途是與用戶互動(dòng)残腌,以及操作DOM。這決定了它只能是單線程,否則會(huì)帶來(lái)很復(fù)雜的同步問(wèn)題。比如,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn)疾忍,這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?

還有人說(shuō)js還有Worker線程黑竞,對(duì)的眨层,為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn)面哥,允許JavaScript腳本創(chuàng)建多個(gè)線程哎壳,但是子線程是完 全受主線程控制的,而且不得操作DOM

所以尚卫,這個(gè)標(biāo)準(zhǔn)并沒(méi)有改變JavaScript是單線程的本質(zhì)

了解了進(jìn)程和線程之后归榕,接下來(lái)看看瀏覽器解析,瀏覽器之間也是有些許差距的吱涉,不過(guò)大致是差不多的刹泄,下文我們皆用市場(chǎng)占有比例最大的Chrome為例

瀏覽器

瀏覽器是多進(jìn)程的

作為前端,免不了和瀏覽器打交道怎爵,瀏覽器是多進(jìn)程的特石,拿Chrome來(lái)說(shuō),我們每打開(kāi)一個(gè)Tab頁(yè)就會(huì)產(chǎn)生一個(gè)進(jìn)程鳖链,我們使用Chrome打開(kāi)很多標(biāo)簽頁(yè)不關(guān)姆蘸,電腦會(huì)越來(lái)越卡,不說(shuō)其他芙委,首先就很耗CPU

瀏覽器包含哪些進(jìn)程

  • Browser進(jìn)程

    • 瀏覽器的主進(jìn)程(負(fù)責(zé)協(xié)調(diào)逞敷、主控),該進(jìn)程只有一個(gè)
    • 負(fù)責(zé)瀏覽器界面顯示灌侣,與用戶交互推捐。如前進(jìn),后退等
    • 負(fù)責(zé)各個(gè)頁(yè)面的管理侧啼,創(chuàng)建和銷毀其他進(jìn)程
    • 將渲染(Renderer)進(jìn)程得到的內(nèi)存中的Bitmap(位圖)牛柒,繪制到用戶界面上
    • 網(wǎng)絡(luò)資源的管理,下載等
  • 第三方插件進(jìn)程

    • 每種類型的插件對(duì)應(yīng)一個(gè)進(jìn)程慨菱,當(dāng)使用該插件時(shí)才創(chuàng)建
  • GPU進(jìn)程

    • 該進(jìn)程也只有一個(gè)焰络,用于3D繪制等等
  • 渲染進(jìn)程(重)

    • 即通常所說(shuō)的瀏覽器內(nèi)核(Renderer進(jìn)程,內(nèi)部是多線程)
    • 每個(gè)Tab頁(yè)面都有一個(gè)渲染進(jìn)程符喝,互不影響
    • 主要作用為頁(yè)面渲染闪彼,腳本執(zhí)行,事件處理等

為什么瀏覽器要多進(jìn)程

我們假設(shè)瀏覽器是單進(jìn)程,那么某個(gè)Tab頁(yè)崩潰了畏腕,就影響了整個(gè)瀏覽器缴川,體驗(yàn)有多差

同理如果插件崩潰了也會(huì)影響整個(gè)瀏覽器

當(dāng)然多進(jìn)程還有其它的諸多優(yōu)勢(shì),不過(guò)多闡述

瀏覽器進(jìn)程有很多描馅,每個(gè)進(jìn)程又有很多線程把夸,都會(huì)占用內(nèi)存

這也意味著內(nèi)存等資源消耗會(huì)很大,有點(diǎn)拿空間換時(shí)間的意思

到此可不只是為了讓我們理解為何Chrome運(yùn)行時(shí)間長(zhǎng)了電腦會(huì)卡铭污,哈哈恋日,第一個(gè)重點(diǎn)來(lái)了

簡(jiǎn)述渲染進(jìn)程Renderer(重)

頁(yè)面的渲染,JS的執(zhí)行嘹狞,事件的循環(huán)岂膳,都在渲染進(jìn)程內(nèi)執(zhí)行,所以我們要重點(diǎn)了解渲染進(jìn)程

渲染進(jìn)程是多線程的磅网,我們來(lái)看渲染進(jìn)程的一些常用較為主要的線程

渲染進(jìn)程Renderer的主要線程

GUI渲染線程

  • 負(fù)責(zé)渲染瀏覽器界面谈截,解析HTML,CSS涧偷,構(gòu)建DOM樹(shù)和RenderObject樹(shù)簸喂,布局和繪制等
    • 解析html代碼(HTML代碼本質(zhì)是字符串)轉(zhuǎn)化為瀏覽器認(rèn)識(shí)的節(jié)點(diǎn),生成DOM樹(shù)燎潮,也就是DOM Tree
    • 解析css喻鳄,生成CSSOM(CSS規(guī)則樹(shù))
    • 把DOM Tree 和CSSOM結(jié)合,生成Rendering Tree(渲染樹(shù))
  • 當(dāng)我們修改了一些元素的顏色或者背景色跟啤,頁(yè)面就會(huì)重繪(Repaint)
  • 當(dāng)我們修改元素的尺寸诽表,頁(yè)面就會(huì)回流(Reflow)
  • 當(dāng)頁(yè)面需要Repaing和Reflow時(shí)GUI線程執(zhí)行,繪制頁(yè)面
  • 回流(Reflow)比重繪(Repaint)的成本要高隅肥,我們要盡量避免Reflow和Repaint
  • GUI渲染線程與JS引擎線程是互斥的
    • 當(dāng)JS引擎執(zhí)行時(shí)GUI線程會(huì)被掛起(相當(dāng)于被凍結(jié)了)
    • GUI更新會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎空閑時(shí)立即被執(zhí)行

JS引擎線程

  • JS引擎線程就是JS內(nèi)核竿奏,負(fù)責(zé)處理Javascript腳本程序(例如V8引擎)
  • JS引擎線程負(fù)責(zé)解析Javascript腳本,運(yùn)行代碼
  • JS引擎一直等待著任務(wù)隊(duì)列中任務(wù)的到來(lái)腥放,然后加以處理
    • 瀏覽器同時(shí)只能有一個(gè)JS引擎線程在運(yùn)行JS程序泛啸,所以js是單線程運(yùn)行的
    • 一個(gè)Tab頁(yè)(renderer進(jìn)程)中無(wú)論什么時(shí)候都只有一個(gè)JS線程在運(yùn)行JS程序
  • GUI渲染線程與JS引擎線程是互斥的,js引擎線程會(huì)阻塞GUI渲染線程
    • 就是我們常遇到的JS執(zhí)行時(shí)間過(guò)長(zhǎng)秃症,造成頁(yè)面的渲染不連貫候址,導(dǎo)致頁(yè)面渲染加載阻塞(就是加載慢)
    • 例如瀏覽器渲染的時(shí)候遇到<script>標(biāo)簽,就會(huì)停止GUI的渲染种柑,然后js引擎線程開(kāi)始工作岗仑,執(zhí)行里面的js代碼,等js執(zhí)行完畢聚请,js引擎線程停止工作荠雕,GUI繼續(xù)渲染下面的內(nèi)容稳其。所以如果js執(zhí)行時(shí)間太長(zhǎng)就會(huì)造成頁(yè)面卡頓的情況

事件觸發(fā)線程

  • 屬于瀏覽器而不是JS引擎,用來(lái)控制事件循環(huán)炸卑,并且管理著一個(gè)事件隊(duì)列(task queue)
  • 當(dāng)js執(zhí)行碰到事件綁定和一些異步操作(如setTimeOut既鞠,也可來(lái)自瀏覽器內(nèi)核的其他線程,如鼠標(biāo)點(diǎn)擊盖文、AJAX異步請(qǐng)求等)嘱蛋,會(huì)走事件觸發(fā)線程將對(duì)應(yīng)的事件添加到對(duì)應(yīng)的線程中(比如定時(shí)器操作,便把定時(shí)器事件添加到定時(shí)器線程)五续,等異步事件有了結(jié)果洒敏,便把他們的回調(diào)操作添加到事件隊(duì)列,等待js引擎線程空閑時(shí)來(lái)處理返帕。
  • 當(dāng)對(duì)應(yīng)的事件符合觸發(fā)條件被觸發(fā)時(shí)桐玻,該線程會(huì)把事件添加到待處理隊(duì)列的隊(duì)尾,等待JS引擎的處理
  • 因?yàn)镴S是單線程荆萤,所以這些待處理隊(duì)列中的事件都得排隊(duì)等待JS引擎處理

定時(shí)觸發(fā)器線程

  • setIntervalsetTimeout所在線程
  • 瀏覽器定時(shí)計(jì)數(shù)器并不是由JavaScript引擎計(jì)數(shù)的(因?yàn)镴avaScript引擎是單線程的,如果處于阻塞線程狀態(tài)就會(huì)影響記計(jì)時(shí)的準(zhǔn)確)
  • 通過(guò)單獨(dú)線程來(lái)計(jì)時(shí)并觸發(fā)定時(shí)(計(jì)時(shí)完畢后铣卡,添加到事件觸發(fā)線程的事件隊(duì)列中链韭,等待JS引擎空閑后執(zhí)行),這個(gè)線程就是定時(shí)觸發(fā)器線程煮落,也叫定時(shí)器線程
  • W3C在HTML標(biāo)準(zhǔn)中規(guī)定敞峭,規(guī)定要求setTimeout中低于4ms的時(shí)間間隔算為4ms

異步http請(qǐng)求線程

  • 在XMLHttpRequest在連接后是通過(guò)瀏覽器新開(kāi)一個(gè)線程請(qǐng)求
  • 將檢測(cè)到狀態(tài)變更時(shí),如果設(shè)置有回調(diào)函數(shù)蝉仇,異步線程就產(chǎn)生狀態(tài)變更事件旋讹,將這個(gè)回調(diào)再放入事件隊(duì)列中再由JavaScript引擎執(zhí)行
  • 簡(jiǎn)單說(shuō)就是當(dāng)執(zhí)行到一個(gè)http異步請(qǐng)求時(shí),就把異步請(qǐng)求事件添加到異步請(qǐng)求線程轿衔,等收到響應(yīng)(準(zhǔn)確來(lái)說(shuō)應(yīng)該是http狀態(tài)變化)沉迹,再把回調(diào)函數(shù)添加到事件隊(duì)列,等待js引擎線程來(lái)執(zhí)行

了解了上面這些基礎(chǔ)后害驹,接下來(lái)我們開(kāi)始進(jìn)入今天的正題

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

首先要知道鞭呕,JS分為同步任務(wù)和異步任務(wù)

同步任務(wù)都在主線程(這里的主線程就是JS引擎線程)上執(zhí)行,會(huì)形成一個(gè)執(zhí)行棧

主線程之外宛官,事件觸發(fā)線程管理著一個(gè)任務(wù)隊(duì)列葫松,只要異步任務(wù)有了運(yùn)行結(jié)果,就在任務(wù)隊(duì)列之中放一個(gè)事件回調(diào)

一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢(也就是JS引擎線程空閑了)底洗,系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列腋么,將可運(yùn)行的異步任務(wù)(任務(wù)隊(duì)列中的事件回調(diào),只要任務(wù)隊(duì)列中有事件回調(diào)亥揖,就說(shuō)明可以執(zhí)行)添加到執(zhí)行棧中珊擂,開(kāi)始執(zhí)行

我們來(lái)看一段簡(jiǎn)單的代碼

let setTimeoutCallBack = function() {
  console.log('我是定時(shí)器回調(diào)');
};
let httpCallback = function() {
  console.log('我是http請(qǐng)求回調(diào)');
}

// 同步任務(wù)
console.log('我是同步任務(wù)1');

// 異步定時(shí)任務(wù)
setTimeout(setTimeoutCallBack,1000);

// 異步http請(qǐng)求任務(wù)
ajax.get('/info',httpCallback);

// 同步任務(wù)
console.log('我是同步任務(wù)2');

上述代碼執(zhí)行過(guò)程

JS是按照順序從上往下依次執(zhí)行的,可以先理解為這段代碼時(shí)的執(zhí)行環(huán)境就是主線程,也就是也就是當(dāng)前執(zhí)行棧

首先未玻,執(zhí)行console.log('我是同步任務(wù)1')

接著灾而,執(zhí)行到setTimeout時(shí),會(huì)移交給定時(shí)器線程扳剿,通知定時(shí)器線程 1s 后將 setTimeoutCallBack 這個(gè)回調(diào)交給事件觸發(fā)線程處理旁趟,在 1s 后事件觸發(fā)線程會(huì)收到 setTimeoutCallBack 這個(gè)回調(diào)并把它加入到事件觸發(fā)線程所管理的事件隊(duì)列中等待執(zhí)行

接著,執(zhí)行http請(qǐng)求庇绽,會(huì)移交給異步http請(qǐng)求線程發(fā)送網(wǎng)絡(luò)請(qǐng)求锡搜,請(qǐng)求成功后將 httpCallback 這個(gè)回調(diào)交由事件觸發(fā)線程處理,事件觸發(fā)線程收到 httpCallback 這個(gè)回調(diào)后把它加入到事件觸發(fā)線程所管理的事件隊(duì)列中等待執(zhí)行

再接著執(zhí)行console.log('我是同步任務(wù)2')1

至此主線程執(zhí)行棧中執(zhí)行完畢瞧掺,JS引擎線程已經(jīng)空閑耕餐,開(kāi)始向事件觸發(fā)線程發(fā)起詢問(wèn),詢問(wèn)事件觸發(fā)線程的事件隊(duì)列中是否有需要執(zhí)行的回調(diào)函數(shù)辟狈,如果有將事件隊(duì)列中的回調(diào)事件加入執(zhí)行棧中肠缔,開(kāi)始執(zhí)行回調(diào),如果事件隊(duì)列中沒(méi)有回調(diào)哼转,JS引擎線程會(huì)一直發(fā)起詢問(wèn)明未,直到有為止

到了這里我們發(fā)現(xiàn),瀏覽器上的所有線程的工作都很單一且獨(dú)立壹蔓,非常符合單一原則

定時(shí)觸發(fā)線程只管理定時(shí)器且只關(guān)注定時(shí)不關(guān)心結(jié)果趟妥,定時(shí)結(jié)束就把回調(diào)扔給事件觸發(fā)線程

異步http請(qǐng)求線程只管理http請(qǐng)求同樣不關(guān)心結(jié)果,請(qǐng)求結(jié)束把回調(diào)扔給事件觸發(fā)線程

事件觸發(fā)線程只關(guān)心異步回調(diào)入事件隊(duì)列

而我們JS引擎線程只會(huì)執(zhí)行執(zhí)行棧中的事件佣蓉,執(zhí)行棧中的代碼執(zhí)行完畢披摄,就會(huì)讀取事件隊(duì)列中的事件并添加到執(zhí)行棧中繼續(xù)執(zhí)行,這樣反反復(fù)復(fù)就是我們所謂的事件循環(huán)(Event Loop)

圖解

首先勇凭,執(zhí)行棧開(kāi)始順序執(zhí)行

判斷是否為同步疚膊,異步則進(jìn)入異步線程,最終事件回調(diào)給事件觸發(fā)線程的任務(wù)隊(duì)列等待執(zhí)行套像,同步繼續(xù)執(zhí)行

執(zhí)行椖鹆空,詢問(wèn)任務(wù)隊(duì)列中是否有事件回調(diào)

任務(wù)隊(duì)列中有事件回調(diào)則把回調(diào)加入執(zhí)行棧末尾繼續(xù)從第一步開(kāi)始執(zhí)行

任務(wù)隊(duì)列中沒(méi)有事件回調(diào)則不停發(fā)起詢問(wèn)

宏任務(wù)(macrotask) & 微任務(wù)(microtask)

宏任務(wù)(macrotask)

在ECMAScript中夺巩,macrotask也被稱為task

我們可以將每次執(zhí)行棧執(zhí)行的代碼當(dāng)做是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取一個(gè)事件回調(diào)并放到執(zhí)行棧中執(zhí)行)贞让, 每一個(gè)宏任務(wù)會(huì)從頭到尾執(zhí)行完畢,不會(huì)執(zhí)行其他

由于JS引擎線程GUI渲染線程是互斥的關(guān)系柳譬,瀏覽器為了能夠使宏任務(wù)DOM任務(wù)有序的進(jìn)行喳张,會(huì)在一個(gè)宏任務(wù)執(zhí)行結(jié)果后,在下一個(gè)宏任務(wù)執(zhí)行前美澳,GUI渲染線程開(kāi)始工作销部,對(duì)頁(yè)面進(jìn)行渲染

宏任務(wù) -> GUI渲染 -> 宏任務(wù) -> ...

常見(jiàn)的宏任務(wù)

  • 主代碼塊
  • setTimeout
  • setInterval
  • setImmediate ()-Node
  • requestAnimationFrame ()-瀏覽器

微任務(wù)(microtask)

ES6新引入了Promise標(biāo)準(zhǔn)摸航,同時(shí)瀏覽器實(shí)現(xiàn)上多了一個(gè)microtask微任務(wù)概念,在ECMAScript中舅桩,microtask也被稱為jobs

我們已經(jīng)知道宏任務(wù)結(jié)束后酱虎,會(huì)執(zhí)行渲染,然后執(zhí)行下一個(gè)宏任務(wù)擂涛, 而微任務(wù)可以理解成在當(dāng)前宏任務(wù)執(zhí)行后立即執(zhí)行的任務(wù)

當(dāng)一個(gè)宏任務(wù)執(zhí)行完读串,會(huì)在渲染前,將執(zhí)行期間所產(chǎn)生的所有微任務(wù)都執(zhí)行完

宏任務(wù) -> 微任務(wù) -> GUI渲染 -> 宏任務(wù) -> ...

常見(jiàn)微任務(wù)

  • process.nextTick ()-Node
  • Promise.then()
  • catch
  • finally
  • Object.observe
  • MutationObserver

簡(jiǎn)單區(qū)分宏任務(wù)與微任務(wù)

看了上述宏任務(wù)微任務(wù)的解釋你可能還不太清楚撒妈,沒(méi)關(guān)系恢暖,往下看,先記住那些常見(jiàn)的宏微任務(wù)即可

我們通過(guò)幾個(gè)例子來(lái)看狰右,這幾個(gè)例子思路來(lái)自掘金云中君的文章參考鏈接【14】杰捂,通過(guò)渲染背景顏色來(lái)區(qū)分宏任務(wù)和微任務(wù),很直觀棋蚌,我覺(jué)得很有意思嫁佳,所以這里也用這種例子

找一個(gè)空白的頁(yè)面,在console中輸入以下代碼

document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:pink';

我們看到上面動(dòng)圖背景直接渲染了粉紅色附鸽,根據(jù)上文里講瀏覽器會(huì)先執(zhí)行完一個(gè)宏任務(wù)脱拼,再執(zhí)行當(dāng)前執(zhí)行棧的所有微任務(wù),然后移交GUI渲染坷备,上面四行代碼均屬于同一次宏任務(wù),全部執(zhí)行完才會(huì)執(zhí)行渲染情臭,渲染時(shí)GUI線程會(huì)將所有UI改動(dòng)優(yōu)化合并省撑,所以視覺(jué)上,只會(huì)看到頁(yè)面變成粉紅色

再接著看

document.body.style = 'background:blue';
setTimeout(()=>{
    document.body.style = 'background:black'
},200)

上述代碼中俯在,頁(yè)面會(huì)先卡一下藍(lán)色竟秫,再變成黑色背景,頁(yè)面上寫的是200毫秒跷乐,大家可以把它當(dāng)成0毫秒肥败,因?yàn)?毫秒的話由于瀏覽器渲染太快,錄屏不好捕捉愕提,我又沒(méi)啥錄屏慢放的工具馒稍,大家可以自行測(cè)試的,結(jié)果也是一樣浅侨,最安全的方法是寫一個(gè)index.html文件纽谒,在這個(gè)文件中插入上面的js腳本,然后瀏覽器打開(kāi)如输,谷歌下使用控制臺(tái)中performance功能查看一幀一幀的加載最為恰當(dāng)鼓黔,不過(guò)這樣錄屏不好錄所以央勒。。澳化。

回歸正題崔步,之所以會(huì)卡一下藍(lán)色,是因?yàn)橐陨洗a屬于兩次宏任務(wù)缎谷,第一次宏任務(wù)執(zhí)行的代碼是將背景變成藍(lán)色井濒,然后觸發(fā)渲染,將頁(yè)面變成藍(lán)色慎陵,再觸發(fā)第二次宏任務(wù)將背景變成黑色

再來(lái)看

document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
    console.log(2);
    document.body.style = 'background:pink'
});
console.log(3);

控制臺(tái)輸出 1 3 2 , 是因?yàn)?promise 對(duì)象的 then 方法的回調(diào)函數(shù)是異步執(zhí)行眼虱,所以 2 最后輸出

頁(yè)面的背景色直接變成粉色,沒(méi)有經(jīng)過(guò)藍(lán)色的階段席纽,是因?yàn)槟笮覀冊(cè)诤耆蝿?wù)中將背景設(shè)置為藍(lán)色,但在進(jìn)行渲染前執(zhí)行了微任務(wù)润梯, 在微任務(wù)中將背景變成了粉色过牙,然后才執(zhí)行的渲染

微任務(wù)宏任務(wù)注意點(diǎn)

  • 瀏覽器會(huì)先執(zhí)行一個(gè)宏任務(wù),緊接著執(zhí)行當(dāng)前執(zhí)行棧產(chǎn)生的微任務(wù)纺铭,再進(jìn)行渲染寇钉,然后再執(zhí)行下一個(gè)宏任務(wù)

    微任務(wù)和宏任務(wù)不在一個(gè)任務(wù)隊(duì)列,不在一個(gè)任務(wù)隊(duì)列

    • 例如setTimeout是一個(gè)宏任務(wù)舶赔,它的事件回調(diào)在宏任務(wù)隊(duì)列扫倡,Promise.then()是一個(gè)微任務(wù),它的事件回調(diào)在微任務(wù)隊(duì)列竟纳,二者并不是一個(gè)任務(wù)隊(duì)列
    • 以Chrome 為例撵溃,有關(guān)渲染的都是在渲染進(jìn)程中執(zhí)行,渲染進(jìn)程中的任務(wù)(DOM樹(shù)構(gòu)建锥累,js解析…等等)需要主線程執(zhí)行的任務(wù)都會(huì)在主線程中執(zhí)行缘挑,而瀏覽器維護(hù)了一套事件循環(huán)機(jī)制,主線程上的任務(wù)都會(huì)放到消息隊(duì)列中執(zhí)行桶略,主線程會(huì)循環(huán)消息隊(duì)列语淘,并從頭部取出任務(wù)進(jìn)行執(zhí)行,如果執(zhí)行過(guò)程中產(chǎn)生其他任務(wù)需要主線程執(zhí)行的际歼,渲染進(jìn)程中的其他線程會(huì)把該任務(wù)塞入到消息隊(duì)列的尾部惶翻,消息隊(duì)列中的任務(wù)都是宏任務(wù)
    • 微任務(wù)是如何產(chǎn)生的呢?當(dāng)執(zhí)行到script腳本的時(shí)候蹬挺,js引擎會(huì)為全局創(chuàng)建一個(gè)執(zhí)行上下文维贺,在該執(zhí)行上下文中維護(hù)了一個(gè)微任務(wù)隊(duì)列,當(dāng)遇到微任務(wù)巴帮,就會(huì)把微任務(wù)回調(diào)放在微隊(duì)列中溯泣,當(dāng)所有的js代碼執(zhí)行完畢虐秋,在退出全局上下文之前引擎會(huì)去檢查該隊(duì)列,有回調(diào)就執(zhí)行垃沦,沒(méi)有就退出執(zhí)行上下文客给,這也就是為什么微任務(wù)要早于宏任務(wù),也是大家常說(shuō)的肢簿,每個(gè)宏任務(wù)都有一個(gè)微任務(wù)隊(duì)列(由于定時(shí)器是瀏覽器的API靶剑,所以定時(shí)器是宏任務(wù),在js中遇到定時(shí)器會(huì)也是放入到瀏覽器的隊(duì)列中)

此時(shí)池充,你可能還很迷惑桩引,沒(méi)關(guān)系,請(qǐng)接著往下看

圖解宏任務(wù)和微任務(wù)

首先執(zhí)行一個(gè)宏任務(wù)收夸,執(zhí)行結(jié)束后判斷是否存在微任務(wù)

有微任務(wù)先執(zhí)行所有的微任務(wù)坑匠,再渲染,沒(méi)有微任務(wù)則直接渲染

然后再接著執(zhí)行下一個(gè)宏任務(wù)

圖解完整的Event Loop

首先卧惜,整體的script(作為第一個(gè)宏任務(wù))開(kāi)始執(zhí)行的時(shí)候厘灼,會(huì)把所有代碼分為同步任務(wù)異步任務(wù)兩部分

同步任務(wù)會(huì)直接進(jìn)入主線程依次執(zhí)行

異步任務(wù)會(huì)再分為宏任務(wù)和微任務(wù)

宏任務(wù)進(jìn)入到Event Table中咽瓷,并在里面注冊(cè)回調(diào)函數(shù)设凹,每當(dāng)指定的事件完成時(shí),Event Table會(huì)將這個(gè)函數(shù)移到Event Queue中

微任務(wù)也會(huì)進(jìn)入到另一個(gè)Event Table中茅姜,并在里面注冊(cè)回調(diào)函數(shù)闪朱,每當(dāng)指定的事件完成時(shí),Event Table會(huì)將這個(gè)函數(shù)移到Event Queue中

當(dāng)主線程內(nèi)的任務(wù)執(zhí)行完畢钻洒,主線程為空時(shí)监透,會(huì)檢查微任務(wù)的Event Queue,如果有任務(wù)航唆,就全部執(zhí)行,如果沒(méi)有就執(zhí)行下一個(gè)宏任務(wù)

上述過(guò)程會(huì)不斷重復(fù)院刁,這就是Event Loop糯钙,比較完整的事件循環(huán)

關(guān)于Promise

new Promise(() => {}).then() ,我們來(lái)看這樣一個(gè)Promise代碼

前面的 new Promise() 這一部分是一個(gè)構(gòu)造函數(shù)退腥,這是一個(gè)同步任務(wù)

后面的 .then() 才是一個(gè)異步微任務(wù)任岸,這一點(diǎn)是非常重要的

new Promise((resolve) => {
    console.log(1)
  resolve()
}).then(()=>{
    console.log(2)
})
console.log(3)

上面代碼輸出1 3 2

關(guān)于 async/await 函數(shù)

async/await本質(zhì)上還是基于Promise的一些封裝,而Promise是屬于微任務(wù)的一種

所以在使用await關(guān)鍵字與Promise.then效果類似

setTimeout(() => console.log(4))

async function test() {
  console.log(1)
  await Promise.resolve()
  console.log(3)
}

test()

console.log(2)

上述代碼輸出1 2 3 4

可以理解為狡刘,await 以前的代碼享潜,相當(dāng)于與 new Promise 的同步代碼,await 以后的代碼相當(dāng)于 Promise.then的異步

舉栗印證

首先給大家來(lái)一個(gè)比較直觀的動(dòng)圖

之所以放這個(gè)動(dòng)圖嗅蔬,就是為了向大家推薦這篇好文剑按,動(dòng)圖錄屏自參考鏈接【1】

極力推薦大家看看這篇帖子疾就,非常nice,分步動(dòng)畫生動(dòng)且直觀艺蝴,有時(shí)間的話可以自己去體驗(yàn)下

不過(guò)在看這個(gè)帖子之前你要先了解下運(yùn)行機(jī)制會(huì)更好讀懂些

接下來(lái)這個(gè)來(lái)自網(wǎng)上隨意找的一個(gè)比較簡(jiǎn)單的面試題猬腰,求輸出結(jié)果

function test() {
  console.log(1)
  setTimeout(function () {  // timer1
    console.log(2)
  }, 1000)
}

test();

setTimeout(function () {        // timer2
  console.log(3)
})

new Promise(function (resolve) {
  console.log(4)
  setTimeout(function () {  // timer3
    console.log(5)
  }, 100)
  resolve()
}).then(function () {
  setTimeout(function () {  // timer4
    console.log(6)
  }, 0)
  console.log(7)
})

console.log(8)

結(jié)合我們上述的JS運(yùn)行機(jī)制再來(lái)看這道題就簡(jiǎn)單明了的多了

JS是順序從上而下執(zhí)行

執(zhí)行到test(),test方法為同步猜敢,直接執(zhí)行姑荷,console.log(1)打印1

test方法中setTimeout為異步宏任務(wù),回調(diào)我們把它記做timer1放入宏任務(wù)隊(duì)列

接著執(zhí)行缩擂,test方法下面有一個(gè)setTimeout為異步宏任務(wù)鼠冕,回調(diào)我們把它記做timer2放入宏任務(wù)隊(duì)列

接著執(zhí)行promise,new Promise是同步任務(wù)胯盯,直接執(zhí)行懈费,打印4

new Promise里面的setTimeout是異步宏任務(wù),回調(diào)我們記做timer3放到宏任務(wù)隊(duì)列

Promise.then是微任務(wù)陨闹,放到微任務(wù)隊(duì)列

console.log(8)是同步任務(wù)楞捂,直接執(zhí)行,打印8

主線程任務(wù)執(zhí)行完畢趋厉,檢查微任務(wù)隊(duì)列中有Promise.then

開(kāi)始執(zhí)行微任務(wù)寨闹,發(fā)現(xiàn)有setTimeout是異步宏任務(wù),記做timer4放到宏任務(wù)隊(duì)列

微任務(wù)隊(duì)列中的console.log(7)是同步任務(wù)君账,直接執(zhí)行繁堡,打印7

微任務(wù)執(zhí)行完畢,第一次循環(huán)結(jié)束

檢查宏任務(wù)隊(duì)列乡数,里面有timer1椭蹄、timer2、timer3净赴、timer4绳矩,四個(gè)定時(shí)器宏任務(wù),按照定時(shí)器延遲時(shí)間得到可以執(zhí)行的順序玖翅,即Event Queue:timer2翼馆、timer4、timer3金度、timer1应媚,依次拿出放入執(zhí)行棧末尾執(zhí)行(插播一條:瀏覽器 event loop 的 Macrotask queue,就是宏任務(wù)隊(duì)列在每次循環(huán)中只會(huì)讀取一個(gè)任務(wù))

執(zhí)行timer2猜极,console.log(3)為同步任務(wù)中姜,直接執(zhí)行,打印3

檢查沒(méi)有微任務(wù)跟伏,第二次Event Loop結(jié)束

執(zhí)行timer4丢胚,console.log(6)為同步任務(wù)翩瓜,直接執(zhí)行,打印6

檢查沒(méi)有微任務(wù)嗜桌,第三次Event Loop結(jié)束

執(zhí)行timer3奥溺,console.log(5)同步任務(wù),直接執(zhí)行骨宠,打印5

檢查沒(méi)有微任務(wù)浮定,第四次Event Loop結(jié)束

執(zhí)行timer1,console.log(2)同步任務(wù)层亿,直接執(zhí)行桦卒,打印2

檢查沒(méi)有微任務(wù),也沒(méi)有宏任務(wù)匿又,第五次Event Loop結(jié)束

結(jié)果:1方灾,4,8碌更,7裕偿,3,6痛单,5嘿棘,2

提一下NodeJS中的運(yùn)行機(jī)制

上面的一切都是針對(duì)于瀏覽器的EventLoop

雖然NodeJS中的JavaScript運(yùn)行環(huán)境也是V8,也是單線程旭绒,但是鸟妙,還是有一些與瀏覽器中的表現(xiàn)是不一樣的

其實(shí)nodejs與瀏覽器的區(qū)別,就是nodejs的宏任務(wù)分好幾種類型挥吵,而這好幾種又有不同的任務(wù)隊(duì)列重父,而不同的任務(wù)隊(duì)列又有順序區(qū)別,而微任務(wù)是穿插在每一種宏任務(wù)之間的

在node環(huán)境下忽匈,process.nextTick的優(yōu)先級(jí)高于Promise房午,可以簡(jiǎn)單理解為在宏任務(wù)結(jié)束后會(huì)先執(zhí)行微任務(wù)隊(duì)列中的nextTickQueue部分,然后才會(huì)執(zhí)行微任務(wù)中的Promise部分

上圖來(lái)自NodeJS官網(wǎng)

如上圖所示丹允,nodejs的宏任務(wù)分好幾種類型歪沃,我們只簡(jiǎn)單介紹大體內(nèi)容了解,不詳細(xì)解釋嫌松,不然又是啰哩啰嗦一大篇

NodeJS的Event Loop相對(duì)比較麻煩

Node會(huì)先執(zhí)行所有類型為 timers 的 MacroTask,然后執(zhí)行所有的 MicroTask(NextTick例外)

進(jìn)入 poll 階段奕污,執(zhí)行幾乎所有 MacroTask萎羔,然后執(zhí)行所有的 MicroTask

再執(zhí)行所有類型為 check 的 MacroTask,然后執(zhí)行所有的 MicroTask

再執(zhí)行所有類型為 close callbacks 的 MacroTask碳默,然后執(zhí)行所有的 MicroTask

至此贾陷,完成一個(gè) Tick缘眶,回到 timers 階段

……

如此反復(fù),無(wú)窮無(wú)盡……

反觀瀏覽器中Event Loop就比較容易理解

先執(zhí)行一個(gè) MacroTask髓废,然后執(zhí)行所有的 MicroTask

再執(zhí)行一個(gè) MacroTask巷懈,然后執(zhí)行所有的 MicroTask

……

如此反復(fù),無(wú)窮無(wú)盡……

好了慌洪,關(guān)于Node中各個(gè)類型階段的解析顶燕,這里就不過(guò)多說(shuō)明了,自己查閱資料吧冈爹,這里就是簡(jiǎn)單提一下涌攻,NodeJS的Event Loop解釋起來(lái)比瀏覽器這繁雜,這里就只做個(gè)對(duì)比

最后

上面的流程圖都是自己畫的频伤,所以有點(diǎn)low恳谎,見(jiàn)諒

水平有限,歡迎指錯(cuò)

碼字不易憋肖,看完對(duì)你有幫助請(qǐng)點(diǎn)贊因痛,有疑問(wèn)請(qǐng)?jiān)u論提出

重新搞了下公眾號(hào)

歡迎大家關(guān)注【不正經(jīng)的前端】,加我岸更,加群鸵膏,或者拿一些資料都可以的,時(shí)不時(shí)發(fā)一些優(yōu)質(zhì)原創(chuàng)

本文首發(fā)于掘金坐慰,原創(chuàng)较性,轉(zhuǎn)載請(qǐng)注明出處

作者:isboyjc

郵箱:214930661@qq.com

GitHub: Github.com/isboyjc

掘金原文:https://juejin.im/post/5e22b391f265da3e204d8c14

參考

  1. Tasks, microtasks, queues and schedules - 重點(diǎn)推薦閱讀

  2. 聊聊 JavaScript 與瀏覽器的那些事 - 引擎與線程

  3. 前端文摘:深入解析瀏覽器的幕后工作原理

  4. 瀏覽器進(jìn)程?線程结胀?傻傻分不清楚赞咙!

  5. 從輸入cnblogs.com到博客園首頁(yè)完全展示發(fā)生了什么

  6. 前端必讀:瀏覽器內(nèi)部工作原理

  7. 什么是 Event Loop?

  8. JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop

  9. 單線程與多線程的區(qū)別

  10. 瀏覽器進(jìn)程/線程模型及JS運(yùn)行機(jī)制

  11. 瀏覽器的運(yùn)行機(jī)制—2.瀏覽器都包含哪些進(jìn)程糟港?

  12. JS 一定要放在 Body 的最底部么攀操?聊聊瀏覽器的渲染機(jī)制

  13. 從瀏覽器多進(jìn)程到JS單線程,JS運(yùn)行機(jī)制最全面的一次梳理

  14. 「前端進(jìn)階」從多線程到Event Loop全面梳理

  15. Js基礎(chǔ)知識(shí)(四) - js運(yùn)行原理與機(jī)制

  16. 這一次秸抚,徹底弄懂 JavaScript 執(zhí)行機(jī)制

  17. 前端性能優(yōu)化:細(xì)說(shuō)瀏覽器渲染的重排與重繪

  18. 10分鐘看懂瀏覽器的渲染過(guò)程及優(yōu)化

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末速和,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子剥汤,更是在濱河造成了極大的恐慌颠放,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吭敢,死亡現(xiàn)場(chǎng)離奇詭異碰凶,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門欲低,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)辕宏,“玉大人,你說(shuō)我怎么就攤上這事砾莱∪鹂穑” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵腊瑟,是天一觀的道長(zhǎng)聚假。 經(jīng)常有香客問(wèn)我,道長(zhǎng)扫步,這世上最難降的妖魔是什么魔策? 我笑而不...
    開(kāi)封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮河胎,結(jié)果婚禮上闯袒,老公的妹妹穿的比我還像新娘。我一直安慰自己游岳,他們只是感情好政敢,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著胚迫,像睡著了一般喷户。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上访锻,一...
    開(kāi)封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天褪尝,我揣著相機(jī)與錄音,去河邊找鬼期犬。 笑死河哑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的龟虎。 我是一名探鬼主播璃谨,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鲤妥!你這毒婦竟也來(lái)了佳吞?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤棉安,失蹤者是張志新(化名)和其女友劉穎底扳,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體贡耽,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡花盐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年羡滑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片算芯。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖凳宙,靈堂內(nèi)的尸體忽然破棺而出熙揍,到底是詐尸還是另有隱情,我是刑警寧澤氏涩,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布届囚,位于F島的核電站,受9級(jí)特大地震影響是尖,放射性物質(zhì)發(fā)生泄漏意系。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一饺汹、第九天 我趴在偏房一處隱蔽的房頂上張望蛔添。 院中可真熱鬧,春花似錦兜辞、人聲如沸迎瞧。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)凶硅。三九已至,卻和暖如春扫皱,著一層夾襖步出監(jiān)牢的瞬間足绅,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工韩脑, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留氢妈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓扰才,卻偏偏與公主長(zhǎng)得像允懂,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子衩匣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

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