# 瀏覽器有哪些線程?
⒈、GUI 渲染線程
負責渲染瀏覽器界面,解析 HTML,CSS,構建 DOM 樹和 RenderObject 樹,布局和繪制等。
當界面需要重繪(repaint)或由于某種操作引發回流(reflow)時,該線程就會執行。
注意,GUI 渲染線程與 JS 引擎線程是互斥的,當 JS 引擎執行時 GUI 線程會被掛起(相當于被凍結了),GUI 更新會被保存在一個隊列中等到 JS 引擎空閑時立即被執行。
[TOC]
⒉、JS 引擎線程
也稱為 JS 內核,負責處理 Javascript 腳本程序。(例如 V8 引擎)
JS 引擎線程負責解析 Javascript 腳本,運行代碼。JS 引擎一直等待著任務隊列中任務的到來,然后加以處理,一個 Tab 頁(renderer 進程)中無論什么時候都只有一個 JS 線程在運行 JS 程序
同樣注意,GUI 渲染線程與 JS 引擎線程是互斥的,所以如果 JS 執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞。(比如你的代碼陷入無限循環那么頁面就將會是一片空白)
⒊、事件觸發線程
歸屬于瀏覽器而不是 JS 引擎,用來控制事件循環(具體見 JS 章節事件循環部分)
當 JS 引擎執行代碼塊如 setTimeout 時(也可來自瀏覽器內核的其他線程,如鼠標點擊、AJAX 異步請求等),會將對應任務添加到事件線程中。
當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待 JS 引擎的處理。
⒋、定時觸發器線程
setInterval 與 setTimeout 所在線程
瀏覽器定時計數器并不是由 JavaScript 引擎計數的,因為 JavaScript 引擎是單線程的, 如果處于阻塞線程狀態就會影響計時的準確性,因此通過單獨線程來計時并觸發定時任務(計時完畢后,添加到事件隊列中,等待 JS 引擎空閑后執行)
> W3C在HTML標準中規定,規定要求 setTimeout 中低于 4ms 的時間間隔算為 4ms。
⒌、異步 http 請求線程
XMLHttpRequest 在連接后通過瀏覽器新開一個線程來處理請求。
檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件,將這個回調再放入事件隊列中。再由 JavaScript 引擎執行。
# 瀏覽器的渲染過程
## 瀏覽器內核指的是什么?
> 瀏覽器最重要或者說核心的部分是“Rendering Engine”,可大概譯為“渲染引擎”,不過我們一般習慣將之稱為“瀏覽器內核”。負責對網頁語法的解釋(如[標準通用標記語言](https://baike.baidu.com/item/%E6%A0%87%E5%87%86%E9%80%9A%E7%94%A8%E6%A0%87%E8%AE%B0%E8%AF%AD%E8%A8%80/6805073)下的一個應用[HTML](https://baike.baidu.com/item/HTML)、[JavaScript](https://baike.baidu.com/item/JavaScript))并渲染(顯示)網頁。 所以,通常所謂的[瀏覽器內核](https://baike.baidu.com/item/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%86%85%E6%A0%B8/10602413)也就是瀏覽器所采用的[渲染引擎](https://baike.baidu.com/item/%E6%B8%B2%E6%9F%93%E5%BC%95%E6%93%8E/10982158),渲染引擎決定了瀏覽器如何顯示網頁的內容以及頁面的格式信息。不同的瀏覽器內核對網頁編寫語法的解釋也有不同,因此同一網頁在不同的內核的瀏覽器里的渲染(顯示)效果也可能不同,這也是網頁編寫者需要在不同內核的瀏覽器中測試網頁顯示效果的原因。
主流瀏覽器:IE、Firefox、Safari、Chrome 及 Opera。
四大內核:Trident(也稱IE內核)、webkit、Blink、Gecko
其他常見瀏覽器及其內核:
1. IE 瀏覽器內核:Trident 內核,也是俗稱的 IE 內核;
2. Chrome 瀏覽器內核:統稱為 Chromium 內核或 Chrome 內核,以前是 Webkit 內核,現在是 Blink 內核;
3. Firefox 瀏覽器內核:Gecko 內核,俗稱 Firefox 內核;
4. Safari 瀏覽器內核:Webkit 內核;
5. Opera 瀏覽器內核:最初是自己的 Presto 內核,后來是 Webkit,現在是Blink 內核;
6. 360 瀏覽器、獵豹瀏覽器內核:IE + Chrome 雙內核;
7. 搜狗、遨游、QQ瀏覽器內核:Trident(兼容模式)+ Webkit(高速模式);
8. 百度瀏覽器、世界之窗內核:IE 內核;
9. 2345瀏覽器內核:以前是 IE 內核,現在是 IE + Chrome 雙內核
好吧回到正題:瀏覽器的渲染過程見下面這張經典的圖

瀏覽器的渲染機制一般分為以下幾個步驟
1. 解析 HTML 并構建 DOM 樹:遇到`<script>`標簽時,會暫停 HTML 解析,先執行 JavaScript(除非標記為`async`或`defer`)。遇到 CSS 文件(`<link>`或`<style>`)時,繼續解析 HTML,但會并行下載和解析 CSS。
2. 處理 CSS 構建 CSSOM 樹:CSS 解析是**阻塞渲染**的,瀏覽器會等待 CSSOM 構建完成后才進行下一步。
3. 將 DOM 與 CSSOM 合并成一個渲染樹。
4. 根據渲染樹來布局,計算每個節點的位置。
5. 繪制:將布局樹轉換為屏幕上的**實際像素**,生成繪制指令(如填充顏色、繪制邊框)。特點:* **分層繪制**:瀏覽器將頁面分為多個圖層(Layer),分別繪制以提高效率(如 GPU 加速)。
6. 合成(Composite):將多個圖層按正確順序合并,最終顯示在屏幕上。僅更新變化的圖層(如 CSS`transform`屬性觸發合成層),避免重新布局和繪制。
在構建 CSSOM 樹時,會阻塞渲染,直至 CSSOM 樹構建完成。并且構建 CSSOM 樹是一個十分消耗性能的過程,所以應該盡量保證層級扁平,減少過度層疊,越是具體的 CSS 選擇器,執行速度越慢。
當 HTML 解析到 script 標簽時,會暫停構建 DOM,完成后才會從暫停的地方重新開始。也就是說,如果你想首屏渲染的越快,就越不應該在首屏就加載 JS 文件。并且 CSS 也會影響 JS 的執行,只有當解析完樣式表才會執行 JS,所以也可以認為這種情況下,CSS 也會暫停構建 DOM。
## 圖層
一般來說,可以把普通文檔流看成一個圖層。特定的屬性可以生成一個新的圖層。**不同的圖層渲染互不影響**,所以對于某些頻繁需要渲染的建議單獨生成一個新圖層,提高性能。**但也不能生成過多的圖層,會引起反作用。**
通過以下幾個常用屬性可以生成新圖層
* 3D 變換:translate3d、ranslateZ
* will-change
* video、iframe 標簽
* 通過動畫實現的 opacity 動畫轉換
* position: fixed
當一個元素在自己圖層內發生變化的時候它的回流和重繪只會在圖層內部發生,利用這一特性可以減少了瀏覽器對于重繪與回流的運算量。
## 回流(Reflow)與重繪(Repaint)
重繪和回流是渲染步驟中的一小節,但是這兩個步驟對于性能影響很大。
* 回流是布局或者幾何屬性需要改變就稱為回流。
* 重繪是當節點需要更改外觀而不會影響布局的,比如改變 color 就叫稱為重繪
回流必定會發生重繪,重繪不一定會引發回流。回流所需的成本比重繪高的多,改變深層次的節點很可能導致父節點的一系列回流。
關于如何減少回流與重繪這一問題就轉到 Performance 章節吧。
# 瀏覽器的同源策略
同源指的是三個相同:
- 協議相同
- 域名相同
- 端口相同
舉例來說,`http://www.example.com/dir/page.html`這個網址,協議是`http://`,域名是`www.example.com`,端口是`80`(默認端口可以省略)。它的同源情況如下。
* `http://www.example.com/dir2/other.html`:同源
* `http://example.com/dir/other.html`:不同源(域名不同)
* `http://v2.www.example.com/dir/other.html`:不同源(域名不同)
* `http://www.example.com:81/dir/other.html`:不同源(端口不同)
## 同源策略限制了什么?
1. Cookie、LocalStorage 和 IndexDB 無法讀取。
2. DOM 無法獲得。
3. AJAX 請求不能發送。
> 同源政策的目的,是為了保證用戶信息的安全,防止惡意的網站竊取數據。
> 設想這樣一種情況:A 網站是一家銀行,用戶登錄以后,又去瀏覽其他網站。如果其他網站可以讀取 A 網站的 Cookie,會發生什么?很顯然,如果 Cookie 包含隱私(比如存款總額),這些信息就會泄漏。更可怕的是,Cookie 往往用來保存用戶的登錄狀態,如果用戶沒有退出登錄,其他網站就可以冒充用戶,為所欲為。因為瀏覽器同時還規定,提交表單不受同源政策的限制。
> 由此可見,"同源政策"是必需的,否則 Cookie 可以共享,互聯網就毫無安全可言了。
## 如何規避同源策略?(跨域)
1、`JSONP`
利用 script 標簽沒有跨域限制的漏洞,通過 script 標簽指向一個需要訪問的地址并提供一個回調函數來接收數據。就是給 url 加上一個 callback 參數
缺點:僅限于 GET 請求
``` js
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
function jsonp (data) {
console.log(data)
}
</script>
```
在開發中可能會遇到多個 JSONP 請求的回調函數名是相同的,這時候就需要自己封裝一個 JSONP,以下是簡單實現
``` js
function jsonp(url, jsonpCallback, success) {
let script = document.createElement("script");
script.src = url;
script.async = true;
script.type = "text/javascript";
window[jsonpCallback] = function(data) {
success && success(data);
};
document.body.appendChild(script);
}
jsonp(
"http://xxx",
"callback",
function(value) {
console.log(value);
}
);
```
2、`CORS`
實現 CORS 通信的關鍵是后端。只要后端實現了 CORS,就實現了跨域。
服務端設置(添加響應頭部)`Access-Control-Allow-Origin`就可以開啟 CORS。 該屬性表示哪些域名可以訪問資源,如果設置通配符則表示所有網站都可以訪問資源。
* Access-Control-Allow-Origin: 指定允許哪些源的網頁發送請求.
* Access-Control-Allow-Credentials: 指定是否允許 cookie 發送.
* Access-Control-Allow-Methods: 指定允許哪些請求方法.
* Access-Control-Allow-Headers: 指定允許哪些常規的頭域字段, 比如說 Content-Type.
* Access-Control-Expose-Headers: 指定允許哪些額外的頭域字段, 比如說 X-Custom-Header.
3、`document.domain`
該方式只能用于二級域名相同的情況下,比如`a.test.com`和`b.test.com`適用于該方式。只需要給頁面添加`document.domain = 'test.com'`表示二級域名都相同就可以實現跨域
4、`postMessage`
這種方式通常用于獲取嵌入頁面中的第三方頁面數據。一個頁面發送消息,另一個頁面判斷來源并接收消息(這個 API 是用于多個頁面之間傳遞消息的)
``` js
// 發送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
var origin = event.origin || event.originalEvent.origin;
if (origin === 'http://test.com') {
console.log('驗證通過')
}
});
```
5、配置代理服務器
開發 vue 或者 react 項目的時候可以很方便地配置代理服務器,底層依賴的是 webpack 的 devServer 配置項:
代理服務器的作用大致如下圖:

使用 create-react-app 創造的應用已經具備了代理功能,只需要在 package.json 中添加如下一行
`"proxy": "http://www.weather.com.cn/"`
這一行配置告訴我們的應用,當接收到不是要求本地資源(localhost)的 HTTP 請求時,這個 HTTP 請求的協議和域名部分"替換"為`http://www.weather.com.cn`轉發出去(代理服服務器會根據配置分析 入 和 出 的關系,重新構建新的請求),并將收到的結果返還給瀏覽器,但是注意在線上環境應該開發自己的代理服務器(如 Nginx 的代理配置)
<span style="color: red">簡單來說就是幫我們重新構建請求偽造成同源的請求,當然還要把響應數據轉發回來</span>
# 優雅降級與漸進增強
<span style="font-family:楷體;font-weight: 700;">漸進增強(Progressive Enhancement)</span>:一開始就針對低版本瀏覽器進行構建頁面,完成基本的功能,然后再針對高級瀏覽器進行效果、交互、追加功能達到更好的體驗。
<span style="font-family:楷體;font-weight: 700;">優雅降級(Graceful Degradation)</span>:一開始就構建站點的完整功能,然后針對瀏覽器測試和修復。比如一開始使用 CSS3 的特性構建了一個應用,然后逐步針對各大瀏覽器進行 hack 使其可以在低版本瀏覽器上正常瀏覽。
# 存儲:cookie session token indexedDB
| 特性 | cookie | localStorage | sessionStorage | indexDB |
| --- | --- | --- | --- | --- |
| 數據生命周期 | 一般由服務器生成,可以設置過期時間 | 除非被清理,否則一直存在 | 頁面關閉就清理 | 除非被清理,否則一直存在 |
| 數據存儲大小 | 4K | 5M | 5M | 無限 |
| 與服務端通信 | 每次都會攜帶在 header 中,會影響請求性能 | 不參與 | 不參與 | 不參與 |
從上表可以看到,`cookie`已經不建議用于存儲(一般用于用戶身份驗證及跟蹤用戶行為)。如果沒有大量數據存儲需求的話,可以使用`localStorage`和`sessionStorage`。對于不怎么改變的數據盡量使用`localStorage`存儲,否則可以用`sessionStorage`存儲。
對于`cookie`,我們還需要注意安全性。
| 屬性 | 作用 |
| --- | --- |
| value | 如果用于保存用戶登錄態,應該將該值加密,不能使用明文的用戶標識 |
| http-only | 不能通過 JS 訪問 cookie,減少 XSS 攻擊 |
| secure | 只能在協議為 HTTPS 的請求中攜帶 |
| same-site | 規定瀏覽器不能在跨域請求中攜帶 cookie,減少 CSRF 攻擊 |
如何設置?
利用 HttpResponse 的 addHeader 方法,設置 Set-Cookie 的值,
cookie字符串的格式:`key=value; Expires=date; Path=path; Domain=domain; Secure; HttpOnly`
```js
// 設置cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
// 設置多個cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
response.addHeader("Set-Cookie", "timeout=30; Path=/test; HttpOnly");
// 設置 https 的cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; Secure; HttpOnly");
```
# 登錄驗證的流程
這就要從登錄驗證的技術迭代說起,假設我們使用最原始的方法:用戶輸入用戶名和密碼驗證成功后,服務器向客戶端設置 cookie,我們假設這個 cookie 存儲一個 username 字段(顯然這是一個很愚蠢的行為),那么在用戶首次登錄之后他下次再登錄的時候就擁有了這個 cookie,前端可以設置在用戶一打開應用時就向服務器發送一個請求(自動攜帶 cookie),后端就通過檢測 cookie 中的信息就可以使得用戶直接進入登錄狀態了。
整理一下:① 首次登錄擁有了 cookie ② 再次登錄通過檢驗 cookie 的存在與否來確定登錄狀態
*****
在 cookie 中直接暴露用戶信息是愚蠢的行為,下面我們來升級一下。
我們在 cookie 中存儲一個 userid,服務器根據傳來的 userid 來得到對應的 username,那么就需要花費空間來存儲這一映射關系,假設我們用全局變量來存儲(即存儲在內存中),這就是所謂的 session 了,即 server 端存儲用戶信息。
那么現在就變成了:① 首次登錄擁有了 cookie,但這次記錄的是 userid ② 再次登錄發送 cookie,服務器分析 cookie 并根據存儲的映射關系判斷登錄狀態
看上去不錯,但是仍然存在一些問題:假設我們是 node.js 的一個進程做服務,用戶數量不斷增加,內存將會暴增,而 OS 是會限制一個進程所能使用的最大內存的;另外,假設我為了充分利用 CPU 的多核特性我開個多進程一起來做服務,那么這些進程之間的內存無法共享,即用戶信息無法共享,這就不太妙了。
*****
于是我們可以通過使用 redis 來解決這一問題,redis 不同于 mysql,其數據存放在內存中(雖然昂貴但訪問存快),我們把原先要在各個進程中存儲的全局變量改為統一存儲在 redis 中,這樣就可以做到多進程共享信息(全部通過訪存 redis 來實現)
<br>
粗略地畫個圖就差不多是這樣吧

- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs