Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 美女又黄又免费的视频,аⅴ天堂一区视频在线观看,成人在线观看国产

          整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          瀏覽器的工作原理你真的全知道嘛?史上最全圖解其原理

          瀏覽器的工作原理你真的全知道嘛?史上最全圖解其原理

          能每一個前端工程師都想要理解瀏覽器的工作原理。

          我們希望知道從在瀏覽器地址欄中輸入 url 到頁面展現(xiàn)的短短幾秒內(nèi)瀏覽器究竟做了什么;

          我們希望了解平時常常聽說的各種代碼優(yōu)化方案是究竟為什么能起到優(yōu)化的作用;

          我們希望更細(xì)致地了解瀏覽器的渲染流程。

          瀏覽器的多進(jìn)程架構(gòu)

          一個好的程序常常被劃分為幾個相互獨立又彼此配合的模塊,瀏覽器也是如此,以 Chrome 為例,它由多個進(jìn)程組成,每個進(jìn)程都有自己核心的職責(zé),它們相互配合完成瀏覽器的整體功能,每個進(jìn)程中又包含多個線程,一個進(jìn)程內(nèi)的多個線程也會協(xié)同工作,配合完成所在進(jìn)程的職責(zé)。

          對一些前端開發(fā)同學(xué)來說,進(jìn)程和線程的概念可能會有些模糊,為了更好的理解瀏覽器的多進(jìn)程架構(gòu),這里我們簡單討論一下進(jìn)程和線程。

          進(jìn)程(process)和線程(thread)

          進(jìn)程就像是一個有邊界的生產(chǎn)廠間,而線程就像是廠間內(nèi)的一個個員工,可以自己做自己的事情,也可以相互配合做同一件事情。

          當(dāng)我們啟動一個應(yīng)用,計算機會創(chuàng)建一個進(jìn)程,操作系統(tǒng)會為進(jìn)程分配一部分內(nèi)存,應(yīng)用的所有狀態(tài)都會保存在這塊內(nèi)存中,應(yīng)用也許還會創(chuàng)建多個線程來輔助工作,這些線程可以共享這部分內(nèi)存中的數(shù)據(jù)。如果應(yīng)用關(guān)閉,進(jìn)程會被終結(jié),操作系統(tǒng)會釋放相關(guān)內(nèi)存。更生動的示意圖如下:

          一個進(jìn)程還可以要求操作系統(tǒng)生成另一個進(jìn)程來執(zhí)行不同的任務(wù),系統(tǒng)會為新的進(jìn)程分配獨立的內(nèi)存,兩個進(jìn)程之間可以使用 IPC (Inter Process Communication)進(jìn)行通信。很多應(yīng)用都會采用這樣的設(shè)計,如果一個工作進(jìn)程反應(yīng)遲鈍,重啟這個進(jìn)程不會影響應(yīng)用其它進(jìn)程的工作。

          如果對進(jìn)程及線程的理解還存在疑惑,可以參考下述文章:

          http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html

          瀏覽器的架構(gòu)

          有了上面的知識做鋪墊,我們可以更合理的討論瀏覽器的架構(gòu)了,其實如果要開發(fā)一個瀏覽器,它可以是單進(jìn)程多線程的應(yīng)用,也可以是使用 IPC 通信的多進(jìn)程應(yīng)用。

          不同瀏覽器的架構(gòu)模型

          不同瀏覽器采用了不同的架構(gòu)模式,這里并不存在標(biāo)準(zhǔn),本文以 Chrome 為例進(jìn)行說明 :

          Chrome 采用多進(jìn)程架構(gòu),其頂層存在一個 Browser process 用以協(xié)調(diào)瀏覽器的其它進(jìn)程。

          Chrome 的不同進(jìn)程

          具體說來,Chrome 的主要進(jìn)程及其職責(zé)如下:

          Browser Process:

          • 負(fù)責(zé)包括地址欄,書簽欄,前進(jìn)后退按鈕等部分的工作;
          • 負(fù)責(zé)處理瀏覽器的一些不可見的底層操作,比如網(wǎng)絡(luò)請求和文件訪問;

          Renderer Process:

          • 負(fù)責(zé)一個 tab 內(nèi)關(guān)于網(wǎng)頁呈現(xiàn)的所有事情

          Plugin Process:

          • 負(fù)責(zé)控制一個網(wǎng)頁用到的所有插件,如 flashGPU Process
          • 負(fù)責(zé)處理 GPU 相關(guān)的任務(wù)

          不同進(jìn)程負(fù)責(zé)的瀏覽器區(qū)域示意圖

          Chrome 還為我們提供了「任務(wù)管理器」,供我們方便的查看當(dāng)前瀏覽器中運行的所有進(jìn)程及每個進(jìn)程占用的系統(tǒng)資源,右鍵單擊還可以查看更多類別信息。

          通過「頁面右上角的三個點點點 --- 更多工具 --- 任務(wù)管理器」即可打開相關(guān)面板。

          Chrome 多進(jìn)程架構(gòu)的優(yōu)缺點

          優(yōu)點

          某一渲染進(jìn)程出問題不會影響其他進(jìn)程更為安全,在系統(tǒng)層面上限定了不同進(jìn)程的權(quán)限

          缺點

          由于不同進(jìn)程間的內(nèi)存不共享,不同進(jìn)程的內(nèi)存常常需要包含相同的內(nèi)容。

          為了節(jié)省內(nèi)存,Chrome 限制了最多的進(jìn)程數(shù),最大進(jìn)程數(shù)量由設(shè)備的內(nèi)存和 CPU 能力決定,當(dāng)達(dá)到這一限制時,新打開的 Tab 會共用之前同一個站點的渲染進(jìn)程。

          測試了一下在 Chrome 中打開不斷打開知乎首頁,在 Mac i5 8g 上可以啟動四十多個渲染進(jìn)程,之后新打開 tab 會合并到已有的渲染進(jìn)程中。

          Chrome 把瀏覽器不同程序的功能看做服務(wù),這些服務(wù)可以方便的分割為不同的進(jìn)程或者合并為一個進(jìn)程。以 Broswer Process 為例,如果 Chrome 運行在強大的硬件上,它會分割不同的服務(wù)到不同的進(jìn)程,這樣 Chrome 整體的運行會更加穩(wěn)定,但是如果 Chrome 運行在資源貧瘠的設(shè)備上,這些服務(wù)又會合并到同一個進(jìn)程中運行,這樣可以節(jié)省內(nèi)存。

          iframe 的渲染 -- Site Isolation

          在上面的進(jìn)程圖中我們還可以看到一些進(jìn)程下還存在著 Subframe,這就是 Site Isolation 機制作用的結(jié)果。

          Site Isolation 機制從 Chrome 67 開始默認(rèn)啟用。這種機制允許在同一個 Tab 下的跨站 iframe 使用單獨的進(jìn)程來渲染,這樣會更為安全。


          iframe 會采用不同的渲染進(jìn)程

          Site Isolation 被大家看做里程碑式的功能, 其成功實現(xiàn)是多年工程努力的結(jié)果。Site Isolation 不是簡單的疊加多個進(jìn)程。這種機制在底層改變了 iframe 之間通信的方法,Chrome 的其它功能都需要做對應(yīng)的調(diào)整,比如說 devtools 需要相應(yīng)的支持,甚至 Ctrl + F 也需要支持。關(guān)于 Site Isolation 的更多內(nèi)容可參考下述鏈接:

          https://developers.google.com/web/updates/2018/07/site-isolation

          介紹完了瀏覽器的基本架構(gòu)模式,接下來我們看看一個常見的導(dǎo)航過程對瀏覽器來說究竟發(fā)生了什么。

          導(dǎo)航過程發(fā)生了什么

          也許大多數(shù)人使用 Chrome 最多的場景就是在地址欄輸入關(guān)鍵字進(jìn)行搜索或者輸入地址導(dǎo)航到某個網(wǎng)站,我們來看看瀏覽器是怎么看待這個過程的。

          我們知道瀏覽器 Tab 外的工作主要由 Browser Process 掌控,Browser Process 又對這些工作進(jìn)一步劃分,使用不同線程進(jìn)行處理:

          • UI thread : 控制瀏覽器上的按鈕及輸入框;
          • network thread: 處理網(wǎng)絡(luò)請求,從網(wǎng)上獲取數(shù)據(jù);
          • storage thread: 控制文件等的訪問;

          瀏覽器主進(jìn)程中的不同線程

          回到我們的問題,當(dāng)我們在瀏覽器地址欄中輸入文字,并點擊回車獲得頁面內(nèi)容的過程在瀏覽器看來可以分為以下幾步:

          1. 處理輸入

          UI thread 需要判斷用戶輸入的是 URL 還是 query;

          1. 開始導(dǎo)航

          當(dāng)用戶點擊回車鍵,UI thread 通知 network thread 獲取網(wǎng)頁內(nèi)容,并控制 tab 上的 spinner 展現(xiàn),表示正在加載中。

          network thread 會執(zhí)行 DNS 查詢,隨后為請求建立 TLS 連接。

          UI thread 通知 Network thread 加載相關(guān)信息

          如果 network thread 接收到了重定向請求頭如 301,network thread 會通知 UI thread 服務(wù)器要求重定向,之后,另外一個 URL 請求會被觸發(fā)。

          1. 讀取響應(yīng)

          當(dāng)請求響應(yīng)返回的時候,network thread 會依據(jù) Content-Type 及 MIME Type sniffing 判斷響應(yīng)內(nèi)容的格式。

          判斷響應(yīng)內(nèi)容的格式

          如果響應(yīng)內(nèi)容的格式是 HTML ,下一步將會把這些數(shù)據(jù)傳遞給 renderer process,如果是 zip 文件或者其它文件,會把相關(guān)數(shù)據(jù)傳輸給下載管理器。

          Safe Browsing 檢查也會在此時觸發(fā),如果域名或者請求內(nèi)容匹配到已知的惡意站點,network thread 會展示一個警告頁。此外 CORB 檢測也會觸發(fā)確保敏感數(shù)據(jù)不會被傳遞給渲染進(jìn)程。

          1. 查找渲染進(jìn)程

          當(dāng)上述所有檢查完成,network thread 確信瀏覽器可以導(dǎo)航到請求網(wǎng)頁,network thread 會通知 UI thread 數(shù)據(jù)已經(jīng)準(zhǔn)備好,UI thread 會查找到一個 renderer process 進(jìn)行網(wǎng)頁的渲染。

          收到 Network thread 返回的數(shù)據(jù)后,UI thread 查找相關(guān)的渲染進(jìn)程由于網(wǎng)絡(luò)請求獲取響應(yīng)需要時間,這里其實還存在著一個加速方案。當(dāng) UI thread 發(fā)送 URL 請求給 network thread 時,瀏覽器其實已經(jīng)知道了將要導(dǎo)航到那個站點。UI thread 會并行的預(yù)先查找和啟動一個渲染進(jìn)程,如果一切正常,當(dāng) network thread 接收到數(shù)據(jù)時,渲染進(jìn)程已經(jīng)準(zhǔn)備就緒了,但是如果遇到重定向,準(zhǔn)備好的渲染進(jìn)程也許就不可用了,這時候就需要重啟一個新的渲染進(jìn)程。

          1. 確認(rèn)導(dǎo)航

          進(jìn)過了上述過程,數(shù)據(jù)以及渲染進(jìn)程都可用了, Browser Process 會給 renderer process 發(fā)送 IPC 消息來確認(rèn)導(dǎo)航,一旦 Browser Process 收到 renderer process 的渲染確認(rèn)消息,導(dǎo)航過程結(jié)束,頁面加載過程開始。

          此時,地址欄會更新,展示出新頁面的網(wǎng)頁信息。history tab 會更新,可通過返回鍵返回導(dǎo)航來的頁面,為了讓關(guān)閉 tab 或者窗口后便于恢復(fù),這些信息會存放在硬盤中。

          1. 額外的步驟

          一旦導(dǎo)航被確認(rèn),renderer process 會使用相關(guān)的資源渲染頁面,下文中我們將重點介紹渲染流程。當(dāng) renderer process 渲染結(jié)束(渲染結(jié)束意味著該頁面內(nèi)的所有的頁面,包括所有 iframe 都觸發(fā)了 onload 時),會發(fā)送 IPC 信號到 Browser process, UI thread 會停止展示 tab 中的 spinner。

          Renderer Process 發(fā)送 IPC 消息通知 browser process 頁面已經(jīng)加載完成

          當(dāng)然上面的流程只是網(wǎng)頁首幀渲染完成,在此之后,客戶端依舊可下載額外的資源渲染出新的視圖。

          在這里我們可以明確一點,所有的 JS 代碼其實都由 renderer Process 控制的,所以在你瀏覽網(wǎng)頁內(nèi)容的過程大部分時候不會涉及到其它的進(jìn)程。不過也許你也曾經(jīng)監(jiān)聽過 beforeunload 事件,這個事件再次涉及到 Browser Process 和 renderer Process 的交互,當(dāng)當(dāng)前頁面關(guān)閉時(關(guān)閉 Tab ,刷新等等),Browser Process 需要通知 renderer Process 進(jìn)行相關(guān)的檢查,對相關(guān)事件進(jìn)行處理。

          瀏覽器進(jìn)程發(fā)送 IPC 消息給渲染進(jìn)程,通知要離開當(dāng)前網(wǎng)站了如果導(dǎo)航由 renderer process 觸發(fā)(比如在用戶點擊某鏈接,或者 JS 執(zhí)行 window.location="http://newsite.com" ) renderer process 會首先檢查是否有 beforeunload 事件處理器,導(dǎo)航請求由 renderer process 傳遞給 Browser process。

          如果導(dǎo)航到新的網(wǎng)站,會啟用一個新的 render process 來處理新頁面的渲染,老的進(jìn)程會留下來處理類似 unload 等事件。

          關(guān)于頁面的生命周期,更多內(nèi)容可參考 Page Lifecycle API 。

          瀏覽器進(jìn)程發(fā)送 IPC 消息到新的渲染進(jìn)程通知渲染新的頁面,同時通知舊的渲染進(jìn)程卸載

          除了上述流程,有些頁面還擁有 Service Worker (服務(wù)工作線程),Service Worker 讓開發(fā)者對本地緩存及判斷何時從網(wǎng)絡(luò)上獲取信息有了更多的控制權(quán),如果 Service Worker 被設(shè)置為從本地 cache 中加載數(shù)據(jù),那么就沒有必要從網(wǎng)上獲取更多數(shù)據(jù)了。

          值得注意的是 service worker 也是運行在渲染進(jìn)程中的 JS 代碼,因此對于擁有 Service Worker 的頁面,上述流程有些許的不同。

          當(dāng)有 Service Worker 被注冊時,其作用域會被保存,當(dāng)有導(dǎo)航時,network thread 會在注冊過的 Service Worker 的作用域中檢查相關(guān)域名,如果存在對應(yīng)的 Service worker,UI thread 會找到一個 renderer process 來處理相關(guān)代碼,Service Worker 可能會從 cache 中加載數(shù)據(jù),從而終止對網(wǎng)絡(luò)的請求,也可能從網(wǎng)上請求新的數(shù)據(jù)。

          Service Worker 依據(jù)具體情形做處理

          關(guān)于 Service Worker 的更多內(nèi)容可參考:

          https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle

          如果 Service Worker 最終決定通過網(wǎng)上獲取數(shù)據(jù),Browser 進(jìn)程 和 renderer 進(jìn)程的交互其實會延后數(shù)據(jù)的請求時間 。Navigation Preload 是一種與 Service Worker 并行的加速加載資源的機制,服務(wù)端通過請求頭可以識別這類請求,而做出相應(yīng)的處理。

          渲染進(jìn)程幾乎負(fù)責(zé) Tab 內(nèi)的所有事情,渲染進(jìn)程的核心目的在于轉(zhuǎn)換 HTML CSS JS 為用戶可交互的 web 頁面。渲染進(jìn)程中主要包含以下線程:

          渲染進(jìn)程包含的線程

          1. 主線程 Main thread
          2. 工作線程 Worker thread
          3. 排版線程 Compositor thread
          4. 光柵線程 Raster thread

          后文我們將逐步介紹不同線程的職責(zé),在此之前我們先看看渲染的流程。

          1. 構(gòu)建 DOM

          當(dāng)渲染進(jìn)程接收到導(dǎo)航的確認(rèn)信息,開始接受 HTML 數(shù)據(jù)時,主線程會解析文本字符串為 DOM。

          渲染 html 為 DOM 的方法由 HTML Standard 定義。

          1. 加載次級的資源

          網(wǎng)頁中常常包含諸如圖片,CSS,JS 等額外的資源,這些資源需要從網(wǎng)絡(luò)上或者 cache 中獲取。主進(jìn)程可以在構(gòu)建 DOM 的過程中會逐一請求它們,為了加速 preload scanner 會同時運行,如果在 html 中存在 <img> <link> 等標(biāo)簽,preload scanner 會把這些請求傳遞給 Browser process 中的 network thread 進(jìn)行相關(guān)資源的下載。

          3.JS 的下載與執(zhí)行

          當(dāng)遇到 <script> 標(biāo)簽時,渲染進(jìn)程會停止解析 HTML,而去加載,解析和執(zhí)行 JS 代碼,停止解析 html 的原因在于 JS 可能會改變 DOM 的結(jié)構(gòu)(使用諸如 documwnt.write()等 API)。

          不過開發(fā)者其實也有多種方式來告知瀏覽器應(yīng)對如何應(yīng)對某個資源,比如說如果在<script> 標(biāo)簽上添加了 async 或 defer 等屬性,瀏覽器會異步的加載和執(zhí)行 JS 代碼,而不會阻塞渲染。更多的方法可參考 Resource Prioritization – Getting the Browser to Help You。

          1. 樣式計算

          僅僅渲染 DOM 還不足以獲知頁面的具體樣式,主進(jìn)程還會基于 CSS 選擇器解析 CSS 獲取每一個節(jié)點的最終的計算樣式值。即使不提供任何 CSS,瀏覽器對每個元素也會有一個默認(rèn)的樣式。

          渲染進(jìn)程主線程計算每一個元素節(jié)點的最終樣式值

          1. 獲取布局

          想要渲染一個完整的頁面,除了獲知每個節(jié)點的具體樣式,還需要獲知每一個節(jié)點在頁面上的位置,布局其實是找到所有元素的幾何關(guān)系的過程。其具體過程如下:

          通過遍歷 DOM 及相關(guān)元素的計算樣式,主線程會構(gòu)建出包含每個元素的坐標(biāo)信息及盒子大小的布局樹。布局樹和 DOM 樹類似,但是其中只包含頁面可見的元素,如果一個元素設(shè)置了 display:none ,這個元素不會出現(xiàn)在布局樹上,偽元素雖然在 DOM 樹上不可見,但是在布局樹上是可見的。

          1. 繪制各元素

          即使知道了不同元素的位置及樣式信息,我們還需要知道不同元素的繪制先后順序才能正確繪制出整個頁面。在繪制階段,主線程會遍歷布局樹以創(chuàng)建繪制記錄。繪制記錄可以看做是記錄各元素繪制先后順序的筆記。

          主線程依據(jù)布局樹構(gòu)建繪制記錄

          1. 合成幀

          熟悉 PS 等繪圖軟件的童鞋肯定對圖層這一概念不陌生,現(xiàn)代 Chrome 其實利用了這一概念來組合不同的層。

          復(fù)合是一種分割頁面為不同的層,并單獨柵格化,隨后組合為幀的技術(shù)。不同層的組合由 compositor 線程(合成器線程)完成。

          主線程會遍歷布局樹來創(chuàng)建層樹(layer tree),添加了 will-change CSS 屬性的元素,會被看做單獨的一層。

          主線程遍歷布局樹生成層樹

          你可能會想給每一個元素都添加上 will-change,不過組合過多的層也許會比在每一幀都柵格化頁面中的某些小部分更慢。為了更合理的使用層,可參考 堅持僅合成器的屬性和管理層計數(shù) 。

          一旦層樹被創(chuàng)建,渲染順序被確定,主線程會把這些信息通知給合成器線程,合成器線程會柵格化每一層。有的層的可以達(dá)到整個頁面的大小,因此,合成器線程將它們分成多個磁貼,并將每個磁貼發(fā)送到柵格線程,柵格線程會柵格化每一個磁貼并存儲在 GPU 顯存中。

          柵格線程會柵格化每一個磁貼并存儲在 GPU 顯存中

          一旦磁貼被光柵化,合成器線程會收集稱為繪制四邊形的磁貼信息以創(chuàng)建合成幀。

          合成幀隨后會通過 IPC 消息傳遞給瀏覽器進(jìn)程,由于瀏覽器的 UI 改變或者其它拓展的渲染進(jìn)程也可以添加合成幀,這些合成幀會被傳遞給 GPU 用以展示在屏幕上,如果滾動發(fā)生,合成器線程會創(chuàng)建另一個合成幀發(fā)送給 GPU。

          合成器線程會發(fā)送合成幀給 GPU 渲染

          合成器的優(yōu)點在于,其工作無關(guān)主線程,合成器線程不需要等待樣式計算或者 JS 執(zhí)行,這就是為什么合成器相關(guān)的動畫 最流暢,如果某個動畫涉及到布局或者繪制的調(diào)整,就會涉及到主線程的重新計算,自然會慢很多。

          瀏覽器對事件的處理

          瀏覽器通過對不同事件的處理來滿足各種交互需求,這一部分我們一起看看從瀏覽器的視角,事件是什么,在此我們先主要考慮鼠標(biāo)事件。

          在瀏覽器的看來,用戶的所有手勢都是輸入,鼠標(biāo)滾動,懸置,點擊等等都是。當(dāng)用戶在屏幕上觸發(fā)諸如 touch 等手勢時,首先收到手勢信息的是 Browser process, 不過 Browser process 只會感知到在哪里發(fā)生了手勢,對 tab 內(nèi)內(nèi)容的處理是還是由渲染進(jìn)程控制的。

          事件發(fā)生時,瀏覽器進(jìn)程會發(fā)送事件類型及相應(yīng)的坐標(biāo)給渲染進(jìn)程,渲染進(jìn)程隨后找到事件對象并執(zhí)行所有綁定在其上的相關(guān)事件處理函數(shù)。

          事件從瀏覽器進(jìn)程傳送給渲染進(jìn)程

          前文中,我們提到過合成器可以獨立于主線程之外通過合成柵格化層平滑的處理滾動。如果頁面中沒有綁定相關(guān)事件,組合器線程可以獨立于主線程創(chuàng)建組合幀。如果頁面綁定了相關(guān)事件處理器,主線程就不得不出來工作了。這時候合成器線程會怎么處理呢?

          這里涉及到一個專業(yè)名詞「理解非快速滾動區(qū)域(non-fast scrollable region)」由于執(zhí)行 JS 是主線程的工作,當(dāng)頁面合成時,合成器線程會標(biāo)記頁面中綁定有事件處理器的區(qū)域為 non-fast scrollable region ,如果存在這個標(biāo)注,合成器線程會把發(fā)生在此處的事件發(fā)送給主線程,如果事件不是發(fā)生在這些區(qū)域,合成器線程則會直接合成新的幀而不用等到主線程的響應(yīng)。

          涉及 non-fast scrollable region 的事件,合成器線程會通知主線程進(jìn)行相關(guān)處理

          web 開發(fā)中常用的事件處理模式是事件委托,基于事件冒泡,我們常常在最頂層綁定事件:

           document.body.addEventListener('touchstart', event=> {
               if (event.target===area) {
                   event.preventDefault();
               }
           });

          上述做法很常見,但是如果從瀏覽器的角度看,整個頁面都成了 non-fast scrollable region 了。

          這意味著即使操作的是頁面無綁定事件處理器的區(qū)域,每次輸入時,合成器線程也需要和主線程通信并等待反饋,流暢的合成器獨立處理合成幀的模式就失效了。

          由于事件綁定在最頂部,整個頁面都成為了 non-fast scrollable region

          為了防止這種情況,我們可以為事件處理器傳遞 passive: true 做為參數(shù),這樣寫就能讓瀏覽器即監(jiān)聽相關(guān)事件,又讓組合器線程在等等主線程響應(yīng)前構(gòu)建新的組合幀。

           document.body.addEventListener('touchstart', event=> {
               if (event.target===area) {
                   event.preventDefault()
               }
            }, {passive: true});

          不過上述寫法可能又會帶來另外一個問題,假設(shè)某個區(qū)域你只想要水平滾動,使用passive: true 可以實現(xiàn)平滑滾動,但是垂直方向的滾動可能會先于event.preventDefault()發(fā)生,此時可以通過 event.cancelable 來防止這種情況。

           document.body.addEventListener('pointermove', event=> {
               if (event.cancelable) {
                   event.preventDefault(); // block the native scroll
                   /*
                   *  do what you want the application to do here
                   */
               } 
           }, {passive: true});

          也可以使用 css 屬性 touch-action 來完全消除事件處理器的影響,如:

           #area { 
             touch-action: pan-x; 
           }

          查找到事件對象

          當(dāng)組合器線程發(fā)送輸入事件給主線程時,主線程首先會進(jìn)行命中測試(hit test)來查找對應(yīng)的事件目標(biāo),命中測試會基于渲染過程中生成的繪制記錄( paint records )查找事件發(fā)生坐標(biāo)下存在的元素。

          主線程依據(jù)繪制記錄查找事件相關(guān)元素 事件的優(yōu)化

          一般我們屏幕的刷新速率為 60fps,但是某些事件的觸發(fā)量會不止這個值,出于優(yōu)化的目的,Chrome 會合并連續(xù)的事件 (如 wheel, mousewheel, mousemove, pointermove, touchmove ),并延遲到下一幀渲染時候執(zhí)行 。

          而如 keydown, keyup, mouseup, mousedown, touchstart, 和 touchend 等非連續(xù)性事件則會立即被觸發(fā)。

          Chrome 會合并連續(xù)事件到下一幀觸發(fā)

          合并事件雖然能提示性能,但是如果你的應(yīng)用是繪畫等,則很難繪制一條平滑的曲線了,此時可以使用 getCoalescedEvents API 來獲取組合的事件。示例代碼如下:

           window.addEventListener('pointermove', event=> {
               const events=event.getCoalescedEvents();
               for (let event of events) {
                   const x=event.pageX;
                   const y=event.pageY;
                   // draw a line using x and y coordinates.
               }
           });

          花了好久來整理上面的內(nèi)容,整理的過程收獲還挺大的,也希望這篇筆記能對你有所啟發(fā),如果有任何疑問,歡迎一起來討論。

          javascript 是一門單線程的語言,在同一個時間只能做完成一件任務(wù),如果有多個任務(wù),就必須排隊,前面一個任務(wù)完成,再去執(zhí)行后面的任務(wù)。作為瀏覽器端的腳本語言,javascript 的主要功能是用來和用戶交互以及操作 dom。假設(shè) javascript 不是單線程語言,在一個線程里我們給某個 dom 節(jié)點增加內(nèi)容的時候,另一個線程同時正在刪除這個 dom 節(jié)點的內(nèi)容,則會造成混亂。

          由于 js 單線程的設(shè)計,假設(shè) js 程序的執(zhí)行都是同步。如果執(zhí)行一些耗時較長的程序,例如 ajax 請求,在請求開始至請求響應(yīng)的這段時間內(nèi),當(dāng)前的工作線程一直是空閑狀態(tài), ajax 請求后面的 js 代碼只能等待請求結(jié)束后執(zhí)行,因此會導(dǎo)致 js 阻塞的問題。

          javascript 單線程指的是瀏覽器中負(fù)責(zé)解釋和執(zhí)行 javascript 代碼的只有一個線程,即為 js 引擎線程,但是瀏覽器的渲染進(jìn)程是提供多個線程的,如下:

          1. js 引擎線程
          2. 事件觸發(fā)線程
          3. 定時器觸發(fā)線程
          4. 異步 http 請求線程
          5. GUI 渲染線程

          一、異步 & 同步

          為解決上述類似上述 js 阻塞的問題,js 引入了同步和異步的概念。

          1、什么是同步?

          “同步”就是后一個任務(wù)等待前一個任務(wù)結(jié)束后再去執(zhí)行。

          2、什么是異步?

          “異步”與同步不同,每一個異步任務(wù)都有一個或多個回調(diào)函數(shù)。webapi 會在其相應(yīng)的時機里將回調(diào)函數(shù)添加進(jìn)入消息隊列中,不直接執(zhí)行,然后再去執(zhí)行后面的任務(wù)。直至當(dāng)前同步任務(wù)執(zhí)行完畢后,再把消息隊列中的消息添加進(jìn)入執(zhí)行棧進(jìn)行執(zhí)行。

          異步任務(wù)在瀏覽器中一般是以下:

          1. 網(wǎng)絡(luò)請求
          2. 計時器
          3. DOM 監(jiān)聽事件
          4. ...

          二、什么是執(zhí)行棧(stack)、堆(heap)、事件隊列(task queue)?

          1、執(zhí)行棧

          “棧”是一種數(shù)據(jù)結(jié)構(gòu),是一種線性表。特點為 LIFO,即先進(jìn)后出 (last in, first out)。

          利用數(shù)組的 push 和 shift 可以實現(xiàn)壓棧和出棧的操作。

          在代碼運行的過程中,函數(shù)的調(diào)用會形成一個由若干幀組成的棧。

          function foo(b) {
            let a=10;
            return a + b + 11;
          }
          
          function bar(x) {
            let y=3;
            return foo(x * y);
          }
          
          console.log(bar(7))

          上面代碼最終會在控制臺打印42,下面梳理一下它的執(zhí)行順序。

          1. console.log 函數(shù)作為第一幀壓入棧中。
          2. 調(diào)用 bar,第二幀被壓入棧中。幀中包含著 bar 的變量對象。
          3. bar 調(diào)用 foo,foo 做一位第三幀被壓入棧中,幀中包含著 foo 的變量對象。
          4. foo 執(zhí)行完畢然后返回。被彈出棧。
          5. bar 執(zhí)行完畢然后返回,被彈出棧。
          6. log 函數(shù)接收到 bar 的返回值。執(zhí)行完畢后,出棧。此時棧已空。

          2、堆

          對象被分配在堆中,堆是一個用來表示一大塊(通常是非結(jié)構(gòu)化的)內(nèi)存區(qū)域的計算機術(shù)語。

          堆和棧的區(qū)別

          首先,stack 是有結(jié)構(gòu)的,每個區(qū)塊按照一定次序存放,可以明確知道每個區(qū)塊的大小;heap 是沒有結(jié)構(gòu)的,數(shù)據(jù)可以任意存放。因此,

          stack 的尋址速度要快于 heap。

          其次,每個線程分配一個 stack,每個進(jìn)程分配一個 heap,也就是說,stack 是線程獨占的,heap 是線程共用的。

          此外,stack 創(chuàng)建的時候,大小是確定的,數(shù)據(jù)從超過這個大小,就發(fā)生 stack overflow 錯誤,而 heap 的大小是不確定的,

          需要的話可以不斷增加。

          public void Method1()
          {
              int i=4;
          
              int y=2;
          
              class1 cls1=new class1();
          }

          上面代碼這三個變量和一個對象實例在內(nèi)存中的存放方式如下。

          從上圖可以看到,i、y和cls1都存放在stack,因為它們占用內(nèi)存空間都是確定的,而且本身也屬于局部變量。但是,cls1指向的對象實例存放在heap,因為它的大小不確定。作為一條規(guī)則可以記住,所有的對象都存放在heap。

          接下來的問題是,當(dāng)Method1方法運行結(jié)束,會發(fā)生什么事?

          回答是整個stack被清空,i、y和cls1這三個變量消失,因為它們是局部變量,區(qū)塊一旦運行結(jié)束,就沒必要再存在了。而heap之中的那個對象實例繼續(xù)存在,直到系統(tǒng)的垃圾清理機制(garbage collector)將這塊內(nèi)存回收。因此,一般來說,內(nèi)存泄漏都發(fā)生在heap,即某些內(nèi)存空間不再被使用了,卻因為種種原因,沒有被系統(tǒng)回收。

          3、事件隊列和事件循環(huán)

          隊列是一種數(shù)據(jù)結(jié)構(gòu),也是一種特殊的線性表。特點為 FIFO,即先進(jìn)先出(first in, first out)

          利用數(shù)組的 push 和 pop 可實現(xiàn)入隊和出隊的操作。

          事件循環(huán)和事件隊列的維護(hù)是由事件觸發(fā)線程控制的。

          事件觸發(fā)線程線程同樣是由瀏覽器渲染引擎提供的,它會維護(hù)一個事件隊列。

          js 引擎遇到上文所列的異步任務(wù)后,會交個相應(yīng)的線程去維護(hù)異步任務(wù),等待某個時機,然后由事件觸發(fā)線程將異步任務(wù)對應(yīng)的回調(diào)函數(shù)加入到事件隊列中,事件隊列中的函數(shù)等待被執(zhí)行。

          js 引擎在執(zhí)行過程中,遇到同步任務(wù),會將任務(wù)直接壓入執(zhí)行棧中執(zhí)行,當(dāng)執(zhí)行棧為空(即 js 引擎線程空閑), 事件觸發(fā)線程 會從事件隊列中取出一個任務(wù)(即異步任務(wù)的回調(diào)函數(shù))放入執(zhí)行在棧中執(zhí)行。

          執(zhí)行完了之后,執(zhí)行棧再次為空,事件觸發(fā)線程會重復(fù)上一步的操作,再從事件隊列中取出一個消息,這種機制就被稱為 事件循環(huán) (Event Loop)機制。

          為了更好地理解Event Loop,請看下圖(轉(zhuǎn)引自Philip Roberts的演講《Help, I'm stuck in an event-loop》)。

          例子代碼:

          console.log('script start')
          
          setTimeout(()=> {
            console.log('timer 1 over')
          }, 1000)
          
          setTimeout(()=> {
            console.log('timer 2 over')
          }, 0)
          
          console.log('script end')
          
          // script start
          // script end
          // timer 2 over
          // timer 1 over

          模擬 js 引擎對其執(zhí)行過程:

          第一輪事件循環(huán):

          1. console.log 為同步任務(wù),入棧,打印“script start”。出棧。
          2. setTimeout 為異步任務(wù),入棧,交給定時器觸發(fā)線程處理(在1秒后加入將回調(diào)加入事件隊列)。出棧。
          3. setTimeout 為異步任務(wù),入棧,交給定時器觸發(fā)線程處理(在4ms之內(nèi)將回調(diào)加入事件隊列)。出棧。
          4. console.log 為同步任務(wù),入棧,打印"script end"。出棧。

          此時,執(zhí)行棧為空,js 引擎線程空閑。便從事件隊列中讀取任務(wù),此時隊列如下:

          第二輪事件循環(huán)

          1. js 引擎線程從事件隊列中讀取 cb2 加入執(zhí)行棧并執(zhí)行,打印”time 2 over“。出棧。

          第三輪事件循環(huán)

          1. js 引擎從事件隊列中讀取 cb1 加入執(zhí)行棧中并執(zhí)行,打印”time 1 over“ 。出棧。

          注意點:

          上面,timer 2 的延時為 0ms,HTML5標(biāo)準(zhǔn)規(guī)定 setTimeout 第二個參數(shù)不得小于4(不同瀏覽器最小值會不一樣),不足會自動增加,所以 "timer 2 over" 還是會在 "script end" 之后。

          就算延時為0ms,只是 time 2 的回調(diào)函數(shù)會立即加入事件隊列而已,回調(diào)的執(zhí)行還是得等到執(zhí)行棧為空時執(zhí)行。

          四、宏任務(wù) & 微任務(wù)

          在 ES6 新增 Promise 處理異步后,js 執(zhí)行引擎的處理過程又發(fā)生了新的變化。

          看代碼:

          console.log('script start')
          
          setTimeout(function() {
              console.log('timer over')
          }, 0)
          
          Promise.resolve().then(function() {
              console.log('promise1')
          }).then(function() {
              console.log('promise2')
          })
          
          console.log('script end')
          
          // script start
          // script end
          // promise1
          // promise2
          // timer over

          這里又新增了兩個新的概念, macrotask (宏任務(wù))和 microtask (微任務(wù))。

          所有的任務(wù)都劃分到宏任務(wù)和微任務(wù)下:

          • macrotask : script 主代碼塊、setTimeout、setInterval、requestAnimationFrame、node 中的setimmediate 等。
          • microtask : Promise.then catch finally、MutationObserver、node 中的process.nextTick 等。

          js 引擎首先執(zhí)行主代碼塊。

          執(zhí)行棧每次執(zhí)行的代碼就是一個宏任務(wù),包括任務(wù)隊列(宏任務(wù)隊列)中的。執(zhí)行棧中的任務(wù)執(zhí)行完畢后,js 引擎會從宏任務(wù)隊列中去添加任務(wù)到執(zhí)行棧中,即同樣是事件循環(huán)的機制。

          當(dāng)在執(zhí)行宏任務(wù)遇到微任務(wù) Promise.then 時,會創(chuàng)建一個微任務(wù),并加入到微任務(wù)隊列中的隊尾。

          微任務(wù)是在宏任務(wù)執(zhí)行的時候創(chuàng)建的,而在下一個宏任務(wù)執(zhí)行之前,瀏覽器會對頁面重新渲染(task >> render >> task(任務(wù)隊列中讀取))。 同時,在上一個宏任務(wù)執(zhí)行完成后,頁面渲染之前,會執(zhí)行當(dāng)前微任務(wù)隊列中的所有微任務(wù)。

          所以上述代碼的執(zhí)行過程就可以解釋了。

          js 引擎執(zhí)行 promise.then 時,promise1、promise2 被認(rèn)為是兩個微任務(wù)按照代碼的先后順序被加入到微任務(wù)隊列中,script end執(zhí)行后,空。

          此時當(dāng)前宏任務(wù)(script 主代碼塊)執(zhí)行完畢,并不從當(dāng)前宏任務(wù)隊列中讀取任務(wù)。而是立馬清空當(dāng)前宏任務(wù)所產(chǎn)生的微任務(wù)隊列。將兩個微任務(wù)依次放入執(zhí)行棧中執(zhí)行。執(zhí)行完畢,打印 promise1、promise2。棧空。 此時,第一輪事件循環(huán)結(jié)束。

          緊接著,再去讀取宏任務(wù)隊列中的任務(wù),time over 被打印。棧空。

          因此,宏任務(wù)和微任務(wù)的執(zhí)行機制如下:

          1. 執(zhí)行一個宏任務(wù)(棧中沒有就從宏任務(wù)隊列中獲取)
          2. 執(zhí)行過程中遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊列中
          3. 宏任務(wù)執(zhí)行完畢,立即執(zhí)行當(dāng)前微任務(wù)隊列中的所有微任務(wù)(依次執(zhí)行)
          4. 當(dāng)前所有微任務(wù)執(zhí)行完畢后,開始檢查渲染,GUI 線程接管渲染
          5. 渲染完畢后,JS 引擎繼續(xù)開始下一個宏任務(wù),從宏任務(wù)隊列中獲取

          async & await

          因為,async 和 await 本質(zhì)上還是基于 Promise 的封裝,而 Promise 是屬于微任務(wù)的一種。所以使用 await 關(guān)鍵字與 Promise.then 效果類似:

          setTimeout(_=> console.log(4))
          
          async function main() {
            console.log(1)
            await Promise.resolve()
            console.log(3)
          }
          
          main()
          
          console.log(2)
          // 1
          // 2
          // 3
          // 4

          async 函數(shù)在 await 之前的代碼都是同步執(zhí)行的, 可以理解為 await 之前的代碼都屬于 new Promise 時傳入的代碼,await 之后的所有代碼都是 Promise.then 中的回調(diào),即在微任務(wù)隊列中。

          五、總結(jié)

          1. js 單線程實際上解釋執(zhí)行 js 代碼的只有一個線程,但是瀏覽器的渲染是多線程的。
          2. 異步和同步的概念與區(qū)別,異步任務(wù)有哪些。
          3. 棧、堆、隊列的特點和使用場景。
          4. 事件隊列以及事件循環(huán)機制。
          5. es6 下,宏任務(wù)與微任務(wù)的執(zhí)行過程。

          參考:

          • JavaScript 異步與事件循環(huán)
          • 并發(fā)模型與事件循環(huán)
          • 微任務(wù)、宏任務(wù)與Event-Loop
          • JavaScript 運行機制詳解:再談Event Loop
          • JS事件循環(huán)
          • [譯] 深入理解 JavaScript 事件循環(huán)(二)— task and microtask
          • Help, I'm stuck in an event-loop

          原文作者:大芒果哇

          原文地址:https://www.cnblogs.com/shenggao/p/13799566.html

          向目標(biāo)

          • 目標(biāo):有道翻譯接口參數(shù)
          • 主頁:https://fanyi.youdao.com/
          • 接口:https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule
          • 逆向參數(shù):
            • Form Data:
              • salt: 16261583727540
              • sign: 151e4b19b07ae410e0e1861a6706d30c
              • bv: 5b3e307b66a6c075d525ed231dcc8dcd

          逆向過程

          抓包分析

          我們在有道翻譯頁面隨便輸入文字,可以看到?jīng)]有刷新頁面,翻譯結(jié)果就出來了,由此可以推斷是 Ajax 加載的,打開開發(fā)者工具,選擇 XHR 過濾 Ajax 請求,可以看到有一條 URL 為 https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule 的 POST 請求,當(dāng)我們輸入“測試”的時候,他返回的數(shù)據(jù)類似于如下結(jié)構(gòu):

          {
              "translateResult": [
                  [{
                      "tgt": "test",
                      "src": "測試"
                  }]
              ],
              "errorCode": 0,
              "type": "zh-CHS2en",
              "smartResult": {
                  "entries": ["", "[試驗] test\r\n", "measurement\r\n"],
                  "type": 1
              }
          }

          translateResult 是翻譯的結(jié)果,smartResult 是智能推薦的其他翻譯,那么這個 URL 就是我們需要的翻譯接口了。

          由于是 POST 請求,我們觀察它的 Form Data:

          • i:待翻譯的字符串;
          • from:待翻譯的語言;
          • to:目標(biāo)語言;
          • lts:時間戳;
          • smartresultclientdoctypeversionkeyfrom:固定值;
          • action:實時翻譯 FY_BY_REALTlME、手動點擊翻譯 FY_BY_CLICKBUTTION
          • saltsignbv 的值每次會改變,需要進(jìn)一步分析。

          參數(shù)逆向

          saltsignbv 三個加密參數(shù),全局搜索任意一個,搜索結(jié)果比較多,依次對比,可以發(fā)現(xiàn) fanyi.min.js 文件第 8969 行左右開始,F(xiàn)orm Data 所有的參數(shù)都齊全了,埋下斷點調(diào)試一下,可以看到所有數(shù)據(jù)和最終結(jié)果一致,加密的四個參數(shù)都在 r 當(dāng)中取值,跟蹤 r,往上找可以看到 r=v.generateSaltSign(n);,其中 n 是輸入的待翻譯的字符串:

          繼續(xù)跟進(jìn) generateSaltSign 函數(shù),點擊跳轉(zhuǎn)到 r 函數(shù),這里可以看到關(guān)鍵的加密代碼:

          var r=function(e) {
              var t=n.md5(navigator.appVersion)
                , r="" + (new Date).getTime()
                , i=r + parseInt(10 * Math.random(), 10);
              return {
                  ts: r,
                  bv: t,
                  salt: i,
                  sign: n.md5("fanyideskweb" + e + i + "Y2FYu%TNSbMCxc3t2u^XT")
              }
          };

          分析這段關(guān)鍵加密代碼:

          • navigator.appVersion 就是 UserAgent

          • bv 的值由 UserAgent 經(jīng)過 MD5 加密得到

          • ts 的值為 13 位時間戳

          • salt 的值由 ts 的值加上一個 0-9 的隨機整數(shù)得到

          • sign 的值由待翻譯的字符串、salt 的值和另外兩個固定的字符串組成,再由 MD5 加密得到最終結(jié)果

          這個過程比較簡單,可以直接使用 Python 來復(fù)現(xiàn):

          import time
          import random
          import hashlib
          
          
          query="待翻譯字符串"
          user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
          
          lts=str(int(time.time() * 1000))                                # 以毫秒為單位的 13 位時間戳
          salt=lts + str(random.randint(0, 9))                            # 13 位時間戳+隨機數(shù)字,生成 salt 值
          sign="fanyideskweb" + query + salt + "Y2FYu%TNSbMCxc3t2u^XT"    # 拼接字符串組成 sign
          sign=hashlib.md5(sign.encode()).hexdigest()                     # 將 sign 進(jìn)行 MD5 加密,生成最終 sign 值
          bv=hashlib.md5(user_agent.encode()).hexdigest()                 # 對 UA 進(jìn)行 MD5 加密,生成 bv 值

          或者直接引用 JS,使用 nodejs 里面的加密模塊 CryptoJS 來進(jìn)行 MD5 加密,改寫 JS 如下:

          // 引用 crypto-js 加密模塊
          var CryptoJS=require('crypto-js')
          
          function getEncryptedParams(data, ua) {
              var bv=CryptoJS.MD5(ua).toString()
                  , lts="" + (new Date).getTime()
                  , salt=lts + parseInt(10 * Math.random(), 10)
              var sign=CryptoJS.MD5('fanyideskweb'+data+salt+']BjuETDhU)zqSxf-=B#7m').toString()
              return {bv: bv, lts: lts, salt: salt, sign: sign}
          }

          完整代碼

          youdao_encrypt.js

          獲取加密參數(shù) saltsignbv

          // 引用 crypto-js 加密模塊
          var CryptoJS=require('crypto-js')
          
          function getEncryptedParams(data, ua) {
              var bv=CryptoJS.MD5(ua).toString(),
                  lts="" + (new Date).getTime(),
                  salt=lts + parseInt(10 * Math.random(), 10)
              var sign=CryptoJS.MD5('fanyideskweb' + data + salt + ']BjuETDhU)zqSxf-=B#7m').toString()
              return { bv: bv, lts: lts, salt: salt, sign: sign }
          }
          
          // var ua="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
          // var data="測試"
          // console.log(getEncryptedParams(data, ua));

          youdaofanyi.py


          主站蜘蛛池模板: 欧美日韩综合一区二区三区| 国产一区二区在线观看app| 精品一区二区三区四区电影 | 国产一区二区三区91| 影院成人区精品一区二区婷婷丽春院影视 | 国产亚洲欧洲Aⅴ综合一区| 人妻体内射精一区二区三区| 综合一区自拍亚洲综合图区| 无码人妻精品一区二区三区66 | 国产伦精品一区二区三区不卡 | 久久青青草原一区二区| 激情爆乳一区二区三区| 中文字幕亚洲一区二区三区| 日韩一区二区在线观看| 精品国产一区二区三区AV | 亚洲av日韩综合一区在线观看| 国产精品无码一区二区在线| 男插女高潮一区二区| 国产免费一区二区三区在线观看| 大香伊人久久精品一区二区| 精品欧洲av无码一区二区三区| 中文字幕精品无码一区二区 | 日本一区二区三区久久| 国产一区二区好的精华液| 末成年女A∨片一区二区| 日韩一本之道一区中文字幕| 国产精品熟女一区二区| 色系一区二区三区四区五区| 国产精品一区二区四区| 欧美日韩精品一区二区在线视频| 亚洲欧洲一区二区| 午夜性色一区二区三区免费不卡视频| 国产熟女一区二区三区四区五区| 国产成人一区二区三中文| 亚洲av成人一区二区三区| 亚洲AV综合色区无码一区爱AV| 日韩精品无码免费一区二区三区| 亚洲线精品一区二区三区| 亚洲免费视频一区二区三区| 国产亚洲自拍一区| 日本大香伊一区二区三区|