摘要:打開一個網(wǎng)頁,看到服務(wù)器返回給客戶端瀏覽器的各種文件類型圖片構(gòu)建瀏覽器會遵守一套步驟將文件轉(zhuǎn)換為樹。因?yàn)闉g覽器有渲染線程與引擎線程,為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果,這兩個線程是互斥的關(guān)系。
1. 瀏覽器架構(gòu)
用戶界面
主進(jìn)程
內(nèi)核
渲染引擎
JS 引擎
執(zhí)行棧
事件觸發(fā)線程
消息隊(duì)列
微任務(wù)
宏任務(wù)
網(wǎng)絡(luò)異步線程
定時器線程
2. 從輸入 url 到頁面展示的過程 2.1 流程跳轉(zhuǎn)
是否有緩存
DNS查找,域名解析ip
創(chuàng)建TCP鏈接,之后才有HTTP三次握手(HTTP尋在TCP之上)
發(fā)送請求(Request)
接收響應(yīng)(Response),返回請求的文件 (html)
瀏覽器渲染(1,2并行,后面是串行)
解析HTML --> DOM ree
標(biāo)記化算法,進(jìn)行元素狀態(tài)的標(biāo)記
生成DOM
解析CSS --> CSS tree
生成CSSOM
結(jié)合 --> Render tree
結(jié)合DOM與CSSOM,生成渲染樹(Render tree)
layout: 布局(布局渲染樹)
painting: 繪制(繪制渲染樹)
2.2 參考https://juejin.im/post/5c64d1...
https://juejin.im/book/5b9365...
Cookie 的本職工作并非本地存儲,而是“維持狀態(tài)”。
因?yàn)镠TTP協(xié)議是無狀態(tài)的,HTTP協(xié)議自身不對請求和響應(yīng)之間的通信狀態(tài)進(jìn)行保存,通俗來說,服務(wù)器不知道用戶上一次做了什么,這嚴(yán)重阻礙了交互式Web應(yīng)用程序的實(shí)現(xiàn)。
在典型的網(wǎng)上購物場景中,用戶瀏覽了幾個頁面,買了一盒餅干和兩瓶飲料。最后結(jié)帳時,由于HTTP的無狀態(tài)性,不通過額外的手段,服務(wù)器并不知道用戶到底買了什么,于是就誕生了Cookie
我們可以把Cookie 理解為一個存儲在瀏覽器里的一個小小的文本文件,它附著在 HTTP 請求上,在瀏覽器和服務(wù)器之間“飛來飛去”。它可以攜帶用戶信息,當(dāng)服務(wù)器檢查 Cookie 的時候,便可以獲取到客戶端的狀態(tài)
在剛才的購物場景中,當(dāng)用戶選購了第一項(xiàng)商品,服務(wù)器在向用戶發(fā)送網(wǎng)頁的同時,還發(fā)送了一段Cookie,記錄著那項(xiàng)商品的信息。當(dāng)用戶訪問另一個頁面,瀏覽器會把Cookie發(fā)送給服務(wù)器,于是服務(wù)器知道他之前選購了什么。用戶繼續(xù)選購飲料,服務(wù)器就在原來那段Cookie里追加新的商品信息。結(jié)帳時,服務(wù)器讀取發(fā)送來的Cookie就行了。
3.1.2 什么是cookieCookie指某些網(wǎng)站為了辨別用戶身份而儲存在用戶本地終端上的數(shù)據(jù)(通常經(jīng)過加密)。 cookie是服務(wù)端生成,客戶端進(jìn)行維護(hù)和存儲
通過cookie,可以讓服務(wù)器知道請求是來源哪個客戶端,就可以進(jìn)行客戶端狀態(tài)的維護(hù),比如登陸后刷新,請求頭就會攜帶登陸時response header中的set-cookie,Web服務(wù)器接到請求時也能讀出cookie的值,根據(jù)cookie值的內(nèi)容就可以判斷和恢復(fù)一些用戶的信息狀態(tài)。
記住密碼,下次自動登錄。
購物車功能
記錄用戶瀏覽數(shù)據(jù),進(jìn)行商品(廣告)推薦。
3.1.4 Cookie的原理第一次訪問網(wǎng)站的時候,瀏覽器發(fā)出請求,服務(wù)器響應(yīng)請求后,會在響應(yīng)頭里面添加一個Set-Cookie選項(xiàng),將cookie放入到響應(yīng)請求中,在瀏覽器第二次發(fā)請求的時候,會通過Cookie請求頭部將Cookie信息發(fā)送給服務(wù)器,服務(wù)端會辨別用戶身份,另外,Cookie的過期時間、域、路徑、有效期、適用站點(diǎn)都可以根據(jù)需要來指定。
3.1.5 Cookie生成方式服務(wù)端
http response header中的set-cookie
客戶端
js中可以通過document.cookie可以讀寫cookie
document.cookie="userName=hello"3.1.6 Cookie的缺陷
Cookie 不夠大
各瀏覽器的cookie每一個name=value的value值大概在4k,所以4k并不是一個域名下所有的cookie共享的,而是一個name的大小。
過多的 Cookie 會帶來巨大的性能浪費(fèi)
Cookie 是緊跟域名的。同一個域名下的所有請求,都會攜帶 Cookie
由于在HTTP請求中的Cookie是明文傳遞的,所以安全性成問題,除非用HTTPS。
3.1.7 Cookie與安全屬性 | 作用 |
---|---|
value | 如果用于保存用戶登錄狀態(tài),應(yīng)該將該值加密,不能使用明文的用戶標(biāo)識(例如可以使用md5加密) |
http-only | 不能通過js訪問cookie,減少xss攻擊 |
secure | 只能在協(xié)議為https的請求中攜帶 |
same-site | 規(guī)定瀏覽器不能在跨域的請求中攜帶cookie,減少csrf攻擊 |
保存的數(shù)據(jù)長期存在,下一次訪問該網(wǎng)站的時候,網(wǎng)頁可以直接讀取以前保存的數(shù)據(jù)。
大小為5M左右
僅在客戶端使用,不和服務(wù)端進(jìn)行通信
LocalStorage可以作為瀏覽器本地緩存方案,用來提升網(wǎng)頁首屏渲染速度(根據(jù)第一請求返回時,將一些不變信息直接存儲在本地)。
3.2.2存入/讀取數(shù)據(jù)localStorage.setItem("key","value"); var valueLocal = localStorage.getItem("key");3.3sessionStorage 3.3.1 sessionStorage的特點(diǎn)
會話級別的瀏覽器存儲
大小為5M左右
僅在客戶端使用,不和服務(wù)端進(jìn)行通信
3.3.2 窗口共享即便是相同域名下的兩個頁面,只要它們不在同一個瀏覽器窗口(瀏覽器的標(biāo)簽頁)中打開,那么它們的 sessionStorage 內(nèi)容便無法共享
localStorage 在所有同源窗口中都是共享的
cookie也是在所有同源窗口中都是共享的
3.3.3存入/讀取數(shù)據(jù)localStorage.setItem("key","value"); var valueLocal = localStorage.getItem("key");3.4 總結(jié)
特性 | cookie | localStorage | sessionStorage | indexDB |
---|---|---|---|---|
數(shù)據(jù)生命周期 | 一般由服務(wù)器生成,可以設(shè)置過期時間 | 除非被手動清理,否則一直存在 | 頁面關(guān)閉就清理 | 除非被手動清理,否則一直存在 |
數(shù)據(jù)存儲大小 | 4k | 5M | 5M | 無限 |
與服務(wù)端通信 | 每次都會攜帶在header中,對于請求有性能影響 | 不參與 | 不參與 | 不參與 |
cookie 已經(jīng)不建議用于存儲。如果沒有大量數(shù)據(jù)存儲需求的話,可以使用 localStorage 和 sessionStorage 。對于不怎么改變的數(shù)據(jù)盡量使用 localStorage 存儲,否則可以用 sessionStorage 存儲。
Cookie 的本職工作并非本地存儲,而是“維持狀態(tài)”
Web Storage 是 HTML5 專門為瀏覽器存儲而提供的數(shù)據(jù)存儲機(jī)制,不與服務(wù)端發(fā)生通信
IndexedDB 用于客戶端存儲大量結(jié)構(gòu)化數(shù)據(jù)
3.5 參考https://github.com/ljianshu/B...
4. 瀏覽器渲染原理 4.1 什么是渲染過程簡單來說,渲染引擎根據(jù) HTML 文件描述構(gòu)建相應(yīng)的數(shù)學(xué)模型,調(diào)用瀏覽器各個零部件,從而將網(wǎng)頁資源代碼轉(zhuǎn)換為圖像結(jié)果,這個過程就是渲染過程(如下圖)
從這個流程來看,瀏覽器呈現(xiàn)網(wǎng)頁這個過程,宛如一個黑盒。在這個神秘的黑盒中,有許多功能模塊,內(nèi)核內(nèi)部的實(shí)現(xiàn)正是這些功能模塊相互配合協(xié)同工作進(jìn)行的。其中我們最需要關(guān)注的,就是HTML 解釋器、CSS 解釋器、圖層布局計(jì)算模塊、視圖繪制模塊與JavaScript 引擎這幾大模塊:
HTML 解釋器:將 HTML 文檔經(jīng)過詞法分析輸出 DOM 樹。
CSS 解釋器:解析 CSS 文檔, 生成樣式規(guī)則。
圖層布局計(jì)算模塊:布局計(jì)算每個對象的精確位置和大小。
視圖繪制模塊:進(jìn)行具體節(jié)點(diǎn)的圖像繪制,將像素渲染到屏幕上。
JavaScript引擎:編譯執(zhí)行 Javascript 代碼。
打開一個網(wǎng)頁,看到服務(wù)器返回給客戶端(瀏覽器)的各種文件類型
document --> html
stylesheet --> css
script --> js
jpeg --> 圖片
4.2 構(gòu)建DOM瀏覽器會遵守一套步驟將HTML 文件轉(zhuǎn)換為 DOM 樹。宏觀上,可以分為幾個步驟
瀏覽器從磁盤或網(wǎng)絡(luò)讀取HTML的原始字節(jié),并根據(jù)文件的指定編碼(例如 UTF-8)將它們轉(zhuǎn)換成字符串。
在網(wǎng)絡(luò)中傳輸?shù)膬?nèi)容其實(shí)都是 0 和 1 這些字節(jié)數(shù)據(jù)。當(dāng)瀏覽器接收到這些字節(jié)數(shù)據(jù)以后,它會將這些字節(jié)數(shù)據(jù)轉(zhuǎn)換為字符串,也就是我們寫的代碼。
將字符串轉(zhuǎn)換成Token,例如:、
等。Token中會標(biāo)識出當(dāng)前Token是“開始標(biāo)簽”或是“結(jié)束標(biāo)簽”亦或是“文本”等信息。
上圖給出了節(jié)點(diǎn)之間的關(guān)系,例如:“Hello”Token位于“title”開始標(biāo)簽與“title”結(jié)束標(biāo)簽之間,表明“Hello”Token是“title”Token的子節(jié)點(diǎn)。同理“title”Token是“head”Token的子節(jié)點(diǎn)。
生成節(jié)點(diǎn)對象并構(gòu)建DOM
事實(shí)上,構(gòu)建DOM的過程中,不是等所有Token都轉(zhuǎn)換完成后再去生成節(jié)點(diǎn)對象,而是一邊生成Token一邊消耗Token來生成節(jié)點(diǎn)對象。換句話說,每個Token被生成后,會立刻消耗這個Token創(chuàng)建出節(jié)點(diǎn)對象。注意:帶有結(jié)束標(biāo)簽標(biāo)識的Token不會創(chuàng)建節(jié)點(diǎn)對象。
實(shí)例
Web page parsing Web page parsing
This is an example Web page.
上面這段HTML會解析成這樣
4.3 構(gòu)建CSSOMDOM會捕獲頁面的內(nèi)容,但瀏覽器還需要知道頁面如何展示,所以需要構(gòu)建CSSOM
構(gòu)建CSSOM的過程與構(gòu)建DOM的過程非常相似,當(dāng)瀏覽器接收到一段CSS,瀏覽器首先要做的是識別出Token,然后構(gòu)建節(jié)點(diǎn)并生成CSSOM。
在這一過程中,瀏覽器會確定下每一個節(jié)點(diǎn)的樣式到底是什么,并且這一過程其實(shí)是很消耗資源的。因?yàn)闃邮侥憧梢宰孕性O(shè)置給某個節(jié)點(diǎn),也可以通過繼承獲得。在這一過程中,瀏覽器得遞歸 CSSOM 樹,然后確定具體的元素到底是什么樣式
注意:CSS匹配HTML元素是一個相當(dāng)復(fù)雜和有性能問題的事情。所以,DOM樹要小,CSS盡量用id和class,千萬不要過渡層疊下去。
4.3 構(gòu)建渲染樹當(dāng)我們生成 DOM 樹和 CSSOM 樹以后,就需要將這兩棵樹組合為渲染樹
在這一過程中,不是簡單的將兩者合并就行了。渲染樹只會包括需要顯示的節(jié)點(diǎn)和這些節(jié)點(diǎn)的樣式信息,如果某個節(jié)點(diǎn)是 display: none 的,那么就不會在渲染樹中顯示。
瀏覽器如果渲染過程中遇到JS文件怎么處理
渲染過程中,如果遇到
4.4 布局與繪制當(dāng)瀏覽器生成渲染樹以后,就會根據(jù)渲染樹來進(jìn)行布局(也可以叫做回流)。這一階段瀏覽器要做的事情是要弄清楚各個節(jié)點(diǎn)在頁面中的確切位置和大小。通常這一行為也被稱為“自動重排”。
布局流程的輸出是一個“盒模型”,它會精確地捕獲每個元素在視口內(nèi)的確切位置和尺寸,所有相對測量值都將轉(zhuǎn)換為屏幕上的絕對像素。
布局完成后,瀏覽器會立即發(fā)出“Paint Setup”和“Paint”事件,將渲染樹轉(zhuǎn)換成屏幕上的像素。
4.5 async和defer的作用是什么?有什么區(qū)別?async 異步
defer 延緩
其中藍(lán)色線代表JavaScript加載;紅色線代表JavaScript執(zhí)行;綠色線代表 HTML 解析。
情況1
沒有 defer 或 async,瀏覽器會立即加載并執(zhí)行指定的腳本,也就是說不等待后續(xù)載入的文檔元素,讀到就加載并執(zhí)行。
情況2 (異步下載)
async 屬性表示異步執(zhí)行引入的 JavaScript,與 defer 的區(qū)別在于,如果已經(jīng)加載好(符合異步),就會開始執(zhí)行——無論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發(fā)之后。需要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發(fā)之前或之后執(zhí)行,但一定在 load 觸發(fā)之前執(zhí)行
情況3 (延遲執(zhí)行)
defer 屬性表示延遲執(zhí)行引入的 JavaScript,即這段 JavaScript 加載時 HTML 并未停止解析,這兩個過程是并行的。整個 document 解析完畢且 defer-script 也加載完成之后(這兩件事情的順序無關(guān)),會執(zhí)行所有由 defer-script 加載的 JavaScript 代碼,然后觸發(fā) DOMContentLoaded 事件。
defer 與相比普通 script,有兩點(diǎn)區(qū)別:載入 JavaScript 文件時不阻塞 HTML 的解析,執(zhí)行階段被放到 HTML 標(biāo)簽解析完成之后。
在加載多個JS腳本的時候,async是無順序的加載,而defer是有順序的加載。
4.6 為什么操作 DOM 慢把 DOM 和 JavaScript 各自想象成一個島嶼,它們之間用收費(fèi)橋梁連接?!陡咝阅?JavaScript》
JS 是很快的,在 JS 中修改 DOM 對象也是很快的。在JS的世界里,一切是簡單的、迅速的。但 DOM 操作并非 JS 一個人的獨(dú)舞,而是兩個模塊之間的協(xié)作。
因?yàn)?DOM 是屬于渲染引擎中的東西,而 JS 又是 JS 引擎中的東西。當(dāng)我們用 JS 去操作 DOM 時,本質(zhì)上是 JS 引擎和渲染引擎之間進(jìn)行了“跨界交流”。這個“跨界交流”的實(shí)現(xiàn)并不簡單,它依賴了橋接接口作為“橋梁”(如下圖)。
過“橋”要收費(fèi)——這個開銷本身就是不可忽略的。我們每操作一次 DOM(不管是為了修改還是僅僅為了訪問其值),都要過一次“橋”。過“橋”的次數(shù)一多,就會產(chǎn)生比較明顯的性能問題。因此“減少 DOM 操作”的建議,并非空穴來風(fēng)。
4.7 總結(jié)瀏覽器工作流程:構(gòu)建DOM -> 構(gòu)建CSSOM -> 構(gòu)建渲染樹 -> 布局 -> 繪制。
CSSOM會阻塞渲染,只有當(dāng)CSSOM構(gòu)建完畢后才會進(jìn)入下一個階段構(gòu)建渲染樹。
通常情況下DOM和CSSOM是并行構(gòu)建的,但是當(dāng)瀏覽器遇到一個不帶defer或async屬性的script標(biāo)簽時,DOM構(gòu)建將暫停,如果此時又恰巧瀏覽器尚未完成CSSOM的下載和構(gòu)建,由于JavaScript可以修改CSSOM,所以需要等CSSOM構(gòu)建完畢后再執(zhí)行JS,最后才重新DOM構(gòu)建。
4.8 參考深入淺出瀏覽器渲染原理
5. 重繪與回流(重排) 5.1 名詞解析通俗理解
重繪: 重新繪制,繪制色彩
回流(重排): 重新排版,排版位置大小
比較官方的理解
重繪:當(dāng)我們對 DOM 的修改導(dǎo)致了樣式的變化、卻并未影響其幾何屬性(比如修改了顏色或背景色)時,瀏覽器不需重新計(jì)算元素的幾何屬性、直接為該元素繪制新的樣式(跳過了上圖所示的回流環(huán)節(jié))。
回流: 當(dāng)我們對 DOM 的修改引發(fā)了 DOM 幾何尺寸的變化(比如修改元素的寬、高或隱藏元素等)時,瀏覽器需要重新計(jì)算元素的幾何屬性(其他元素的幾何屬性和位置也會因此受到影響),然后再將計(jì)算的結(jié)果繪制出來。這個過程就是回流(也叫重排)
5.2 瀏覽器渲染的流程中的回流與重繪計(jì)算CSS樣式
構(gòu)建RenderTree
Layout(布局) –-> 定位坐標(biāo)和大小
paint(繪制)-->正式開畫
注意:上圖流程中有很多連接線,這表示了Javascript動態(tài)修改了DOM屬性或是CSS屬性會導(dǎo)致重新Layout,但有些改變不會重新Layout,就是上圖中那些指到天上的箭頭,比如修改后的CSS rule沒有被匹配到元素。
我們知道,當(dāng)網(wǎng)頁生成的時候,至少會渲染一次。在用戶訪問的過程中,還會不斷重新渲染。重新渲染會重復(fù)回流+重繪或者只有重繪
回流必定會發(fā)生重繪,重繪不一定會引發(fā)回流。重繪和回流會在我們設(shè)置節(jié)點(diǎn)樣式時頻繁出現(xiàn),同時也會很大程度上影響性能?;亓魉璧某杀颈戎乩L高的多,改變父節(jié)點(diǎn)里的子節(jié)點(diǎn)很可能會導(dǎo)致父節(jié)點(diǎn)的一系列回流。
5.3 常見引起回流屬性和方法任何會改變元素幾何信息(元素的位置和尺寸大小)的操作,都會觸發(fā)回流,
添加或者刪除可見的DOM元素;
元素尺寸改變——邊距、填充、邊框、寬度和高度
內(nèi)容變化,比如用戶在input框中輸入文字
瀏覽器窗口尺寸改變——resize事件發(fā)生時
計(jì)算 offsetWidth 和 offsetHeight 屬性
設(shè)置 style 屬性的值
5.4 常見引起重繪屬性和方法文字屬性
邊框樣式(非大?。?/p>
色彩
背景色
陰影
5.5如何減少回流、重繪使用 transform 替代 top
使用 visibility 替換 display: none ,因?yàn)榍罢咧粫鹬乩L,后者會引發(fā)回流(改變了布局)
不要把節(jié)點(diǎn)的屬性值放在一個循環(huán)里當(dāng)成循環(huán)里的變量。
for(let i = 0; i < 1000; i++) { // 獲取 offsetTop 會導(dǎo)致回流,因?yàn)樾枰カ@取正確的值 console.log(document.querySelector(".test").style.offsetTop) }
不要使用 table 布局,可能很小的一個小改動會造成整個 table 的重新布局
動畫實(shí)現(xiàn)的速度的選擇,動畫速度越快,回流次數(shù)越多,也可以選擇使用 requestAnimationFrame
CSS 選擇符從右往左匹配查找,避免節(jié)點(diǎn)層級過多
將頻繁重繪或者回流的節(jié)點(diǎn)設(shè)置為圖層,圖層能夠阻止該節(jié)點(diǎn)的渲染行為影響別的節(jié)點(diǎn)。比如對于 video 標(biāo)簽來說,瀏覽器會自動將該節(jié)點(diǎn)變?yōu)閳D層。
5.6 參考https://github.com/ljianshu/B...
6. 瀏覽器下的Event Loop 6.1 線程與進(jìn)程 6.1.1概念我們經(jīng)常說JS 是單線程執(zhí)行的,指的是一個進(jìn)程里只有一個主線程,那到底什么是線程?什么是進(jìn)程?
官方的說法是
進(jìn)程: CPU資源分配的最小單位
線程: CPU調(diào)度的最小單位
進(jìn)程好比圖中的工廠,有多帶帶的專屬自己的工廠資源。
線程好比圖中的工人,多個工人在一個工廠中協(xié)作工作,工廠與工人是 1:n的關(guān)系。也就是說一個進(jìn)程由一個或多個線程組成,線程是一個進(jìn)程中代碼的不同執(zhí)行路線;
廠的空間是工人們共享的,這象征一個進(jìn)程的內(nèi)存空間是共享的,每個線程都可用這些共享內(nèi)存。
多個工廠之間獨(dú)立存在
6.1.2 多進(jìn)程與多線程多進(jìn)程:在同一個時間里,同一個計(jì)算機(jī)系統(tǒng)中如果允許兩個或兩個以上的進(jìn)程處于運(yùn)行狀態(tài)。多進(jìn)程帶來的好處是明顯的,比如你可以聽歌的同時,打開編輯器敲代碼,編輯器和聽歌軟件的進(jìn)程之間絲毫不會相互干擾。
多線程:程序中包含多個執(zhí)行流,即在一個程序中可以同時運(yùn)行多個不同的線程來執(zhí)行不同的任務(wù),也就是說允許單個程序創(chuàng)建多個并行執(zhí)行的線程來完成各自的任務(wù)。
以Chrome瀏覽器中為例,當(dāng)你打開一個 Tab 頁時,其實(shí)就是創(chuàng)建了一個進(jìn)程,一個進(jìn)程中可以有多個線程(下文會詳細(xì)介紹),比如渲染線程、JS 引擎線程、HTTP 請求線程等等。當(dāng)你發(fā)起一個請求時,其實(shí)就是創(chuàng)建了一個線程,當(dāng)請求結(jié)束后,該線程可能就會被銷毀。
6.1.3js單線程存在的問題js是單線程的,處理任務(wù)是一件接著一件處理,所以如果一個任務(wù)需要處理很久的話,后面的任務(wù)就會被阻塞
所以js通過Event Loop事件循環(huán)的方式解決了這個問題
簡單來說瀏覽器內(nèi)核是通過取得頁面內(nèi)容、整理信息(應(yīng)用CSS)、計(jì)算和組合最終輸出可視化的圖像結(jié)果,通常也被稱為渲染引擎。
瀏覽器內(nèi)核是多線程,在內(nèi)核控制下各線程相互配合以保持同步,一個瀏覽器通常由以下常駐線程組成:
GUI 渲染線程
JavaScript引擎線程
定時觸發(fā)器線程
事件觸發(fā)線程
異步http請求線程
6.2.2GUI渲染線程主要負(fù)責(zé)頁面的渲染,解析HTML、CSS,構(gòu)建DOM樹,布局和繪制等。
當(dāng)界面需要重繪或者由于某種操作引發(fā)回流時,將執(zhí)行該線程。
該線程與JS引擎線程互斥,當(dāng)執(zhí)行JS引擎線程時,GUI渲染會被掛起,當(dāng)任務(wù)隊(duì)列空閑時,主線程才會去執(zhí)行GUI渲染。
6.2.3 JS引擎線程該線程當(dāng)然是主要負(fù)責(zé)處理 JavaScript腳本,執(zhí)行代碼。
也是主要負(fù)責(zé)執(zhí)行準(zhǔn)備好待執(zhí)行的事件,即定時器計(jì)數(shù)結(jié)束,或者異步請求成功并正確返回時,將依次進(jìn)入任務(wù)隊(duì)列,等待 JS引擎線程的執(zhí)行。
當(dāng)然,該線程與 GUI渲染線程互斥,當(dāng) JS引擎線程執(zhí)行 JavaScript腳本時間過長,將導(dǎo)致頁面渲染的阻塞。
6.2.4 定時器觸發(fā)線程負(fù)責(zé)執(zhí)行異步定時器一類的函數(shù)的線程,如: setTimeout,setInterval。
主線程依次執(zhí)行代碼時,遇到定時器,會將定時器交給該線程處理,當(dāng)計(jì)數(shù)完畢后,事件觸發(fā)線程會將計(jì)數(shù)完畢后的事件加入到任務(wù)隊(duì)列的尾部,等待JS引擎線程執(zhí)行。
6.2.5 事件觸發(fā)線程主要負(fù)責(zé)將準(zhǔn)備好的事件交給 JS引擎線程執(zhí)行
比如 setTimeout定時器計(jì)數(shù)結(jié)束, ajax等異步請求成功并觸發(fā)回調(diào)函數(shù),或者用戶觸發(fā)點(diǎn)擊事件時,該線程會將整裝待發(fā)的事件依次加入到任務(wù)隊(duì)列的隊(duì)尾,等待 JS引擎線程的執(zhí)行。
負(fù)責(zé)執(zhí)行異步請求一類的函數(shù)的線程,如: Promise,axios,ajax等。
主線程依次執(zhí)行代碼時,遇到異步請求,會將函數(shù)交給該線程處理,當(dāng)監(jiān)聽到狀態(tài)碼變更,如果有回調(diào)函數(shù),事件觸發(fā)線程會將回調(diào)函數(shù)加入到任務(wù)隊(duì)列的尾部,等待JS引擎線程執(zhí)行。
6.3 event loop 6.3.1stack,queue,heapstack(棧),先進(jìn)后出
queue(隊(duì)列),先進(jìn)先出,生活中的排隊(duì)
heap(堆):存儲obj對象
6.3.2 執(zhí)行棧js引擎運(yùn)行時,當(dāng)代碼開始運(yùn)行的時候,會將代碼,壓入執(zhí)行棧進(jìn)行執(zhí)行
實(shí)例
當(dāng)代碼被解析后,函數(shù)會依次被壓入到棧中
有入棧,就要有出棧,當(dāng)函數(shù)c執(zhí)行完,開始出棧
前面執(zhí)行棧,先入后出,但其實(shí)也是同步的,同步就意味著會阻塞,所以需要異步,那當(dāng)執(zhí)行棧中出現(xiàn)異步代碼會怎么樣
當(dāng)瀏覽器在執(zhí)行棧執(zhí)行的時候,發(fā)現(xiàn)有異步任務(wù)之后,會交給webapi去維護(hù),而執(zhí)行棧則繼續(xù)執(zhí)行后面的任務(wù)
同樣,setTimeout同樣會被添加到webapi中
webapi是瀏覽器自己實(shí)現(xiàn)的功能,這里專門維護(hù)事件。
上面setTimeout旁邊有個進(jìn)度條,這個進(jìn)度就是設(shè)置的等待時間
6.3.4 回調(diào)隊(duì)列callback queue當(dāng)setTimeout執(zhí)行結(jié)束的時候,是不是就應(yīng)該回到執(zhí)行棧,進(jìn)行執(zhí)行輸出呢?
答案:并不是!
此時,倒計(jì)時結(jié)束后的setTimeout的可執(zhí)行函數(shù),被放入了回調(diào)隊(duì)列
最后,setTimeout的可執(zhí)行函數(shù),被從回調(diào)隊(duì)列中取出,再次放入了執(zhí)行棧
這樣的執(zhí)行過程就叫 event loop事件循環(huán)
6.4 Event Loop的具體流程 6.4.1 執(zhí)行棧任務(wù)清空后,才會從回調(diào)隊(duì)列頭部取出一個任務(wù)console.log(1)被壓入執(zhí)行棧
setTimeout在執(zhí)行棧被識別為異步任務(wù),放入webapis中
console.log(3)被壓入執(zhí)行棧,此時setTimeout倒計(jì)時結(jié)束后,把可執(zhí)行代碼console.log(2)放入回調(diào)隊(duì)列里等待
console.log(3)執(zhí)行完成后,從回調(diào)隊(duì)列頭部取出console.log(2),放入執(zhí)行棧
console.log(2)執(zhí)行
6.4.2 回調(diào)隊(duì)列先進(jìn)先出當(dāng)console.log(4)執(zhí)行完成后,從回調(diào)隊(duì)列里取出了console.log(2);
只有console.log(2)執(zhí)行完成,執(zhí)行棧再次清空時,才會從回調(diào)隊(duì)列取出console.log(3)一個一個拿,先拿console.log(2),執(zhí)行完后,再拿console.log(3),執(zhí)行
6.4.3 把代碼從回調(diào)隊(duì)列拿到棧中執(zhí)行,發(fā)現(xiàn)在這段代碼中有異步輸出1,將2push進(jìn)回調(diào)隊(duì)列
將4push進(jìn)回調(diào)隊(duì)列
輸出5
清空了執(zhí)行棧,讀取輸出2(從回調(diào)隊(duì)列中放到棧中執(zhí)行),發(fā)現(xiàn)有3(異步),將3push進(jìn)回調(diào)隊(duì)列
清空了執(zhí)行棧,讀取輸出4
清空了執(zhí)行棧,讀取輸出3
5.4 Macrotask(宏任務(wù))、Microtask(微任務(wù)) 5.4.1 什么是宏任務(wù),微任務(wù)宏任務(wù): setTimeout、setInterval、script(整體代碼)、 I/O 操作、UI 渲染等
微任務(wù): new Promise().then(回調(diào))、MutationObserver(html5新特性)、process.nextTick、Object.observe 等。
Microtask(微任務(wù))同樣是一個任務(wù)隊(duì)列,這個隊(duì)列的執(zhí)行順序是在清空執(zhí)行棧之后
可以看到Macrotask(宏任務(wù))也就是回調(diào)隊(duì)列上面還有一個Microtask(微任務(wù))
Microtask(微任務(wù))雖然是隊(duì)列,但并不是一個一個放入執(zhí)行棧,而是當(dāng)執(zhí)行棧請空,會執(zhí)行全部Microtask(微任務(wù))隊(duì)列中的任務(wù),最后才是取回調(diào)隊(duì)列的第一個Macrotask(宏任務(wù))
將setTimeout給push進(jìn)宏任務(wù)
將then(2)push進(jìn)微任務(wù)
將then(4)push進(jìn)微任務(wù)
任務(wù)隊(duì)列為空,取出微任務(wù)第一個then(2)壓入執(zhí)行棧
輸出2,將then(3)push進(jìn)微任務(wù)
任務(wù)隊(duì)列為空,取出微任務(wù)第一個then(4)壓入執(zhí)行棧
輸出4
任務(wù)隊(duì)列為空,取出微任務(wù)第一個then(3)壓入執(zhí)行棧
輸出3
任務(wù)隊(duì)列為空,微任務(wù)也為空,取出宏任務(wù)中的setTimeout(1)
輸出1
實(shí)例
console.log("1"); setTimeout(()=>{ console.log(2) Promise.resolve().then(()=>{ console.log(3); process.nextTick(function foo() { console.log(4); }); }) }) Promise.resolve().then(()=>{ console.log(5); setTimeout(()=>{ console.log(6) }) Promise.resolve().then(()=>{ console.log(7); }) }) process.nextTick(function foo() { console.log(8); process.nextTick(function foo() { console.log(9); }); }); console.log("10")
1,輸出1
2,將setTimeout(2)push進(jìn)宏任務(wù)
3,將then(5)push進(jìn)微任務(wù)
4,在執(zhí)行棧底部添加nextTick(8)
5,輸出10
6,執(zhí)行nextTick(8)
7,輸出8
8,在執(zhí)行棧底部添加nextTick(9)
9,輸出9
10,執(zhí)行微任務(wù)then(5)
11,輸出5
12,將setTimeout(6)push進(jìn)宏任務(wù)
13,將then(7)push進(jìn)微任務(wù)
14,執(zhí)行微任務(wù)then(7)
15,輸出7
16,取出setTimeout(2)
17,輸出2
18,將then(3)push進(jìn)微任務(wù)
19,執(zhí)行微任務(wù)then(3)
20,輸出3
21,在執(zhí)行棧底部添加nextTick(4)
22,輸出4
23,取出setTimeout(6)
24,輸出6
https://juejin.im/post/5a6309...
https://github.com/ljianshu/B...
程序的運(yùn)行需要內(nèi)存。只要程序提出要求,操作系統(tǒng)或者運(yùn)行時就必須供給內(nèi)存
所謂的內(nèi)存泄漏簡單來說是不再用到的內(nèi)存,沒有及時釋放
Javascript具有自動垃圾回收機(jī)制(Garbage Collecation)。
由于字符串、對象和數(shù)組沒有固定大小,所有當(dāng)他們的大小已知時,才能對他們進(jìn)行動態(tài)的存儲分配。JavaScript程序每次創(chuàng)建字符串、數(shù)組或?qū)ο髸r,解釋器都必須分配內(nèi)存來存儲那個實(shí)體。只要像這樣動態(tài)地分配了內(nèi)存,最終都要釋放這些內(nèi)存以便他們能夠被再用,否則,JavaScript的解釋器將會消耗完系統(tǒng)中所有可用的內(nèi)存,造成系統(tǒng)崩潰。
最簡單的垃圾回收
JavaScript垃圾回收的機(jī)制很簡單:找出不再使用的變量,然后釋放掉其占用的內(nèi)存,但是這個過程不是時時的,因?yàn)槠溟_銷比較大,所以垃圾回收器會按照固定的時間間隔周期性的執(zhí)行。
var a = "浪里行舟"; var b = "前端工匠"; var a = b; //重寫a
這段代碼運(yùn)行之后,“浪里行舟”這個字符串失去了引用(之前是被a引用),系統(tǒng)檢測到這個事實(shí)之后,就會釋放該字符串的存儲空間以便這些空間可以被再利用。
6.3 垃圾回收機(jī)制垃圾回收有兩種方法:標(biāo)記清除、引用計(jì)數(shù)。引用計(jì)數(shù)不太常用,標(biāo)記清除較為常用。
標(biāo)記清除這是javascript中最常用的垃圾回收方式。當(dāng)變量進(jìn)入執(zhí)行環(huán)境是,就標(biāo)記這個變量為“進(jìn)入環(huán)境”。從邏輯上講,永遠(yuǎn)不能釋放進(jìn)入環(huán)境的變量所占用的內(nèi)存,因?yàn)橹灰獔?zhí)行流進(jìn)入相應(yīng)的環(huán)境,就可能會用到他們。當(dāng)變量離開環(huán)境時,則將其標(biāo)記為“離開環(huán)境”。
function addTen(num){ var sum += num; //垃圾收集已將這個變量標(biāo)記為“進(jìn)入環(huán)境”。 return sum; //垃圾收集已將這個變量標(biāo)記為“離開環(huán)境”。 } addTen(10); //輸出20
var user = {name : "scott", age : "21", gender : "male"}; //在全局中定義變量,標(biāo)記變量為“進(jìn)入環(huán)境” user = null; //最后定義為null,釋放內(nèi)存
var m = 0,n = 19 // 把 m,n,add() 標(biāo)記為進(jìn)入環(huán)境。 add(m, n) // 把 a, b,標(biāo)記為進(jìn)入環(huán)境。 console.log(n) // n標(biāo)記為離開環(huán)境,等待垃圾回收。 function add(a, b) { a++ var c = a + b //c標(biāo)記為進(jìn)入環(huán)境 return c //c標(biāo)記離開環(huán)境 }6.4 哪些情況會引起內(nèi)存泄漏
雖然JavaScript會自動垃圾收集,但是如果我們的代碼寫法不當(dāng),會讓變量一直處于“進(jìn)入環(huán)境”的狀態(tài),無法被回收
6.4.1 意外的全局變量function foo(arg) { bar = "this is a hidden global variable"; }
bar沒被聲明,會變成一個全局變量,在頁面關(guān)閉之前不會被釋放。
另一種意外的全局變量可能由 this 創(chuàng)建:
function foo() { this.variable = "potential accidental global"; } // foo 調(diào)用自己,this 指向了全局對象(window) foo();
在 JavaScript 文件頭部加上 "use strict",可以避免此類錯誤發(fā)生。啟用嚴(yán)格模式解析 JavaScript ,避免意外的全局變量。
6.4.2 被遺忘的計(jì)時器或回調(diào)函數(shù)var someResource = getData(); setInterval(function() { var node = document.getElementById("Node"); if(node) { // 處理 node 和 someResource node.innerHTML = JSON.stringify(someResource)); } }, 1000);
如果id為Node的元素從DOM中移除,該定時器仍會存在,同時,因?yàn)榛卣{(diào)函數(shù)中包含對someResource的引用,定時器外面的someResource也不會被釋放。
6.4.3 閉包function bindEvent(){ var obj=document.createElement("xxx") obj.onclick=function(){ // Even if it is a empty function } }
閉包可以維持函數(shù)內(nèi)局部變量,使其得不到釋放。上例定義事件回調(diào)時,由于是函數(shù)內(nèi)定義函數(shù),并且內(nèi)部函數(shù)--事件回調(diào)引用外部函數(shù),形成了閉包。
// 將事件處理函數(shù)定義在外面 function bindEvent() { var obj = document.createElement("xxx") obj.onclick = onclickHandler } // 或者在定義事件處理函數(shù)的外部函數(shù)中,刪除對dom的引用 function bindEvent() { var obj = document.createElement("xxx") obj.onclick = function() { // Even if it is a empty function } obj = null }6.4.4 沒有清理的DOM元素引用
有時,保存 DOM 節(jié)點(diǎn)內(nèi)部數(shù)據(jù)結(jié)構(gòu)很有用。假如你想快速更新表格的幾行內(nèi)容,把每一行 DOM 存成字典(JSON 鍵值對)或者數(shù)組很有意義。此時,同樣的 DOM 元素存在兩個引用:一個在 DOM 樹中,另一個在字典中。將來你決定刪除這些行時,需要把兩個引用都清除。
var elements = { button: document.getElementById("button"), image: document.getElementById("image"), text: document.getElementById("text") }; function doStuff() { image.src = "http://some.url/image"; button.click(); console.log(text.innerHTML); } function removeButton() { document.body.removeChild(document.getElementById("button")); // 此時,仍舊存在一個全局的 #button 的引用 // elements 字典。button 元素仍舊在內(nèi)存中,不能被 GC 回收。 }
雖然我們用removeChild移除了button,但是還在elements對象里保存著#button的引用,換言之,DOM元素還在內(nèi)存里面。
6.5 內(nèi)存泄漏的識別方法步驟
打開開發(fā)者工具 Performance
勾選 Screenshots 和 memory
左上角小圓點(diǎn)開始錄制(record)
停止錄制
圖中 Heap 對應(yīng)的部分就可以看到內(nèi)存在周期性的回落也可以看到垃圾回收的周期,如果垃圾回收之后的最低值(我們稱為min),min在不斷上漲,那么肯定是有較為嚴(yán)重的內(nèi)存泄漏問題。
避免內(nèi)存泄漏的一些方式
減少不必要的全局變量,或者生命周期較長的對象,及時對無用的數(shù)據(jù)進(jìn)行垃圾回收
注意程序邏輯,避免“死循環(huán)”之類的
避免創(chuàng)建過多的對象
不用了的東西要及時歸還
6.6 垃圾回收的使用場景優(yōu)化 6.6.1 數(shù)組array優(yōu)化[]賦值給一個數(shù)組對象,是清空數(shù)組的捷徑(例如: arr = [];),但是需要注意的是,這種方式又創(chuàng)建了一個新的空對象,并且將原來的數(shù)組對象變成了一小片內(nèi)存垃圾!實(shí)際上,將數(shù)組長度賦值為0(arr.length = 0)也能達(dá)到清空數(shù)組的目的,并且同時能實(shí)現(xiàn)數(shù)組重用,減少內(nèi)存垃圾的產(chǎn)生。
const arr = [1, 2, 3, 4]; console.log("浪里行舟"); arr.length = 0 // 可以直接讓數(shù)字清空,而且數(shù)組類型不變。 // arr = []; 雖然讓a變量成一個空數(shù)組,但是在堆上重新申請了一個空數(shù)組對象。6.6.2 對象盡量復(fù)用
對象盡量復(fù)用,尤其是在循環(huán)等地方出現(xiàn)創(chuàng)建新對象,能復(fù)用就復(fù)用。不用的對象,盡可能設(shè)置為null,盡快被垃圾回收掉。
var t = {} // 每次循環(huán)都會創(chuàng)建一個新對象。 for (var i = 0; i < 10; i++) { // var t = {};// 每次循環(huán)都會創(chuàng)建一個新對象。 t.age = 19 t.name = "123" t.index = i console.log(t) } t = null //對象如果已經(jīng)不用了,那就立即設(shè)置為null;等待垃圾回收。6.6.3 在循環(huán)中的函數(shù)表達(dá)式,能復(fù)用最好放到循環(huán)外面
// 在循環(huán)中最好也別使用函數(shù)表達(dá)式。 for (var k = 0; k < 10; k++) { var t = function(a) { // 創(chuàng)建了10次 函數(shù)對象。 console.log(a) } t(k) }
// 推薦用法 function t(a) { console.log(a) } for (var k = 0; k < 10; k++) { t(k) } t = null6.7 參考
https://github.com/ljianshu/B...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/104189.html
摘要:適當(dāng)引導(dǎo)面試官。如果有機(jī)會來實(shí)習(xí),如何最有效的快速成長淘寶技術(shù)部前端內(nèi)部有針對新同學(xué)的前端夜校,有專門的老師授課。 阿里巴巴2019前端實(shí)習(xí)生招聘還剩最后兩周,面向2019年11月1日至2020年10月31日之間畢業(yè)的同學(xué),在這里分享下阿里前端面試考核的關(guān)鍵點(diǎn): Q:在面試過程中,前端面試官如何考核面試者?A:會看同學(xué)為什么選擇前端行業(yè)?是因?yàn)樗惴ㄌy?Java、C++太難?還是因?yàn)闊?..
摘要:前言秋招宣告結(jié)束,面試了接近家公司,有幸拿到,感謝這段時間一起找工作面試的朋友和陪伴我的人。一定要提前準(zhǔn)備好,不然面試官叫你說遇到的難點(diǎn),或者直接問問題時可能會懵逼。 前言 秋招宣告結(jié)束,面試了接近20家公司,有幸拿到offer,感謝這段時間一起找工作面試的朋友和陪伴我的人。這是一段難忘的經(jīng)歷,相信不亞于當(dāng)年的高考吧,也許現(xiàn)在想起來高考不算什么,也許只有經(jīng)歷過秋招的人才懂得找工作的艱辛...
摘要:前言秋招宣告結(jié)束,面試了接近家公司,有幸拿到,感謝這段時間一起找工作面試的朋友和陪伴我的人。一定要提前準(zhǔn)備好,不然面試官叫你說遇到的難點(diǎn),或者直接問問題時可能會懵逼。 前言 秋招宣告結(jié)束,面試了接近20家公司,有幸拿到offer,感謝這段時間一起找工作面試的朋友和陪伴我的人。這是一段難忘的經(jīng)歷,相信不亞于當(dāng)年的高考吧,也許現(xiàn)在想起來高考不算什么,也許只有經(jīng)歷過秋招的人才懂得找工作的艱辛...
摘要:閉包有多重前端知識點(diǎn)大百科全書前端掘金,,技巧使你的更加專業(yè)前端掘金一個幫你提升技巧的收藏集。 Vue全家桶實(shí)現(xiàn)還原豆瓣電影wap版 - 掘金用vue全家桶仿寫豆瓣電影wap版。 最近在公司項(xiàng)目中嘗試使用vue,但奈何自己初學(xué)水平有限,上了vue沒有上vuex,開發(fā)過程特別難受。 于是玩一玩本項(xiàng)目,算是對相關(guān)技術(shù)更加熟悉了。 原計(jì)劃仿寫完所有頁面,礙于豆瓣的接口API有限,實(shí)現(xiàn)頁面也有...
摘要:計(jì)算數(shù)組的極值微信面試題獲取元素的最終前端掘金一題目用代碼求出頁面上一個元素的最終的,不考慮瀏覽器,不考慮元素情況。 Excuse me?這個前端面試在搞事! - 前端 - 掘金金三銀四搞事季,前端這個近年的熱門領(lǐng)域,搞事氣氛特別強(qiáng)烈,我朋友小偉最近就在瘋狂面試,遇到了許多有趣的面試官,有趣的面試題,我來幫這個搞事 boy 轉(zhuǎn)述一下。 以下是我一個朋友的故事,真的不是我。 ... ja...
摘要:知識點(diǎn)前端面試有很多知識點(diǎn),因?yàn)榍岸吮揪蜕婕暗蕉鄠€方面。因?yàn)閷τ谶@樣的前端框架我還不是很熟練,在這方面不能提供很好的學(xué)習(xí)思路。 關(guān)于這幾次的面試 前幾次的面試,讓我對于一個前端工程師需要掌握的知識體系有了一個全新的認(rèn)識。之前自己在學(xué)習(xí)方面一直屬于野路子,沒有一個很規(guī)范的學(xué)習(xí)路徑,往往都是想到什么就去學(xué)什么。而且基本都是處于會用的那種水平。并沒有真正的做到知其然且知其所以然。面試基本都沒...
閱讀 3783·2021-11-23 09:51
閱讀 4421·2021-11-15 11:37
閱讀 3534·2021-09-02 15:21
閱讀 2756·2021-09-01 10:31
閱讀 887·2021-08-31 14:19
閱讀 865·2021-08-11 11:20
閱讀 3318·2021-07-30 15:30
閱讀 1699·2019-08-30 15:54