《浏览器工作原理与实践》学习笔记
瀏覽器原理
前言
本文是學習李兵老師的《瀏覽器工作原理與實踐》過程中記錄筆記,詳細鏈接見文末
進程vs線程
進程:一個應用程序的運行實例就是一個進程,詳細來說就是:啟動一個應用程序的時候,操作系統會為該程序分配一片內存空間,用來存放代碼、數據和一個主線程,這樣的運行環境就稱為一個進程
線程:依附于進程,多個線程并行可以提高運行效率
進程和線程之間的幾個特點
1.進程中的任意一個線程執行出錯,都會導致進程崩潰
2.線程之間共享父進程的數據
3.當進程關閉的時候,整個進程的資源都會被回收
4.進程之間的內容相互隔離,如果需要進程間數據的通信,需要IPC(進程間通信)機制
單進程瀏覽瀏覽器
瀏覽器中所有功能模塊都運行在一個進程里,比如各個網頁的網絡、插件、js運行環境等
單進程瀏覽器的缺點
1.不穩定
早期的瀏覽器需要通過插件來實現一些功能,這就造成了一個問題:如果一個插件的運行出現問題,這將導致整個瀏覽器崩潰
2.不流暢
所有的渲染模塊、js執行環境以及插件都是運行在同一個線程中,這就意味著同一時刻只有一個模塊可以執行,如果有一個類似于以下的無限循環的腳本
function freeze() {while (1) {console.log("freeze");} } freeze();當這個腳本執行的時候,會獨占整個線程并且不會退出,其它未執行任務將會一直等待,從而造成瀏覽器卡頓
除此之外,頁面的內存泄漏也是單進程變慢的一個重要原因。通常瀏覽器的內核都是非常復雜的,運行一個復雜點的頁面再關閉頁面,會存在內存不能完全回收的情況,這樣導致的問題是使用時間越長,內存占用越高,瀏覽器會變得越慢
3.不安全
這里依然可以從插件和頁面腳本兩個方面來解釋該原因。插件可以使用 C/C++ 等代碼編寫,通過插件可以獲取到操作系統的任意資源,當你在頁面運行一個插件時也就意味著這個插件能完全操作你的電腦。如果是個惡意插件,那么它就可以釋放病毒、竊取你的賬號密碼,引發安全性問題
多進程瀏覽器
多進程瀏覽器框架
多進程瀏覽器如何解決單進程瀏覽器的各個缺點
1.解決不穩定的問題
各個進程之間相互隔離,即使一個頁面或者插件崩潰時,影響到的只是當前頁面,不會影響到其他頁面,更不會造成整個瀏覽器的崩潰
2.解決不流暢的問題
同樣,各個進程相互隔離,一個頁面的js阻塞的只是自己的渲染進程,不會影響到別的頁面的渲染進程,內存泄漏的問題就更簡單了,當關閉一個頁面的時候,該進程下的所有資源都會被回收
3.解決不安全的問題
多進程架構使用了安全沙箱,可以把沙箱看成是操作系統給進程上了一把鎖,沙箱里面的程序可以運行,但是不能在硬盤上寫入任何數據,也不能在敏感位置讀取任何數據,例如文檔和桌面。Chrome 把插件進程和渲染進程鎖在沙箱里面,這樣即使在渲染進程或者插件進程里面執行了惡意程序,惡意程序也無法突破沙箱去獲取系統權限
不過凡事都有兩面性,雖然多進程模型提升了瀏覽器的穩定性、流暢性和安全性,但同樣不可避免地帶來了一些問題:
更高的資源占用。因為每個進程都會包含公共基礎結構的副本(如 JavaScript 運行環境),這就意味著瀏覽器會消耗更多的內存資源
更復雜的體系架構。瀏覽器各模塊之間耦合性高、擴展性差等問題,會導致現在的架構已經很難適應新的需求了
TCP/IP
在衡量 Web 頁面性能的時候有一個重要的指標叫“FP(First Paint)”,是指從頁面加載到首次開始繪制的時長。這個指標直接影響了用戶的跳出率,更快的頁面響應意味著更多的 PV、更高的參與度,以及更高的轉化率。那什么影響 FP 指標呢?其中一個重要的因素是網絡加載速度
IP
數據包要在互聯網上進行傳輸,就要符合網際協議(Internet Protocol,簡稱 IP)標準
每個物理機都有一個唯一的IP地址,訪問一個網站實際上就是訪問這個網站的IP地址
簡化的 IP 網絡三層傳輸模型:
UDP
IP只是把數據包送到指定主機,并不知道需要講數據送給什么應用程序,因此,需要基于IP之上能和應用打交道的協議,最常見的就是UDP(用戶數據包協議,User Datagram Protocol)
IP通過IP地址將數據包送到對應的主機,UDP則是通過端口號把數據包分發給對應的應用程序,在傳輸中,網絡層會給數據包加上IP頭,而傳輸層則會給數據包加上UDP頭
簡化的 UDP 網絡四層傳輸模型:
UDP的優缺點
缺點:在使用 UDP 發送數據時,有各種因素會導致數據包出錯,雖然 UDP 可以校驗數據是否正確,但是對于錯誤的數據包,UDP 并不提供重發機制,只是丟棄當前的包,而且 UDP 在發送之后也無法知道是否能達到目的地,所以UDP不能保證數據的可靠性
優點:UDP的傳輸速度非常快
TCP
UDP傳輸中存在兩個問題:
1.數據包在傳輸過程中容易丟失
2.傳輸的時候大的數據包會被拆分成多個小數據包進行傳輸,這些小的數據包會在不同時間到達,UDP協議并不知道如何組裝這些數據包,所以無法還原
基于這兩個問題,TCP來了
TCP(傳輸控制協議,Transmission Control Protocol)是一種面向連接的,可靠的,基于字節流的傳輸協議
解決UDP傳輸存在的兩個問題:
1.提供超時重傳機制
2.TCP引入了數據包排序機制,用以保證接收的時候將亂序的數據包完整的還原
簡化的 TCP 網絡四層傳輸模型:
完整的TCP連接過程
一個TCP的完整生命周期:
首先,建立連接。三次握手是指建立連接的時候發送端和接收端需要發送三次數據包用以確認連接成功
傳輸數據階段。在此階段,接收端需要在接收到每個數據包的時候發送一個確認接收的數據包,發送端在發送之后規定時間內沒有接收到對應的確認接收的數據包,則會進行重傳。一個大文件被拆分成許多小數據包進行發送的時候,接收端會根據TCP頭的排序信息對小數據包進行排序。
斷開連接階段。四次揮手保證雙方都能斷開連接
HTTP
HTTP 是一種允許瀏覽器向服務器獲取資源的協議,是 Web 的基礎,通常由瀏覽器發起請求,用來獲取不同類型的文件,例如 HTML 文件、CSS 文件、JavaScript 文件、圖片、視頻等。此外,HTTP 也是瀏覽器使用最廣的協議
瀏覽器發起HTTP請求的過程
輸入一個URL都發生了什么(http://time.geekbang.org/index.html)
構建請求
首先瀏覽器會構建請求行信息
GET /index.html HTTP1.1查找緩存
真正發起網絡請求之前,瀏覽器會檢查瀏覽器緩存中是否有請求相關的文件。其中,瀏覽器緩存是一種在本地保存資源副本,以供下次請求時直接使用的技術
如果發現瀏覽器緩存中有請求資源的副本,則會攔截請求,返回副本資源,并直接結束請求,不會去服務器請求資源
這樣做有兩個好處:
1.緩解服務端壓力,提升性能
2.對于網站來說,緩存是實現快速加載資源的重要部分
如果緩存查找失敗,會進入網絡請求過程
準備IP地址和端口
HTTP是基于TCP之上進行的
首先進行TCP連接,則需要對應的IP,而將域名映射為IP地址需要DNS(域名系統,Domain Name System)
所以,瀏覽器會先請求DNS返回域名對應的IP地址,而這個過程中,瀏覽器也提供了DNS數據緩存服務,如果某個域名已經解析過了,瀏覽器會保存解析結果,以便下次使用,這樣又減少了一次請求。拿到IP之后就是獲取端口號
等待TCP隊列
如果當前域名的TCP連接數超過最大值,則會排隊等待連接,沒有超過則進行下一步TCP連接
TCP連接
發起HTTP請求
TCP連接之后,可以進行HTTP通信了
首先瀏覽器會向服務器發送請求行,如果請求方法是POST,那么準備的數據通過請求體發送
服務端處理HTTP請求過程
返回HTTP請求
服務器處理結束之后,向客戶端返回數據,curl可以查看請求返回數據
curl -i https://time.geekbang.org/斷開連接
一般服務端向客戶端返回了請求數據就會關閉連接,如果瀏覽器或者服務器在頭信息中攜帶了
Connection:Keep-Alive那么TCP會保持連接狀態,瀏覽器可以繼續通過同一個TCP連接發送請求,可以提升加載資源速度
重定向
curl 來查看下請求 geekbang.org 會返回什么內容?
為什么很多站點第二次打開速度會很快?
第二次打開網頁很快,是因為第一次加載的時候,緩存了一些耗時的數據,比如DNS緩存和頁面資源緩存
頁面資源緩存的處理過程:
從上圖的第一次請求可以看出,當服務器返回 HTTP 響應頭給瀏覽器時,瀏覽器是通過響應頭中的 Cache-Control 字段來設置是否緩存該資源。通常,我們還需要為這個資源設置一個緩存過期時長,而這個時長是通過 Cache-Control 中的 Max-age 參數來設置的,比如上圖設置的緩存過期時間是 2000 秒
Cache-Control:Max-age=2000但如果緩存過期了,瀏覽器則會繼續發起網絡請求,并且在 HTTP 請求頭中帶上:
If-None-Match:"4f80f-13c-3a1xb12a"服務器收到請求頭后,會根據值來判斷緩存資源是否有更新:
如果沒有更新,就返回 304 狀態碼,相當于服務器告訴瀏覽器:“這個緩存可以繼續使用,這次就不重復發送數據給你了。”
如果資源有更新,服務器就直接返回最新資源給瀏覽器
登錄狀態是如何保持的?
用戶打開登錄頁面,填入用戶名和密碼,點擊確定按鈕。點擊按鈕會觸發頁面腳本生成用戶登錄信息,然后調用 POST 方法提交用戶登錄信息給服務器。
服務器接收到瀏覽器提交的信息之后,查詢后臺,驗證用戶登錄信息是否正確,如果正確的話,會生成一段表示用戶身份的字符串,并把該字符串寫到響應頭的 Set-Cookie 字段里,如下所示,然后把響應頭發送給瀏覽器
Set-Cookie: UID=3431uad;瀏覽器在接收到服務器的響應頭后,開始解析響應頭,如果遇到響應頭里含有 Set-Cookie 字段的情況,瀏覽器就會把這個字段信息保存到本地。比如把“UID=3431uad”保存到本地
當用戶再次訪問時,瀏覽器會發起 HTTP 請求,但在發起請求之前,瀏覽器會讀取之前保存的 Cookie 數據,并把數據寫進請求頭里的 Cookie 字段里(如下所示),然后瀏覽器再將請求頭發送給服務器
Cookie: UID=3431uad;服務器在收到 HTTP 請求頭數據之后,就會查找請求頭里面的“Cookie”字段信息,當查找到包含UID=3431uad的信息時,服務器查詢后臺,并判斷該用戶是已登錄狀態,然后生成含有該用戶信息的頁面數據,并把生成的數據發送給瀏覽器
瀏覽器在接收到該含有當前用戶的頁面數據后,就可以正確展示用戶登錄的狀態信息了
Cookie 流程可以參考下圖:
HTTP請求流程圖
輸入一個URL發生了什么?
1.輸入URL,瀏覽器判斷是搜索內容還是網址,如果是搜索內容,搜索內容+默認搜索引擎地址合成新的URL,如果輸入內容符合URL規則,則拼接協議合成合法的URL
2.輸入完內容,敲下回車之后,瀏覽器導航欄呈現loading狀態,停留在前一個頁面,因為新的資源還沒獲得
3.瀏覽器進程合成請求行信息,通過IPC(進程間通信)發送給網絡進程
4.網絡進程拿到URL和相關信息,查找緩存中是否有該URL對應的資源,如果有,攔截請求,返回200和緩存的資源文件,否則,進入網絡請求階段
5.網絡進程請求DNS查找URL對應的IP,如果之前DNS緩存過這個URL的信息,就會直接返回緩存信息,否則,發起請求獲取解析出來的IP和端口號,如果沒有端口號,HTTP默認80,HTTPS默認443,如果是HTTPS,還會建立TLS連接
6.拿到了IP和端口號,接下來進行TCP連接,在正式連接之前,會檢查該域名下是否超過了6個TCP連接,如果超過,進入等待連接狀態,如果沒有超過,則發起正式連接
7.發起正式TCP連接,這個過程中,數據包會帶上TCP頭信息–包括源端口號、目的端口號和用于確保數據包順序的序列信息,向網絡層傳輸
8.網絡層給數據包頭部加上IP頭信息,向物理層傳輸
9.物理層通過物理網絡傳輸到目的主機
10.目的主機網絡層接收到數據包,解析出IP頭,剩下的數據包向上傳輸給傳輸層
11.目的主機傳輸層接收到數據包,解析出TCP頭,根據端口信息把數據傳輸給應用層
12.應用層解析出請求頭信息,如果需要重定向,返回響應數據的狀態301或者302,同時在location字段中加上重定向的地址信息,瀏覽器會根據location發起一次新的連接,如果不是重定向,服務器會根據請求頭的If-None-Match來判斷緩存資源是否需要更新,如果不需要更新,返回304,告訴瀏覽器可以用之前的緩存數據,如果有更新,帶著更新信息一起返回給瀏覽器,并在響應頭中加入字段:Cache-Control:Max-age=2000
數據又順著應用層-網絡層-傳輸層-傳輸層-網絡層-應用層返回到瀏覽器網絡進程
13.數據傳輸完成,TCP四次揮手斷開連接,但如果瀏覽器或服務器在HTTP頭中帶上了:Connection:Keep-Alive,TCP就會保持連接,不會斷開
14.網絡層獲取到數據包解析出Content-Type,如果是字節流類型,就提交給下載管理器,導航流程結束,如果是text/html類型,就通知瀏覽器進程準備進行渲染
15.瀏覽器進程收到通知,根據當前頁面和打開的新頁面是否是同一站點判斷是否新開一個渲染進程
16.確認之后,瀏覽器進程會發出**“提交文檔”信息給渲染進程,渲染進程接收到消息后,打開和網絡進程之間的管道,傳輸文檔數據,傳輸完成后,渲染進程會告訴瀏覽器進程“確認提交**”
17.瀏覽器進程收到“確認提交”消息后,更新瀏覽器的狀態,包括地址欄URL、前進后退的歷史狀態,并更新web頁面
18.渲染進程對文檔進行頁面解析和子資源加載,HTML 通過HTML解析器轉成DOM Tree(二叉樹類似結構的東西),CSS按照CSS 規則和CSS解釋器轉成CSSOM TREE,兩個tree結合,形成render tree(不包含HTML的具體元素和元素要畫的具體位置),通過Layout可以計算出每個元素具體的寬高顏色位置,結合起來,開始繪制,最后顯示在屏幕中新頁面顯示出來
JS性能優化
提升單次腳本的執行速度,避免 JavaScript 的長任務霸占主線程,這樣可以使得頁面快速響應交互;
避免大的內聯腳本,因為在解析 HTML 的過程中,解析和編譯也會占用主線程;
減少 JavaScript 文件的容量,因為更小的文件會提升下載速度,并且占用更低的內存。
消息隊列和事件循環
消息隊列和事件循環模型
從圖中可以看出:
渲染主線程負責事件循環,事件的來源為消息隊列
IO線程負責將各種事件添加到消息隊列中
對于其他進程觸發的事件,通過進程間通信告訴渲染進程中的IO線程
注意:由于是多個線程操作一個消息隊列,所以在添加任務和取出任務的時候還會加上一個同步鎖
如何安全退出
確定要退出當前頁面時,頁面主線程會設置一個退出標志的變量,在每次執行完一個任務時,判斷是否有設置退出標志
如果有退出標志,直接中斷當前的所有任務,退出線程,可以參考以下代碼:
TaskQueue task_queue; void ProcessTask(); bool keep_running = true; void MainThread(){for(;;){Task task = task_queue.takeTask();ProcessTask(task);if(!keep_running) //如果設置了退出標志,那么直接退出線程循環break; } }頁面使用單線程的缺點
頁面線程執行的任務都是來自于消息隊列,而消息隊列又要遵從“先進先出”的規則,所以有以下兩個問題需要解決:
1.如何處理高優先級任務
微任務:
一般消息隊列的任務都被稱為宏任務,每個宏任務里面都包含了一個微任務隊列,當有高優先級任務觸發時(比如DOM上的變化),該任務就會被添加到當前執行的宏任務的微任務隊列中,這樣就不會阻塞宏任務的繼續執行,提高了執行率
當宏任務執行完成之后,不會立即去執行下一個宏任務,而是先執行微任務隊列里面的任務,因為DOM變化的事件都保存在微任務隊列中,這樣就提高了實時性
2.如何解決單個任務執行時間過長的問題
因為所有的任務都是在單線程中執行的,所以一旦某個任務執行時間過長,就會阻塞后續任務的執行。
針對這種情況,JavaScript可以通過回調功能來解決這個問題,也就是讓執行時間過長的任務滯后執行
垃圾回收
JavaScript的垃圾回收是通過垃圾回收器來進行的,分為棧區的垃圾回收和堆區的垃圾回收
調用棧中的垃圾是如何回收的
當一個函數執行完后,JavaScript引擎會通過下移ESP來銷毀函數保存在調用棧中的執行上下文,可見下圖
function foo(){var a = 1var b = {name:"極客邦"}function showName(){var c = 2var d = {name:"極客時間"}}showName() } foo()堆中的垃圾是如何回收的
從上一個例子中可以知道,在showName的執行上下文被銷毀之后,堆中還存有1003和1050這兩個數據
V8中會把堆分為老生區和新生區,新生區中是生存時間短的數據,老生區中是生存時間較長的數據或占用空間較大的數據,針對這兩個去,V8使用兩個垃圾回收器來對應回收垃圾:
新生區:副垃圾回收器,老生區:主垃圾回收器
不管什么垃圾回收器,都有一套共同的流程,標記-回收-整理
回收流程:
新生區:新生區采用Scavenge算法,將新生區分為對象區域和空閑區域,新加入的對象都會存放在對象區域,在區域快要被寫滿的時候,執行垃圾回收。
首先對對象區域中的垃圾做標記,然后進入垃圾回收階段
副垃圾回收器會把清理之后存活的數據復制到空閑區,同時還會把這些數據有序排列起來,這樣操作之后,空閑區域就沒有內存碎片了
最后將對象區域和空閑區域進行角色翻轉,這樣就完成了垃圾回收
值得注意的一個點是:如果新生區設置得太大了,每次清理時間就會邊長,所以為了執行效率,新生區一般都會設置得比較小,就會很容易被寫滿,為了解決這個問題,JavaScript引擎采用了對象晉升策略:經過兩次垃圾回收還存在的對象,會晉升到老生區
老生區:由于老生區的對象比較大,若要在老生區中使用 Scavenge 算法進行垃圾回收,復制這些大的對象將會花費比較多的時間,從而導致回收執行效率不高,同時還會浪費一半的空間,所以主垃圾回收器是采用**標記 - 清除(Mark-Sweep)**的算法進行垃圾回收的
首先是標記過程階段。標記階段就是從一組根元素開始,遞歸遍歷這組根元素,在這個遍歷過程中,能到達的元素稱為活動對象,沒有到達的元素就可以判斷為垃圾數據,還是上面那個例子,關于是否可到達,可參考下圖:
接下來就是垃圾的清除過程。標記 - 整理(Mark-Compact),這個標記過程仍然與標記 - 清除算法里的是一樣的,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存
全停頓
由于 JavaScript 是運行在主線程之上的,一旦執行垃圾回收算法,都需要將正在執行的 JavaScript 腳本暫停下來,待垃圾回收完畢后再恢復腳本執行。這種行為叫做全停頓(Stop-The-World),全停頓對新生區的影響不大,因為新生區較小,存活對象也少,但對老生區的影響就比較大了,老生區存放了較大的數據,如果清理過程中耗時過長,主線程又不能做其他的事情,造成頁面卡頓就不好了
為了降低老生代的垃圾回收而造成的卡頓,V8 將標記過程分為一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成,我們把這個算法稱為增量標記(Incremental Marking)算法,如下圖:
setTimeout是如何實現的
首先,我們知道瀏覽器要執行一個任務,需要先將任務添加到消息隊列中,然后事件循環系統再按照順序執行消息隊列中的任務,先來看看一些典型的事件:
- 當接收到 HTML 文檔數據,渲染引擎就會將“解析 DOM”事件添加到消息隊列中
- 當用戶改變了 Web 頁面的窗口大小,渲染引擎就會將“重新布局”的事件添加到消息隊列中
- 當觸發了 JavaScript 引擎垃圾回收機制,渲染引擎會將“垃圾回收”任務添加到消息隊列中
- 同樣,如果要執行一段異步 JavaScript 代碼,也是需要將執行任務添加到消息隊列中
但是,設置定時器的回調需要在指定時間間隔內被調用,但是消息隊列的任務是按照順序執行的,換個說法,不能將定時器的回調函數直接放到消息隊列中
針對這個問題,Chrome中除了正常的消息隊列外,還維護了一個延遲隊列,用以維護需要延遲執行的任務
使用setTimeout的一些注意事項
首先,如果當前任務執行時間過長,會影響定時器任務的執行,比如以下代碼:
function bar() {console.log('bar') } function foo() {setTimeout(bar, 0);for (let i = 0; i < 5000; i++) {let i = 5+8+8+8console.log(i)} } foo()這段代碼中,foo函數里面設置了一個延時為0的回調任務,設置好回調之后,foo函數會執行5000次的循環
通過 setTimeout 設置的回調任務被放入了消息隊列中并且等待下一次執行,由于當前任務的執行時間較長,一定會影響到下一次任務的執行時間
延時執行時間有最大值
Chrome、Safari、Firefox 都是以 32 個 bit 來存儲延時值的,32bit 最大只能存放的數字是 2147483647 毫秒,這就意味著,如果 setTimeout 設置的延遲值大于 2147483647 毫秒(大約 24.8 天)時就會溢出,那么相當于延時值被設置為 0 了,這導致定時器會被立即執行
使用 setTimeout 設置的回調函數中的 this 不符合直覺
如果被 setTimeout 推遲執行的回調函數是某個對象的方法,那么該方法中的 this 關鍵字將指向全局環境,而不是定義時所在的那個對象,可以看看這段代碼:
var name= 1; var MyObj = {name: 2,showName: function(){console.log(this.name);} } setTimeout(MyObj.showName,1000)這里輸出的是 1,因為這段代碼在編譯的時候,執行上下文中的 this 會被設置為全局 window,如果是嚴格模式,會被設置為 undefined
解決辦法:
1.將MyObj.showName放在匿名函數中執行
//箭頭函數 setTimeout(() => {MyObj.showName() }, 1000); //或者function函數 setTimeout(function() {MyObj.showName(); }, 1000)2.使用 bind 方法,將 showName 綁定在 MyObj 上面
setTimeout(MyObj.showName.bind(MyObj), 1000)宏任務和微任務
隨著瀏覽器的應用領域越來越廣泛,消息隊列中的粗時間顆粒度的任務已經不能勝任部分領域的需求,所以又出現了一種新的技術——微任務,微任務可以在實時性和效率之間做一個有效的權衡
宏任務
頁面中的大部分任務都是在主線程上執行的,這些任務包括了:
- 渲染事件(如解析 DOM、計算布局、繪制)
- 用戶交互事件(如鼠標點擊、滾動頁面、放大縮小等)
- JavaScript 腳本執行事件
- 網絡請求完成、文件讀寫完成事件
為了協調這些任務有條不紊地在主線程上執行,頁面進程引入了消息隊列和事件循環機制,渲染進程內部會維護多個消息隊列,比如延遲執行隊列和普通的消息隊列。然后主線程采用一個 for 循環,不斷地從這些任務隊列中取出任務并執行任務。我們把這些消息隊列中的任務稱為宏任務
宏任務可以滿足我們日常中的很大部分需求,但如果遇到對時間精度要求很高的需求時,宏任務就頂不住了因為:
頁面的渲染事件、各種 IO 的完成事件、執行 JavaScript 腳本的事件、用戶交互的事件等都隨時有可能被添加到消息隊列中,而且添加事件是由系統操作的,JavaScript 代碼不能準確掌控任務要添加到隊列中的位置,控制不了任務在消息隊列中的位置,所以很難控制開始執行任務的時間
所以宏任務的時間粒度比較大,執行時間是不能精準控制的
微任務
了解什么是微任務之前,先了解一下異步回調的兩種主要方式:
那么微任務到底是什么呢?
微任務就是一個需要異步執行的函數,執行時機是在主函數執行結束之后、當前宏任務結束之前
我們知道,當執行一段JavaScript腳本的時候,V8會為其創建一個全局執行上下文,在這同時V8也會創建一個微任務隊列,不過這個微任務隊列是V8內部訪問的,所以無法通過JavaScript訪問到,也就是說每個宏任務都關聯了一個微任務隊列
微任務是怎么產生的
主要有兩種方式
這些微任務都會被JavaScript引擎按照執行順序保存到微任務隊列中
微任務是什么時候執行的
在當前宏任務中的 JavaScript 快執行完成時,也就在 JavaScript 引擎準備退出全局執行上下文并清空調用棧的時候,JavaScript 引擎會檢查全局執行上下文中的微任務隊列,然后按照順序執行隊列中的微任務。WHATWG 把執行微任務的時間點稱為檢查點
如果在執行微任務的過程中,產生了新的微任務,同樣會將該微任務添加到微任務隊列中,V8 引擎一直循環執行微任務隊列中的任務,直到隊列為空才算執行結束。也就是說在執行微任務過程中產生的新的微任務并不會推遲到下個宏任務中執行,而是在當前的宏任務中繼續執行
結合下圖進行理解:
該示意圖是在執行一個 ParseHTML 的宏任務,在執行過程中,遇到了 JavaScript 腳本,那么就暫停解析流程,進入到 JavaScript 的執行環境。從圖中可以看到,全局上下文中包含了微任務列表
由上文可以得出幾個結論:
HTTP1\HTTP2\HTTP3
HTTP/0.9
HTTP/0.9主要用來實現小體積的html文件傳輸,實現上有以下三個特點:
- 第一個是只有一個請求行,并沒有 HTTP 請求頭和請求體,因為只需要一個請求行就可以完整表達客戶端的需求了
- 第二個是服務器也沒有返回頭信息,這是因為服務器端并不需要告訴客戶端太多信息,只需要返回數據就可以了
- 第三個是返回的文件內容是以 ASCII 字符流來傳輸的,因為都是 HTML 格式的文件,所以使用 ASCII 字節碼來傳輸是最合適的
HTTP/1.0
HTTP/1.0 除了對多文件提供良好的支持外,還依據當時實際的需求引入了很多其他的特性,這些特性都是通過請求頭和響應頭來實現的。
下面我們來看看新增的幾個典型的特性:
- 有的請求服務器可能無法處理,或者處理出錯,這時候就需要告訴瀏覽器服務器最終處理該請求的情況,這就引入了狀態碼。狀態碼是通過響應行的方式來通知瀏覽器的。
- 為了減輕服務器的壓力,在 HTTP/1.0 中提供了 Cache 機制,用來緩存已經下載過的數據。
- 服務器需要統計客戶端的基礎信息,比如 Windows 和 macOS 的用戶數量分別是多少,所以 HTTP/1.0 的請求頭中還加入了用戶代理的字段。
HTTP/1.1
對比HTTP/1.0 遇到了哪些主要的問題,來看看HTTP/1.1 是如何改進的
1.改進持久連接
HTTP/1.0 每進行一次 HTTP 通信,都需要經歷建立 TCP 連接、傳輸 HTTP 數據和斷開 TCP 連接三個階段,在如今的需求中,頁面需要下載大量的外部資源文件,每次都進行TCP連接、傳輸、斷開,無疑是增大開銷
為了解決這個問題,HTTP/1.1 中增加了持久連接的方法,它的特點是在一個 TCP 連接上可以傳輸多個 HTTP 請求,只要瀏覽器或者服務器沒有明確斷開連接,那么該 TCP 連接會一直保持
持久連接在 HTTP/1.1 中是默認開啟的,所以你不需要專門為了持久連接去 HTTP 請求頭設置信息,如果你不想要采用持久連接,可以在 HTTP 請求頭中加上Connection: close
2.不成熟的 HTTP 管線化
持久連接雖然可以減少TCP多次連接、傳輸、斷開的次數,但也產生了一個問題,單詞連接傳輸過程中,需要等待前面的請求返回之后才可以進行下一次請求,如果某個請求耗時很長,就會阻塞后面的所有請求,這就是隊頭阻塞問題
HTTP/1.1 中試圖通過管線化的技術來解決隊頭阻塞的問題。HTTP/1.1 中的管線化是指將多個 HTTP 請求整批提交給服務器的技術,雖然可以整批發送請求,不過服務器依然需要根據請求順序來回復瀏覽器的請求,FireFox、Chrome 都做過管線化的試驗,但是由于各種原因,它們最終都放棄了管線化技術
3.提供虛擬主機的支持
在 HTTP/1.0 中,每個域名綁定了一個唯一的 IP 地址,因此一個服務器只能支持一個域名。但是隨著虛擬主機技術的發展,需要實現在一臺物理主機上綁定多個虛擬主機,每個虛擬主機都有自己的單獨的域名,這些單獨的域名都公用同一個 IP 地址。
因此,HTTP/1.1 的請求頭中增加了 Host 字段,用來表示當前的域名地址,這樣服務器就可以根據不同的 Host 值做不同的處理
4.對動態生成的內容提供了完美支持
在設計 HTTP/1.0 時,需要在響應頭中設置完整的數據大小,如Content-Length: 901,這樣瀏覽器就可以根據設置的數據大小來接收數據。不過隨著服務器端的技術發展,很多頁面的內容都是動態生成的,因此在傳輸數據之前并不知道最終的數據大小,這就導致了瀏覽器不知道何時會接收完所有的文件數據。
HTTP/1.1 通過引入 Chunk transfer 機制來解決這個問題,服務器會將數據分割成若干個任意大小的數據塊,每個數據塊發送時會附上上個數據塊的長度,最后使用一個零長度的塊作為發送數據完成的標志。這樣就提供了對動態內容的支持
5.客戶端 Cookie、安全機制
除此之外,HTTP/1.1 還引入了客戶端 Cookie 機制和安全機制
HTTP/2
雖然HTTP/1.1已經做了大量優化,但依然有很多瓶頸,由此來說說HTTP/2
首先,我們知道 HTTP/1.1 為網絡效率做了大量的優化,最核心的有如下三種方式:
- 增加了持久連接
- 瀏覽器為每個域名最多同時維護 6 個 TCP 持久連接
- 使用 CDN 的實現域名分片機制
影響HTTP/1.1效率的三個因素:TCP 的慢啟動、多條 TCP 連接競爭帶寬和隊頭阻塞
針對這些問題,HTTP/2通過多路復用機制解決了這些問題
多路復用是通過在協議棧中添加二進制分幀層來實現的,有了二進制分幀層還能夠實現請求的優先級、服務器推送、頭部壓縮等特性,從而大大提升了文件傳輸效率
雖然 HTTP/2 引入了二進制分幀層,不過 HTTP/2 的語義和 HTTP/1.1 依然是一樣的,也就是說它們通信的語言并沒有改變,比如開發者依然可以通過 Accept 請求頭告訴服務器希望接收到什么類型的文件,依然可以使用 Cookie 來保持登錄狀態,依然可以使用 Cache 來緩存本地文件,這些都沒有變,發生改變的只是傳輸方式
HTTP/3
HTTP/2依然有一些缺陷:
TCP的隊頭阻塞
首先看看什么是TCP隊頭阻塞,可以看看下圖:
在 TCP 傳輸過程中,由于單個數據包的丟失而造成的阻塞稱為 TCP 上的隊頭阻塞
那么他是如何影響到HTTP/2的多路復用的呢?
首先看看正常情況下的多路復用:
HTTP/2 中,多個請求是跑在一個 TCP 管道中的,如果其中任意一路數據流中出現了丟包的情況,那么就會阻塞該 TCP 連接中的所有請求。這不同于 HTTP/1.1,使用 HTTP/1.1 時,瀏覽器為每個域名開啟了 6 個 TCP 連接,如果其中的 1 個 TCP 連接發生了隊頭阻塞,那么其他的 5 個連接依然可以繼續傳輸數據。
所以隨著丟包率的增加,HTTP/2 的傳輸效率也會越來越差。有測試數據表明,當系統達到了 2% 的丟包率時,HTTP/1.1 的傳輸效率反而比 HTTP/2 表現得更好
TCP 建立連接的延時
HTTP/1 和 HTTP/2 都是使用 TCP 協議來傳輸的,而如果使用 HTTPS 的話,還需要使用 TLS 協議進行安全傳輸,而使用 TLS 也需要一個握手過程,這樣就需要有兩個握手延遲過程。
在建立 TCP 連接的時候,需要和服務器進行三次握手來確認連接成功,也就是說需要在消耗完 1.5 個 RTT 之后才能進行數據傳輸。
進行 TLS 連接,TLS 有兩個版本——TLS1.2 和 TLS1.3,每個版本建立連接所花的時間不同,大致是需要 1~2 個 RTT
總之,在傳輸數據之前,我們需要花掉 3~4 個 RTT。如果瀏覽器和服務器的物理距離較近,那么 1 個 RTT 的時間可能在 10 毫秒以內,也就是說總共要消耗掉 30~40 毫秒。這個時間也許用戶還可以接受,但如果服務器相隔較遠,那么 1 個 RTT 就可能需要 100 毫秒以上了,這種情況下整個握手過程需要 300~400 毫秒,這時用戶就能明顯地感受到“慢”了
TCP 協議僵化
總之就是TCP 協議存在隊頭阻塞和建立連接延遲等缺點,很難通過改進TCP來解決這些問題
QUIC協議
QUIC協議是基于 UDP 實現了類似于 TCP 的多路數據流、傳輸可靠性等功能的一個協議
通過上圖可以看出,HTTP/3 中的 QUIC 協議集合了以下幾點功能:
- 實現了類似 TCP 的流量控制、傳輸可靠性的功能。雖然 UDP 不提供可靠性的傳輸,但 QUIC 在 UDP 的基礎之上增加了一層來保證數據可靠性傳輸。它提供了數據包重傳、擁塞控制以及其他一些 TCP 中存在的特性。
- 集成了 TLS 加密功能。目前 QUIC 使用的是 TLS1.3,相較于早期版本 TLS1.3 有更多的優點,其中最重要的一點是減少了握手所花費的 RTT 個數。
- 實現了 HTTP/2 中的多路復用功能。和 TCP 不同,QUIC 實現了在同一物理連接上可以有多個獨立的邏輯數據流(如下圖)。實現了數據流的單獨傳輸,就解決了 TCP 中隊頭阻塞的問題,QUIC 協議的多路復用:
- 實現了快速握手功能。由于 QUIC 是基于 UDP 的,所以 QUIC 可以實現使用 0-RTT 或者 1-RTT 來建立連接,這意味著 QUIC 可以用最快的速度來發送和接收數據,這樣可以大大提升首次打開頁面的速度
總結
以上是學習李兵老師的《瀏覽器工作原理與實踐》記錄下的筆記,詳細學習:
《瀏覽器工作原理與實踐》
總結
以上是生活随笔為你收集整理的《浏览器工作原理与实践》学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。