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

----------正文開始----------

最近發(fā)現(xiàn)有不少介紹JS單線程運(yùn)行機(jī)制的文章,但是發(fā)現(xiàn)很多都僅僅是介紹某一部分的知識(shí)赋朦,而且各個(gè)地方的說法還不統(tǒng)一,容易造成困惑缰盏。

因此準(zhǔn)備梳理這塊知識(shí)點(diǎn),結(jié)合已有的認(rèn)知,基于網(wǎng)上的大量參考資料娇唯,

從瀏覽器多進(jìn)程到JS單線程漱凝,將JS引擎的運(yùn)行機(jī)制系統(tǒng)的梳理一遍感论。

展現(xiàn)形式:由于是屬于系統(tǒng)梳理型润努,就沒有由淺入深了倚聚,而是從頭到尾的梳理知識(shí)體系砌些,

重點(diǎn)是將關(guān)鍵節(jié)點(diǎn)的知識(shí)點(diǎn)串聯(lián)起來纵东,而不是僅僅剖析某一部分知識(shí)。

內(nèi)容是:從瀏覽器進(jìn)程啥寇,再到瀏覽器內(nèi)核運(yùn)行偎球,再到JS引擎單線程,再到JS事件循環(huán)機(jī)制辑甜,從頭到尾系統(tǒng)的梳理一遍衰絮,擺脫碎片化,形成一個(gè)知識(shí)體系

目標(biāo)是:看完這篇文章后磷醋,對(duì)瀏覽器多進(jìn)程猫牡,JS單線程,JS事件循環(huán)機(jī)制這些都能有一定理解邓线,

有一個(gè)知識(shí)體系骨架淌友,而不是似懂非懂的感覺。

另外骇陈,本文適合有一定經(jīng)驗(yàn)的前端人員震庭,新手請(qǐng)規(guī)避,避免受到過多的概念沖擊你雌∑髁可以先存起來,有了一定理解后再看婿崭,也可以分成多批次觀看拨拓,避免過度疲勞。

大綱

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

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

瀏覽器都包含哪些進(jìn)程逛球?

瀏覽器多進(jìn)程的優(yōu)勢(shì)

重點(diǎn)是瀏覽器內(nèi)核(渲染進(jìn)程)

Browser進(jìn)程和瀏覽器內(nèi)核(Renderer進(jìn)程)的通信過程

梳理瀏覽器內(nèi)核中線程之間的關(guān)系

GUI渲染線程與JS引擎線程互斥

JS阻塞頁面加載

WebWorker千元,JS的多線程?

WebWorker與SharedWorker

簡(jiǎn)單梳理下瀏覽器渲染流程

load事件與DOMContentLoaded事件的先后

css加載是否會(huì)阻塞dom樹渲染颤绕?

普通圖層和復(fù)合圖層

從Event Loop談JS的運(yùn)行機(jī)制

事件循環(huán)機(jī)制進(jìn)一步補(bǔ)充

單獨(dú)說說定時(shí)器

setTimeout而不是setInterval

事件循環(huán)進(jìn)階:macrotask與microtask

寫在最后的話

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

線程和進(jìn)程區(qū)分不清幸海,是很多新手都會(huì)犯的錯(cuò)誤,沒有關(guān)系奥务。這很正常物独。先看看下面這個(gè)形象的比喻:

- 進(jìn)程是一個(gè)工廠,工廠有它的獨(dú)立資源- 工廠之間相互獨(dú)立- 線程是工廠中的工人氯葬,多個(gè)工人協(xié)作完成任務(wù)- 工廠內(nèi)有一個(gè)或多個(gè)工人- 工人之間共享空間復(fù)制代碼

再完善完善概念:

- 工廠的資源 -> 系統(tǒng)分配的內(nèi)存(獨(dú)立的一塊內(nèi)存)- 工廠之間的相互獨(dú)立 -> 進(jìn)程之間相互獨(dú)立- 多個(gè)工人協(xié)作完成任務(wù) -> 多個(gè)線程在進(jìn)程中協(xié)作完成任務(wù)- 工廠內(nèi)有一個(gè)或多個(gè)工人 -> 一個(gè)進(jìn)程由一個(gè)或多個(gè)線程組成- 工人之間共享空間 -> 同一進(jìn)程下的各個(gè)線程之間共享程序的內(nèi)存空間(包括代碼段容诬、數(shù)據(jù)集、堆等)復(fù)制代碼

然后再鞏固下:

如果是windows電腦中蛤铜,可以打開任務(wù)管理器,可以看到有一個(gè)后臺(tái)進(jìn)程列表秽澳。對(duì),那里就是查看進(jìn)程的地方戏羽,而且可以看到每個(gè)進(jìn)程的內(nèi)存資源信息以及cpu占有率担神。


所以,應(yīng)該更容易理解了:進(jìn)程是cpu資源分配的最小單位(系統(tǒng)會(huì)給它分配內(nèi)存)

最后始花,再用較為官方的術(shù)語描述一遍:

進(jìn)程是cpu資源分配的最小單位(是能擁有資源和獨(dú)立運(yùn)行的最小單位)

線程是cpu調(diào)度的最小單位(線程是建立在進(jìn)程的基礎(chǔ)上的一次程序運(yùn)行單位妄讯,一個(gè)進(jìn)程中可以有多個(gè)線程)

tips

不同進(jìn)程之間也可以通信,不過代價(jià)較大

現(xiàn)在酷宵,一般通用的叫法:單線程與多線程亥贸,都是指在一個(gè)進(jìn)程內(nèi)的單和多。(所以核心還是得屬于一個(gè)進(jìn)程才行)

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

理解了進(jìn)程與線程了區(qū)別后浇垦,接下來對(duì)瀏覽器進(jìn)行一定程度上的認(rèn)識(shí):(先看下簡(jiǎn)化理解)

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

瀏覽器之所以能夠運(yùn)行炕置,是因?yàn)橄到y(tǒng)給它的進(jìn)程分配了資源(cpu、內(nèi)存)

簡(jiǎn)單點(diǎn)理解溜族,每打開一個(gè)Tab頁讹俊,就相當(dāng)于創(chuàng)建了一個(gè)獨(dú)立的瀏覽器進(jìn)程。

關(guān)于以上幾點(diǎn)的驗(yàn)證煌抒,請(qǐng)?jiān)俚谝粡垐D


圖中打開了Chrome瀏覽器的多個(gè)標(biāo)簽頁仍劈,然后可以在Chrome的任務(wù)管理器中看到有多個(gè)進(jìn)程(分別是每一個(gè)Tab頁面有一個(gè)獨(dú)立的進(jìn)程,以及一個(gè)主進(jìn)程)寡壮。感興趣的可以自行嘗試下贩疙,如果再多打開一個(gè)Tab頁,進(jìn)程正常會(huì)+1以上

**注意:**在這里瀏覽器應(yīng)該也有自己的優(yōu)化機(jī)制况既,有時(shí)候打開多個(gè)tab頁后这溅,可以在Chrome任務(wù)管理器中看到,有些進(jìn)程被合并了

(所以每一個(gè)Tab標(biāo)簽對(duì)應(yīng)一個(gè)進(jìn)程并不一定是絕對(duì)的)

瀏覽器都包含哪些進(jìn)程棒仍?

知道了瀏覽器是多進(jìn)程后悲靴,再來看看它到底包含哪些進(jìn)程:(為了簡(jiǎn)化理解,僅列舉主要進(jìn)程)

1.Browser進(jìn)程:瀏覽器的主進(jìn)程(負(fù)責(zé)協(xié)調(diào)莫其、主控)癞尚,只有一個(gè)。作用有

負(fù)責(zé)瀏覽器界面顯示乱陡,與用戶交互浇揩。如前進(jìn),后退等

負(fù)責(zé)各個(gè)頁面的管理憨颠,創(chuàng)建和銷毀其他進(jìn)程

將Renderer進(jìn)程得到的內(nèi)存中的Bitmap胳徽,繪制到用戶界面上

網(wǎng)絡(luò)資源的管理积锅,下載等

2.第三方插件進(jìn)程:每種類型的插件對(duì)應(yīng)一個(gè)進(jìn)程,僅當(dāng)使用該插件時(shí)才創(chuàng)建

3.GPU進(jìn)程:最多一個(gè)养盗,用于3D繪制等

4.瀏覽器渲染進(jìn)程(瀏覽器內(nèi)核)(Renderer進(jìn)程缚陷,內(nèi)部是多線程的):默認(rèn)每個(gè)Tab頁面一個(gè)進(jìn)程,互不影響爪瓜。主要作用為

頁面渲染蹬跃,腳本執(zhí)行,事件處理等

強(qiáng)化記憶:在瀏覽器中打開一個(gè)網(wǎng)頁相當(dāng)于新起了一個(gè)進(jìn)程(進(jìn)程內(nèi)有自己的多線程)

當(dāng)然铆铆,瀏覽器有時(shí)會(huì)將多個(gè)進(jìn)程合并(譬如打開多個(gè)空白標(biāo)簽頁后,會(huì)發(fā)現(xiàn)多個(gè)空白標(biāo)簽頁被合并成了一個(gè)進(jìn)程)丹喻,如圖


另外薄货,可以通過Chrome的更多工具 -> 任務(wù)管理器自行驗(yàn)證

瀏覽器多進(jìn)程的優(yōu)勢(shì)

相比于單進(jìn)程瀏覽器,多進(jìn)程有如下優(yōu)點(diǎn):

避免單個(gè)page crash影響整個(gè)瀏覽器

避免第三方插件crash影響整個(gè)瀏覽器

多進(jìn)程充分利用多核優(yōu)勢(shì)

方便使用沙盒模型隔離插件等進(jìn)程碍论,提高瀏覽器穩(wěn)定性

簡(jiǎn)單點(diǎn)理解:如果瀏覽器是單進(jìn)程谅猾,那么某個(gè)Tab頁崩潰了,就影響了整個(gè)瀏覽器鳍悠,體驗(yàn)有多差税娜;同理如果是單進(jìn)程,插件崩潰了也會(huì)影響整個(gè)瀏覽器藏研;而且多進(jìn)程還有其它的諸多優(yōu)勢(shì)敬矩。。蠢挡。

當(dāng)然弧岳,內(nèi)存等資源消耗也會(huì)更大,有點(diǎn)空間換時(shí)間的意思业踏。

重點(diǎn)是瀏覽器內(nèi)核(渲染進(jìn)程)

重點(diǎn)來了禽炬,我們可以看到,上面提到了這么多的進(jìn)程勤家,那么腹尖,對(duì)于普通的前端操作來說,最終要的是什么呢伐脖?答案是渲染進(jìn)程

可以這樣理解热幔,頁面的渲染,JS的執(zhí)行晓殊,事件的循環(huán)断凶,都在這個(gè)進(jìn)程內(nèi)進(jìn)行。接下來重點(diǎn)分析這個(gè)進(jìn)程

請(qǐng)牢記巫俺,瀏覽器的渲染進(jìn)程是多線程的(這點(diǎn)如果不理解认烁,請(qǐng)回頭看進(jìn)程和線程的區(qū)分

終于到了線程這個(gè)概念了??,好親切。那么接下來看看它都包含了哪些線程(列舉一些主要常駐線程):

1.GUI渲染線程

負(fù)責(zé)渲染瀏覽器界面却嗡,解析HTML舶沛,CSS,構(gòu)建DOM樹和RenderObject樹窗价,布局和繪制等如庭。

當(dāng)界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時(shí),該線程就會(huì)執(zhí)行

注意撼港,GUI渲染線程與JS引擎線程是互斥的坪它,當(dāng)JS引擎執(zhí)行時(shí)GUI線程會(huì)被掛起(相當(dāng)于被凍結(jié)了),GUI更新會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎空閑時(shí)立即被執(zhí)行帝牡。

2.JS引擎線程

也稱為JS內(nèi)核往毡,負(fù)責(zé)處理Javascript腳本程序。(例如V8引擎)

JS引擎線程負(fù)責(zé)解析Javascript腳本靶溜,運(yùn)行代碼开瞭。

JS引擎一直等待著任務(wù)隊(duì)列中任務(wù)的到來,然后加以處理罩息,一個(gè)Tab頁(renderer進(jìn)程)中無論什么時(shí)候都只有一個(gè)JS線程在運(yùn)行JS程序

同樣注意嗤详,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執(zhí)行的時(shí)間過長(zhǎng)瓷炮,這樣就會(huì)造成頁面的渲染不連貫葱色,導(dǎo)致頁面渲染加載阻塞。

3.事件觸發(fā)線程

歸屬于瀏覽器而不是JS引擎崭别,用來控制事件循環(huán)(可以理解冬筒,JS引擎自己都忙不過來,需要瀏覽器另開線程協(xié)助)

當(dāng)JS引擎執(zhí)行代碼塊如setTimeOut時(shí)(也可來自瀏覽器內(nèi)核的其他線程,如鼠標(biāo)點(diǎn)擊茅主、AJAX異步請(qǐng)求等)舞痰,會(huì)將對(duì)應(yīng)任務(wù)添加到事件線程中

當(dāng)對(duì)應(yīng)的事件符合觸發(fā)條件被觸發(fā)時(shí),該線程會(huì)把事件添加到待處理隊(duì)列的隊(duì)尾诀姚,等待JS引擎的處理

注意响牛,由于JS的單線程關(guān)系,所以這些待處理隊(duì)列中的事件都得排隊(duì)等待JS引擎處理(當(dāng)JS引擎空閑時(shí)才會(huì)去執(zhí)行)

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

傳說中的setInterval與setTimeout所在線程

瀏覽器定時(shí)計(jì)數(shù)器并不是由JavaScript引擎計(jì)數(shù)的,(因?yàn)镴avaScript引擎是單線程的, 如果處于阻塞線程狀態(tài)就會(huì)影響記計(jì)時(shí)的準(zhǔn)確)

因此通過單獨(dú)線程來計(jì)時(shí)并觸發(fā)定時(shí)(計(jì)時(shí)完畢后赫段,添加到事件隊(duì)列中呀打,等待JS引擎空閑后執(zhí)行)

注意,W3C在HTML標(biāo)準(zhǔn)中規(guī)定糯笙,規(guī)定要求setTimeout中低于4ms的時(shí)間間隔算為4ms贬丛。

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

在XMLHttpRequest在連接后是通過瀏覽器新開一個(gè)線程請(qǐng)求

將檢測(cè)到狀態(tài)變更時(shí),如果設(shè)置有回調(diào)函數(shù)给涕,異步線程就產(chǎn)生狀態(tài)變更事件豺憔,將這個(gè)回調(diào)再放入事件隊(duì)列中额获。再由JavaScript引擎執(zhí)行。

看到這里恭应,如果覺得累了抄邀,可以先休息下,這些概念需要被消化昼榛,畢竟后續(xù)將提到的事件循環(huán)機(jī)制就是基于事件觸發(fā)線程的境肾,所以如果僅僅是看某個(gè)碎片化知識(shí),可能會(huì)有一種似懂非懂的感覺胆屿。要完成的梳理一遍才能快速沉淀奥喻,不易遺忘。放張圖鞏固下吧:


再說一點(diǎn)非迹,為什么JS引擎是單線程的衫嵌?額,這個(gè)問題其實(shí)應(yīng)該沒有標(biāo)準(zhǔn)答案彻秆,譬如,可能僅僅是因?yàn)橛捎诙嗑€程的復(fù)雜性结闸,譬如多線程操作一般要加鎖唇兑,因此最初設(shè)計(jì)時(shí)選擇了單線程。桦锄。扎附。

Browser進(jìn)程和瀏覽器內(nèi)核(Renderer進(jìn)程)的通信過程

看到這里,首先结耀,應(yīng)該對(duì)瀏覽器內(nèi)的進(jìn)程和線程都有一定理解了留夜,那么接下來,再談?wù)劄g覽器的Browser進(jìn)程(控制進(jìn)程)是如何和內(nèi)核通信的图甜,

這點(diǎn)也理解后碍粥,就可以將這部分的知識(shí)串聯(lián)起來,從頭到尾有一個(gè)完整的概念黑毅。

如果自己打開任務(wù)管理器嚼摩,然后打開一個(gè)瀏覽器,就可以看到:任務(wù)管理器中出現(xiàn)了兩個(gè)進(jìn)程(一個(gè)是主控進(jìn)程矿瘦,一個(gè)則是打開Tab頁的渲染進(jìn)程)枕面,然后在這前提下,看下整個(gè)的過程:(簡(jiǎn)化了很多)

Browser進(jìn)程收到用戶請(qǐng)求缚去,首先需要獲取頁面內(nèi)容(譬如通過網(wǎng)絡(luò)下載資源)潮秘,隨后將該任務(wù)通過RendererHost接口傳遞給Render進(jìn)程

Renderer進(jìn)程的Renderer接口收到消息,簡(jiǎn)單解釋后易结,交給渲染線程枕荞,然后開始渲染

渲染線程接收請(qǐng)求柜候,加載網(wǎng)頁并渲染網(wǎng)頁,這其中可能需要Browser進(jìn)程獲取資源和需要GPU進(jìn)程來幫助渲染

當(dāng)然可能會(huì)有JS線程操作DOM(這樣可能會(huì)造成回流并重繪)

最后Render進(jìn)程將結(jié)果傳遞給Browser進(jìn)程

Browser進(jìn)程接收到結(jié)果并將結(jié)果繪制出來

這里繪一張簡(jiǎn)單的圖:(很簡(jiǎn)化)


看完這一整套流程买猖,應(yīng)該對(duì)瀏覽器的運(yùn)作有了一定理解了改橘,這樣有了知識(shí)架構(gòu)的基礎(chǔ)后,后續(xù)就方便往上填充內(nèi)容玉控。

這塊再往深處講的話就涉及到瀏覽器內(nèi)核源碼解析了飞主,不屬于本文范圍。

如果這一塊要深挖高诺,建議去讀一些瀏覽器內(nèi)核源碼解析文章碌识,或者可以先看看參考下來源中的第一篇文章,寫的不錯(cuò)

梳理瀏覽器內(nèi)核中線程之間的關(guān)系

到了這里虱而,已經(jīng)對(duì)瀏覽器的運(yùn)行有了一個(gè)整體的概念筏餐,接下來,先簡(jiǎn)單梳理一些概念

GUI渲染線程與JS引擎線程互斥

由于JavaScript是可操縱DOM的牡拇,如果在修改這些元素屬性同時(shí)渲染界面(即JS線程和UI線程同時(shí)運(yùn)行)魁瞪,那么渲染線程前后獲得的元素?cái)?shù)據(jù)就可能不一致了。

因此為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果惠呼,瀏覽器設(shè)置GUI渲染線程與JS引擎為互斥的關(guān)系导俘,當(dāng)JS引擎執(zhí)行時(shí)GUI線程會(huì)被掛起,

GUI更新則會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎線程空閑時(shí)立即被執(zhí)行剔蹋。

JS阻塞頁面加載

從上述的互斥關(guān)系旅薄,可以推導(dǎo)出,JS如果執(zhí)行時(shí)間過長(zhǎng)就會(huì)阻塞頁面泣崩。

譬如少梁,假設(shè)JS引擎正在進(jìn)行巨量的計(jì)算,此時(shí)就算GUI有更新矫付,也會(huì)被保存到隊(duì)列中凯沪,等待JS引擎空閑后執(zhí)行。

然后技即,由于巨量計(jì)算著洼,所以JS引擎很可能很久很久后才能空閑,自然會(huì)感覺到巨卡無比而叼。

所以身笤,要盡量避免JS執(zhí)行時(shí)間過長(zhǎng),這樣就會(huì)造成頁面的渲染不連貫葵陵,導(dǎo)致頁面渲染加載阻塞的感覺液荸。

WebWorker,JS的多線程脱篙?

前文中有提到JS引擎是單線程的娇钱,而且JS執(zhí)行時(shí)間過長(zhǎng)會(huì)阻塞頁面伤柄,那么JS就真的對(duì)cpu密集型計(jì)算無能為力么?

所以文搂,后來HTML5中支持了Web Worker适刀。

MDN的官方解釋是:

Web Worker為Web內(nèi)容在后臺(tái)線程中運(yùn)行腳本提供了一種簡(jiǎn)單的方法。線程可以執(zhí)行任務(wù)而不干擾用戶界面一個(gè)worker是使用一個(gè)構(gòu)造函數(shù)創(chuàng)建的一個(gè)對(duì)象(e.g. Worker()) 運(yùn)行一個(gè)命名的JavaScript文件 這個(gè)文件包含將在工作線程中運(yùn)行的代碼; workers 運(yùn)行在另一個(gè)全局上下文中,不同于當(dāng)前的window因此煤蹭,使用window快捷方式獲取當(dāng)前全局的范圍 (而不是self) 在一個(gè) Worker 內(nèi)將返回錯(cuò)誤復(fù)制代碼

這樣理解下:

創(chuàng)建Worker時(shí)笔喉,JS引擎向?yàn)g覽器申請(qǐng)開一個(gè)子線程(子線程是瀏覽器開的,完全受主線程控制硝皂,而且不能操作DOM)

JS引擎線程與worker線程間通過特定的方式通信(postMessage API常挚,需要通過序列化對(duì)象來與線程交互特定的數(shù)據(jù))

所以,如果有非常耗時(shí)的工作稽物,請(qǐng)單獨(dú)開一個(gè)Worker線程奄毡,這樣里面不管如何翻天覆地都不會(huì)影響JS引擎主線程,

只待計(jì)算出結(jié)果后贝或,將結(jié)果通信給主線程即可吼过,perfect!

而且注意下,JS引擎是單線程的咪奖,這一點(diǎn)的本質(zhì)仍然未改變那先,Worker可以理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計(jì)算問題赡艰。

其它,關(guān)于Worker的詳解就不是本文的范疇了斤葱,因此不再贅述慷垮。

WebWorker與SharedWorker

既然都到了這里,就再提一下SharedWorker(避免后續(xù)將這兩個(gè)概念搞混)

WebWorker只屬于某個(gè)頁面揍堕,不會(huì)和其他頁面的Render進(jìn)程(瀏覽器內(nèi)核進(jìn)程)共享

所以Chrome在Render進(jìn)程中(每一個(gè)Tab頁就是一個(gè)render進(jìn)程)創(chuàng)建一個(gè)新的線程來運(yùn)行Worker中的JavaScript程序料身。

SharedWorker是瀏覽器所有頁面共享的,不能采用與Worker同樣的方式實(shí)現(xiàn)衩茸,因?yàn)樗浑`屬于某個(gè)Render進(jìn)程芹血,可以為多個(gè)Render進(jìn)程共享使用

所以Chrome瀏覽器為SharedWorker單獨(dú)創(chuàng)建一個(gè)進(jìn)程來運(yùn)行JavaScript程序,在瀏覽器中每個(gè)相同的JavaScript只存在一個(gè)SharedWorker進(jìn)程楞慈,不管它被創(chuàng)建多少次幔烛。

看到這里,應(yīng)該就很容易明白了囊蓝,本質(zhì)上就是進(jìn)程和線程的區(qū)別饿悬。SharedWorker由獨(dú)立的進(jìn)程管理,WebWorker只是屬于render進(jìn)程下的一個(gè)線程

簡(jiǎn)單梳理下瀏覽器渲染流程

本來是直接計(jì)劃開始談JS運(yùn)行機(jī)制的聚霜,但想了想狡恬,既然上述都一直在談瀏覽器珠叔,直接跳到JS可能再突兀,因此弟劲,中間再補(bǔ)充下瀏覽器的渲染流程(簡(jiǎn)單版本)

為了簡(jiǎn)化理解祷安,前期工作直接省略成:(要展開的或完全可以寫另一篇超長(zhǎng)文)

- 瀏覽器輸入url,瀏覽器主進(jìn)程接管兔乞,開一個(gè)下載線程汇鞭,然后進(jìn)行 http請(qǐng)求(略去DNS查詢,IP尋址等等操作)报嵌,然后等待響應(yīng)虱咧,獲取內(nèi)容,隨后將內(nèi)容通過RendererHost接口轉(zhuǎn)交給Renderer進(jìn)程- 瀏覽器渲染流程開始復(fù)制代碼

瀏覽器器內(nèi)核拿到內(nèi)容后锚国,渲染大概可以劃分成以下幾個(gè)步驟:

1.解析html建立dom樹

2.解析css構(gòu)建render樹(將CSS代碼解析成樹形的數(shù)據(jù)結(jié)構(gòu)腕巡,然后結(jié)合DOM合并成render樹)

3.布局render樹(Layout/reflow),負(fù)責(zé)各元素尺寸血筑、位置的計(jì)算

4.繪制render樹(paint)绘沉,繪制頁面像素信息

5.瀏覽器會(huì)將各層的信息發(fā)送給GPU,GPU會(huì)將各層合成(composite)豺总,顯示在屏幕上车伞。

所有詳細(xì)步驟都已經(jīng)略去,渲染完畢后就是load事件了喻喳,之后就是自己的JS邏輯處理了

既然略去了一些詳細(xì)的步驟另玖,那么就提一些可能需要注意的細(xì)節(jié)把。

這里重繪參考來源中的一張圖:(參考來源第一篇)


load事件與DOMContentLoaded事件的先后

上面提到表伦,渲染完畢后會(huì)觸發(fā)load事件谦去,那么你能分清楚load事件與DOMContentLoaded事件的先后么?

很簡(jiǎn)單蹦哼,知道它們的定義就可以了:

當(dāng) DOMContentLoaded 事件觸發(fā)時(shí)鳄哭,僅當(dāng)DOM加載完成,不包括樣式表纲熏,圖片妆丘。

(譬如如果有async加載的腳本就不一定完成)

當(dāng) onload 事件觸發(fā)時(shí),頁面上所有的DOM局劲,樣式表勺拣,腳本,圖片都已經(jīng)加載完成了鱼填。

(渲染完畢了)

所以宣脉,順序是:DOMContentLoaded -> load

css加載是否會(huì)阻塞dom樹渲染?

這里說的是頭部引入css的情況

首先剔氏,我們都知道:css是由單獨(dú)的下載線程異步下載的塑猖。

然后再說下幾個(gè)現(xiàn)象:

css加載不會(huì)阻塞DOM樹解析(異步加載時(shí)DOM照常構(gòu)建)

但會(huì)阻塞render樹渲染(渲染時(shí)需等css加載完畢竹祷,因?yàn)閞ender樹需要css信息)

這可能也是瀏覽器的一種優(yōu)化機(jī)制。

因?yàn)槟慵虞dcss的時(shí)候羊苟,可能會(huì)修改下面DOM節(jié)點(diǎn)的樣式塑陵,

如果css加載不阻塞render樹渲染的話,那么當(dāng)css加載完之后蜡励,

render樹可能又得重新重繪或者回流了令花,這就造成了一些沒有必要的損耗。

所以干脆就先把DOM樹的結(jié)構(gòu)先解析完凉倚,把可以做的工作做完兼都,然后等你css加載完之后,

在根據(jù)最終的樣式來渲染render樹稽寒,這種做法性能方面確實(shí)會(huì)比較好一點(diǎn)扮碧。

普通圖層和復(fù)合圖層

渲染步驟中就提到了composite概念。

可以簡(jiǎn)單的這樣理解杏糙,瀏覽器渲染的圖層一般包含兩大類:普通圖層以及復(fù)合圖層

首先慎王,普通文檔流內(nèi)可以理解為一個(gè)復(fù)合圖層(這里稱為默認(rèn)復(fù)合層,里面不管添加多少元素宏侍,其實(shí)都是在同一個(gè)復(fù)合圖層中)

其次赖淤,absolute布局(fixed也一樣),雖然可以脫離普通文檔流谅河,但它仍然屬于默認(rèn)復(fù)合層咱旱。

然后,可以通過硬件加速的方式绷耍,聲明一個(gè)新的復(fù)合圖層莽龟,它會(huì)單獨(dú)分配資源(當(dāng)然也會(huì)脫離普通文檔流,這樣一來锨天,不管這個(gè)復(fù)合圖層中怎么變化,也不會(huì)影響默認(rèn)復(fù)合層里的回流重繪)

可以簡(jiǎn)單理解下:GPU中剃毒,各個(gè)復(fù)合圖層是單獨(dú)繪制的病袄,所以互不影響,這也是為什么某些場(chǎng)景硬件加速效果一級(jí)棒

可以Chrome源碼調(diào)試 -> More Tools -> Rendering -> Layer borders中看到赘阀,黃色的就是復(fù)合圖層信息

如下圖益缠。可以驗(yàn)證上述的說法


如何變成復(fù)合圖層(硬件加速)

將該元素變成一個(gè)復(fù)合圖層基公,就是傳說中的硬件加速技術(shù)

最常用的方式:translate3d幅慌、translateZ

opacity屬性/過渡動(dòng)畫(需要?jiǎng)赢媹?zhí)行的過程中才會(huì)創(chuàng)建合成層,動(dòng)畫沒有開始或結(jié)束后元素還會(huì)回到之前的狀態(tài))

will-chang屬性(這個(gè)比較偏僻)轰豆,一般配合opacity與translate使用(而且經(jīng)測(cè)試胰伍,除了上述可以引發(fā)硬件加速的屬性外齿诞,其它屬性并不會(huì)變成復(fù)合層),作用是提前告訴瀏覽器要變化骂租,這樣瀏覽器會(huì)開始做一些優(yōu)化工作(這個(gè)最好用完后就釋放)

等元素

其它祷杈,譬如以前的flash插件

absolute和硬件加速的區(qū)別

可以看到,absolute雖然可以脫離普通文檔流渗饮,但是無法脫離默認(rèn)復(fù)合層但汞。

所以,就算absolute中信息改變時(shí)不會(huì)改變普通文檔流中render樹互站,

但是私蕾,瀏覽器最終繪制時(shí),是整個(gè)復(fù)合層繪制的胡桃,所以absolute中信息的改變踩叭,仍然會(huì)影響整個(gè)復(fù)合層的繪制。

(瀏覽器會(huì)重繪它标捺,如果復(fù)合層中內(nèi)容多懊纳,absolute帶來的繪制信息變化過大,資源消耗是非常嚴(yán)重的)

而硬件加速直接就是在另一個(gè)復(fù)合層了(另起爐灶)亡容,所以它的信息改變不會(huì)影響默認(rèn)復(fù)合層

(當(dāng)然了嗤疯,內(nèi)部肯定會(huì)影響屬于自己的復(fù)合層),僅僅是引發(fā)最后的合成(輸出視圖)

復(fù)合圖層的作用闺兢?

一般一個(gè)元素開啟硬件加速后會(huì)變成復(fù)合圖層茂缚,可以獨(dú)立于普通文檔流中,改動(dòng)后可以避免整個(gè)頁面重繪屋谭,提升性能

但是盡量不要大量使用復(fù)合圖層脚囊,否則由于資源消耗過度,頁面反而會(huì)變的更卡

硬件加速時(shí)請(qǐng)使用index

使用硬件加速時(shí)桐磁,盡可能的使用index悔耘,防止瀏覽器默認(rèn)給后續(xù)的元素創(chuàng)建復(fù)合層渲染

具體的原理時(shí)這樣的:webkit CSS3中,如果這個(gè)元素添加了硬件加速我擂,并且index層級(jí)比較低衬以,

那么在這個(gè)元素的后面其它元素(層級(jí)比這個(gè)元素高的,或者相同的校摩,并且releative或absolute屬性相同的)看峻,

會(huì)默認(rèn)變?yōu)閺?fù)合層渲染,如果處理不當(dāng)會(huì)極大的影響性能

簡(jiǎn)單點(diǎn)理解衙吩,其實(shí)可以認(rèn)為是一個(gè)隱式合成的概念:如果a是一個(gè)復(fù)合圖層互妓,而且b在a上面,那么b也會(huì)被隱式轉(zhuǎn)為一個(gè)復(fù)合圖層,這點(diǎn)需要特別注意

另外冯勉,這個(gè)問題可以在這個(gè)地址看到重現(xiàn)(原作者分析的挺到位的澈蚌,直接上鏈接):

web.jobbole.com/83575/

從Event Loop談JS的運(yùn)行機(jī)制

到此時(shí),已經(jīng)是屬于瀏覽器頁面初次渲染完畢后的事情珠闰,JS引擎的一些運(yùn)行機(jī)制分析惜浅。

注意,這里不談可執(zhí)行上下文伏嗜,VO坛悉,scop chain等概念(這些完全可以整理成另一篇文章了),這里主要是結(jié)合Event Loop來談JS代碼是如何執(zhí)行的承绸。

讀這部分的前提是已經(jīng)知道了JS引擎是單線程裸影,而且這里會(huì)用到上文中的幾個(gè)概念:(如果不是很理解,可以回頭溫習(xí))

JS引擎線程

事件觸發(fā)線程

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

然后再理解一個(gè)概念:

JS分為同步任務(wù)和異步任務(wù)

同步任務(wù)都在主線程上執(zhí)行军熏,形成一個(gè)執(zhí)行棧

主線程之外轩猩,事件觸發(fā)線程管理著一個(gè)任務(wù)隊(duì)列,只要異步任務(wù)有了運(yùn)行結(jié)果荡澎,就在任務(wù)隊(duì)列之中放置一個(gè)事件均践。

一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢(此時(shí)JS引擎空閑),系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列摩幔,將可運(yùn)行的異步任務(wù)添加到可執(zhí)行棧中彤委,開始執(zhí)行。

看圖:


看到這里或衡,應(yīng)該就可以理解了:為什么有時(shí)候setTimeout推入的事件不能準(zhǔn)時(shí)執(zhí)行焦影?因?yàn)榭赡茉谒迫氲绞录斜頃r(shí),主線程還不空閑封断,正在執(zhí)行其它代碼斯辰,

所以自然有誤差。

事件循環(huán)機(jī)制進(jìn)一步補(bǔ)充

這里就直接引用一張圖片來協(xié)助理解:(參考自Philip Roberts的演講《Help, I'm stuck in an event-loop》)


上圖大致描述就是:

主線程運(yùn)行時(shí)會(huì)產(chǎn)生執(zhí)行棧坡疼,

棧中的代碼調(diào)用某些api時(shí)彬呻,它們會(huì)在事件隊(duì)列中添加各種事件(當(dāng)滿足觸發(fā)條件后,如ajax請(qǐng)求完畢)

而棧中的代碼執(zhí)行完畢柄瑰,就會(huì)讀取事件隊(duì)列中的事件闸氮,去執(zhí)行那些回調(diào)

如此循環(huán)

注意,總是要等待棧中的代碼執(zhí)行完畢后才會(huì)去讀取事件隊(duì)列中的事件

單獨(dú)說說定時(shí)器

上述事件循環(huán)機(jī)制的核心是:JS引擎線程和事件觸發(fā)線程

但事件上狱意,里面還有一些隱藏細(xì)節(jié),譬如調(diào)用setTimeout后拯欧,是如何等待特定時(shí)間后才添加到事件隊(duì)列中的详囤?

是JS引擎檢測(cè)的么?當(dāng)然不是了。它是由定時(shí)器線程控制(因?yàn)镴S引擎自己都忙不過來藏姐,根本無暇分身)

為什么要單獨(dú)的定時(shí)器線程隆箩?因?yàn)镴avaScript引擎是單線程的, 如果處于阻塞線程狀態(tài)就會(huì)影響記計(jì)時(shí)的準(zhǔn)確,因此很有必要單獨(dú)開一個(gè)線程用來計(jì)時(shí)羔杨。

什么時(shí)候會(huì)用到定時(shí)器線程捌臊?當(dāng)使用setTimeout或setInterval時(shí),它需要定時(shí)器線程計(jì)時(shí)兜材,計(jì)時(shí)完成后就會(huì)將特定的事件推入事件隊(duì)列中理澎。

譬如:

setTimeout(function(){console.log('hello!');},1000);復(fù)制代碼

這段代碼的作用是當(dāng)1000毫秒計(jì)時(shí)完畢后(由定時(shí)器線程計(jì)時(shí)),將回調(diào)函數(shù)推入事件隊(duì)列中曙寡,等待主線程執(zhí)行

setTimeout(function(){console.log('hello!');},0);console.log('begin');復(fù)制代碼

這段代碼的效果是最快的時(shí)間內(nèi)將回調(diào)函數(shù)推入事件隊(duì)列中糠爬,等待主線程執(zhí)行

注意:

執(zhí)行結(jié)果是:先begin后hello!

雖然代碼的本意是0毫秒后就推入事件隊(duì)列,但是W3C在HTML標(biāo)準(zhǔn)中規(guī)定举庶,規(guī)定要求setTimeout中低于4ms的時(shí)間間隔算為4ms执隧。

(不過也有一說是不同瀏覽器有不同的最小時(shí)間設(shè)定)

就算不等待4ms,就算假設(shè)0毫秒就推入事件隊(duì)列户侥,也會(huì)先執(zhí)行begin(因?yàn)橹挥锌蓤?zhí)行棧內(nèi)空了后才會(huì)主動(dòng)讀取事件隊(duì)列)

setTimeout而不是setInterval

用setTimeout模擬定期計(jì)時(shí)和直接用setInterval是有區(qū)別的镀琉。

因?yàn)槊看蝧etTimeout計(jì)時(shí)到后就會(huì)去執(zhí)行,然后執(zhí)行一段時(shí)間后才會(huì)繼續(xù)setTimeout蕊唐,中間就多了誤差

(誤差多少與代碼執(zhí)行時(shí)間有關(guān))

而setInterval則是每次都精確的隔一段時(shí)間推入一個(gè)事件

(但是屋摔,事件的實(shí)際執(zhí)行時(shí)間不一定就準(zhǔn)確,還有可能是這個(gè)事件還沒執(zhí)行完畢刃泌,下一個(gè)事件就來了)

而且setInterval有一些比較致命的問題就是:

累計(jì)效應(yīng)(上面提到的)凡壤,如果setInterval代碼在(setInterval)再次添加到隊(duì)列之前還沒有完成執(zhí)行,

就會(huì)導(dǎo)致定時(shí)器代碼連續(xù)運(yùn)行好幾次耙替,而之間沒有間隔亚侠。

就算正常間隔執(zhí)行,多個(gè)setInterval的代碼執(zhí)行時(shí)間可能會(huì)比預(yù)期兴咨取(因?yàn)榇a執(zhí)行需要一定時(shí)間)

譬如像iOS的webview,或者Safari等瀏覽器中都有一個(gè)特點(diǎn)硝烂,在滾動(dòng)的時(shí)候是不執(zhí)行JS的,如果使用了setInterval铜幽,會(huì)發(fā)現(xiàn)在滾動(dòng)結(jié)束后會(huì)執(zhí)行多次由于滾動(dòng)不執(zhí)行JS積攢回調(diào)滞谢,如果回調(diào)執(zhí)行時(shí)間過長(zhǎng),就會(huì)非常容器造成卡頓問題和一些不可知的錯(cuò)誤(這一塊后續(xù)有補(bǔ)充,setInterval自帶的優(yōu)化除抛,不會(huì)重復(fù)添加回調(diào))

而且把瀏覽器最小化顯示等操作時(shí)狮杨,setInterval并不是不執(zhí)行程序,

它會(huì)把setInterval的回調(diào)函數(shù)放在隊(duì)列中到忽,等瀏覽器窗口再次打開時(shí)橄教,一瞬間全部執(zhí)行時(shí)

所以清寇,鑒于這么多但問題,目前一般認(rèn)為的最佳方案是:用setTimeout模擬setInterval护蝶,或者特殊場(chǎng)合直接用requestAnimationFrame

補(bǔ)充:JS高程中有提到华烟,JS引擎會(huì)對(duì)setInterval進(jìn)行優(yōu)化,如果當(dāng)前事件隊(duì)列中有setInterval的回調(diào)持灰,不會(huì)重復(fù)添加盔夜。不過,仍然是有很多問題堤魁。喂链。。

事件循環(huán)進(jìn)階:macrotask與microtask

這段參考了參考來源中的第2篇文章(英文版的)姨涡,(加了下自己的理解重新描述了下)衩藤,

強(qiáng)烈推薦有英文基礎(chǔ)的同學(xué)直接觀看原文,作者描述的很清晰涛漂,示例也很不錯(cuò)赏表,如下:

jakearchibald.com/2015/tasks-…

上文中將JS事件循環(huán)機(jī)制梳理了一遍,在ES5的情況是夠用了匈仗,但是在ES6盛行的現(xiàn)在瓢剿,仍然會(huì)遇到一些問題,譬如下面這題:

console.log('script start');setTimeout(function(){console.log('setTimeout');},0);Promise.resolve().then(function(){console.log('promise1');}).then(function(){console.log('promise2');});console.log('script end');復(fù)制代碼

嗯哼悠轩,它的正確執(zhí)行順序是這樣子的:

script startscript endpromise1promise2setTimeout復(fù)制代碼

為什么呢间狂?因?yàn)镻romise里有了一個(gè)一個(gè)新的概念:microtask

或者,進(jìn)一步火架,JS中分為兩種任務(wù)類型:macrotask和microtask鉴象,在ECMAScript中,microtask稱為jobs何鸡,macrotask可稱為task

它們的定義纺弊?區(qū)別?簡(jiǎn)單點(diǎn)可以按如下理解:

macrotask(又稱之為宏任務(wù))骡男,可以理解是每次執(zhí)行棧執(zhí)行的代碼就是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取一個(gè)事件回調(diào)并放到執(zhí)行棧中執(zhí)行)

每一個(gè)task會(huì)從頭到尾將這個(gè)任務(wù)執(zhí)行完畢淆游,不會(huì)執(zhí)行其它

瀏覽器為了能夠使得JS內(nèi)部task與DOM任務(wù)能夠有序的執(zhí)行,會(huì)在一個(gè)task執(zhí)行結(jié)束后隔盛,在下一個(gè) task 執(zhí)行開始前犹菱,對(duì)頁面進(jìn)行重新渲染(task->渲染->task->...)

microtask(又稱為微任務(wù)),可以理解是在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)

也就是說吮炕,在當(dāng)前task任務(wù)后腊脱,下一個(gè)task之前,在渲染之前

所以它的響應(yīng)速度相比setTimeout(setTimeout是task)會(huì)更快龙亲,因?yàn)闊o需等渲染

也就是說陕凹,在某一個(gè)macrotask執(zhí)行完后震鹉,就會(huì)將在它執(zhí)行期間產(chǎn)生的所有microtask都執(zhí)行完畢(在渲染前)

分別很么樣的場(chǎng)景會(huì)形成macrotask和microtask呢?

macrotask:主代碼塊捆姜,setTimeout,setInterval等(可以看到迎膜,事件隊(duì)列中的每一個(gè)事件都是一個(gè)macrotask)

microtask:Promise泥技,process.nextTick等

補(bǔ)充:在node環(huán)境下,process.nextTick的優(yōu)先級(jí)高于Promise磕仅,也就是可以簡(jiǎn)單理解為:在宏任務(wù)結(jié)束后會(huì)先執(zhí)行微任務(wù)隊(duì)列中的nextTickQueue部分珊豹,然后才會(huì)執(zhí)行微任務(wù)中的Promise部分。

參考:segmentfault.com/q/101000001…

再根據(jù)線程來理解下:

macrotask中的事件都是放在一個(gè)事件隊(duì)列中的榕订,而這個(gè)隊(duì)列由事件觸發(fā)線程維護(hù)

microtask中的所有微任務(wù)都是添加到微任務(wù)隊(duì)列(Job Queues)中店茶,等待當(dāng)前macrotask執(zhí)行完畢后執(zhí)行,而這個(gè)隊(duì)列由JS引擎線程維護(hù)(這點(diǎn)由自己理解+推測(cè)得出劫恒,因?yàn)樗窃谥骶€程下無縫執(zhí)行的)

所以贩幻,總結(jié)下運(yùn)行機(jī)制:

執(zhí)行一個(gè)宏任務(wù)(棧中沒有就從事件隊(duì)列中獲取)

執(zhí)行過程中如果遇到微任務(wù)两嘴,就將它添加到微任務(wù)的任務(wù)隊(duì)列中

宏任務(wù)執(zhí)行完畢后丛楚,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)

當(dāng)前宏任務(wù)執(zhí)行完畢,開始檢查渲染憔辫,然后GUI線程接管渲染

渲染完畢后趣些,JS線程繼續(xù)接管,開始下一個(gè)宏任務(wù)(從事件隊(duì)列中獲确∧)

如圖:


另外坏平,請(qǐng)注意下Promise的polyfill與官方版本的區(qū)別:

官方版本中,是標(biāo)準(zhǔn)的microtask形式

polyfill锦亦,一般都是通過setTimeout模擬的舶替,所以是macrotask形式

請(qǐng)?zhí)貏e注意這兩點(diǎn)區(qū)別

注意,有一些瀏覽器執(zhí)行結(jié)果不一樣(因?yàn)樗鼈兛赡馨裮icrotask當(dāng)成macrotask來執(zhí)行了)孽亲,

但是為了簡(jiǎn)單坎穿,這里不描述一些不標(biāo)準(zhǔn)的瀏覽器下的場(chǎng)景(但記住,有些瀏覽器可能并不標(biāo)準(zhǔn))

20180126補(bǔ)充:使用MutationObserver實(shí)現(xiàn)microtask

MutationObserver可以用來實(shí)現(xiàn)microtask

(它屬于microtask返劲,優(yōu)先級(jí)小于Promise玲昧,

一般是Promise不支持時(shí)才會(huì)這樣做)

它是HTML5中的新特性,作用是:監(jiān)聽一個(gè)DOM變動(dòng)篮绿,

當(dāng)DOM對(duì)象樹發(fā)生任何變動(dòng)時(shí)孵延,Mutation Observer會(huì)得到通知

像以前的Vue源碼中就是利用它來模擬nextTick的,

具體原理是亲配,創(chuàng)建一個(gè)TextNode并監(jiān)聽內(nèi)容變化尘应,

然后要nextTick的時(shí)候去改一下這個(gè)節(jié)點(diǎn)的文本內(nèi)容惶凝,

如下:(Vue的源碼,未修改)

varcounter =1varobserver =newMutationObserver(nextTickHandler)vartextNode =document.createTextNode(String(counter))observer.observe(textNode, {characterData:true})timerFunc =()=>{? ? counter = (counter +1) %2textNode.data =String(counter)}復(fù)制代碼

對(duì)應(yīng)Vue源碼鏈接

不過犬钢,現(xiàn)在的Vue(2.5+)的nextTick實(shí)現(xiàn)移除了MutationObserver的方式(據(jù)說是兼容性原因)苍鲜,

取而代之的是使用MessageChannel

(當(dāng)然,默認(rèn)情況仍然是Promise玷犹,不支持才兼容的)混滔。

MessageChannel屬于宏任務(wù),優(yōu)先級(jí)是:MessageChannel->setTimeout歹颓,所以Vue(2.5+)內(nèi)部的nextTick與2.4及之前的實(shí)現(xiàn)是不一樣的坯屿,需要注意下。

這里不展開巍扛,可以看下juejin.im/post/5a1af8…

作者:dailc

鏈接:https://juejin.im/post/5a6547d0f265da3e283a1df7

來源:掘金

著作權(quán)歸作者所有领跛。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市刷后,隨后出現(xiàn)的幾起案子硕并,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡幻妓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門劫拢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肉津,“玉大人,你說我怎么就攤上這事舱沧∶蒙常” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵熟吏,是天一觀的道長(zhǎng)距糖。 經(jīng)常有香客問我,道長(zhǎng)牵寺,這世上最難降的妖魔是什么悍引? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮帽氓,結(jié)果婚禮上趣斤,老公的妹妹穿的比我還像新娘。我一直安慰自己黎休,他們只是感情好浓领,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布玉凯。 她就那樣靜靜地躺著,像睡著了一般联贩。 火紅的嫁衣襯著肌膚如雪漫仆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天泪幌,我揣著相機(jī)與錄音歹啼,去河邊找鬼。 笑死座菠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的藤树。 我是一名探鬼主播浴滴,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼岁钓!你這毒婦竟也來了升略?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤屡限,失蹤者是張志新(化名)和其女友劉穎品嚣,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钧大,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡翰撑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了啊央。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片眶诈。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瓜饥,靈堂內(nèi)的尸體忽然破棺而出逝撬,到底是詐尸還是另有隱情,我是刑警寧澤乓土,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布宪潮,位于F島的核電站,受9級(jí)特大地震影響趣苏,放射性物質(zhì)發(fā)生泄漏狡相。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一食磕、第九天 我趴在偏房一處隱蔽的房頂上張望谣光。 院中可真熱鬧,春花似錦芬为、人聲如沸萄金。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽氧敢。三九已至日戈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間孙乖,已是汗流浹背浙炼。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留唯袄,地道東北人弯屈。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像恋拷,于是被迫代替她去往敵國和親资厉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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