前端面試 瀏覽器原理篇

2023-02-17 10:50 更新

一、瀏覽器安全


1. 什么是 XSS 攻擊?

(1)概念

XSS 攻擊指的是跨站腳本攻擊,是一種代碼注入攻擊。攻擊者通過(guò)在網(wǎng)站注入惡意腳本,使之在用戶(hù)的瀏覽器上運(yùn)行,從而盜取用戶(hù)的信息如 cookie 等。

XSS 的本質(zhì)是因?yàn)榫W(wǎng)站沒(méi)有對(duì)惡意代碼進(jìn)行過(guò)濾,與正常的代碼混合在一起了,瀏覽器沒(méi)有辦法分辨哪些腳本是可信的,從而導(dǎo)致了惡意代碼的執(zhí)行。

攻擊者可以通過(guò)這種攻擊方式可以進(jìn)行以下操作:

  • 獲取頁(yè)面的數(shù)據(jù),如DOM、cookie、localStorage;
  • DOS攻擊,發(fā)送合理請(qǐng)求,占用服務(wù)器資源,從而使用戶(hù)無(wú)法訪(fǎng)問(wèn)服務(wù)器;
  • 破壞頁(yè)面結(jié)構(gòu);
  • 流量劫持(將鏈接指向某網(wǎng)站);

(2)攻擊類(lèi)型

XSS 可以分為存儲(chǔ)型、反射型和 DOM 型:

  • 存儲(chǔ)型指的是惡意腳本會(huì)存儲(chǔ)在目標(biāo)服務(wù)器上,當(dāng)瀏覽器請(qǐng)求數(shù)據(jù)時(shí),腳本從服務(wù)器傳回并執(zhí)行。
  • 反射型指的是攻擊者誘導(dǎo)用戶(hù)訪(fǎng)問(wèn)一個(gè)帶有惡意代碼的 URL 后,服務(wù)器端接收數(shù)據(jù)后處理,然后把帶有惡意代碼的數(shù)據(jù)發(fā)送到瀏覽器端,瀏覽器端解析這段帶有 XSS 代碼的數(shù)據(jù)后當(dāng)做腳本執(zhí)行,最終完成 XSS 攻擊。
  • DOM 型指的通過(guò)修改頁(yè)面的 DOM 節(jié)點(diǎn)形成的 XSS。

1)存儲(chǔ)型 XSS 的攻擊步驟:

  1. 攻擊者將惡意代碼提交到?標(biāo)?站的數(shù)據(jù)庫(kù)中。
  2. ?戶(hù)打開(kāi)?標(biāo)?站時(shí),?站服務(wù)端將惡意代碼從數(shù)據(jù)庫(kù)取出,拼接在 HTML 中返回給瀏覽器。
  3. ?戶(hù)瀏覽器接收到響應(yīng)后解析執(zhí)?,混在其中的惡意代碼也被執(zhí)?。
  4. 惡意代碼竊取?戶(hù)數(shù)據(jù)并發(fā)送到攻擊者的?站,或者冒充?戶(hù)的?為,調(diào)??標(biāo)?站接?執(zhí)?攻擊者指定的操作。

這種攻擊常?于帶有?戶(hù)保存數(shù)據(jù)的?站功能,如論壇發(fā)帖、商品評(píng)論、?戶(hù)私信等。

2)反射型 XSS 的攻擊步驟:

  1. 攻擊者構(gòu)造出特殊的 URL,其中包含惡意代碼。
  2. ?戶(hù)打開(kāi)帶有惡意代碼的 URL 時(shí),?站服務(wù)端將惡意代碼從 URL 中取出,拼接在 HTML 中返回給瀏覽器。
  3. ?戶(hù)瀏覽器接收到響應(yīng)后解析執(zhí)?,混在其中的惡意代碼也被執(zhí)?。
  4. 惡意代碼竊取?戶(hù)數(shù)據(jù)并發(fā)送到攻擊者的?站,或者冒充?戶(hù)的?為,調(diào)??標(biāo)?站接?執(zhí)?攻擊者指定的操作。

反射型 XSS 跟存儲(chǔ)型 XSS 的區(qū)別是:存儲(chǔ)型 XSS 的惡意代碼存在數(shù)據(jù)庫(kù)?,反射型 XSS 的惡意代碼存在 URL ?。

反射型 XSS 漏洞常?于通過(guò) URL 傳遞參數(shù)的功能,如?站搜索、跳轉(zhuǎn)等。 由于需要?戶(hù)主動(dòng)打開(kāi)惡意的 URL 才能?效,攻擊者往往會(huì)結(jié)合多種?段誘導(dǎo)?戶(hù)點(diǎn)擊。

3)DOM  XSS 的攻擊步驟:

  1. 攻擊者構(gòu)造出特殊的 URL,其中包含惡意代碼。
  2. ?戶(hù)打開(kāi)帶有惡意代碼的 URL。
  3. ?戶(hù)瀏覽器接收到響應(yīng)后解析執(zhí)?,前端 JavaScript 取出 URL 中的惡意代碼并執(zhí)?。
  4. 惡意代碼竊取?戶(hù)數(shù)據(jù)并發(fā)送到攻擊者的?站,或者冒充?戶(hù)的?為,調(diào)??標(biāo)?站接?執(zhí)?攻擊者指定的操作。

DOM 型 XSS 跟前兩種 XSS 的區(qū)別:DOM 型 XSS 攻擊中,取出和執(zhí)?惡意代碼由瀏覽器端完成,屬于前端JavaScript ?身的安全漏洞,?其他兩種 XSS 都屬于服務(wù)端的安全漏洞。

2. 如何防御 XSS 攻擊?

可以看到XSS危害如此之大, 那么在開(kāi)發(fā)網(wǎng)站時(shí)就要做好防御措施,具體措施如下:

  • 可以從瀏覽器的執(zhí)行來(lái)進(jìn)行預(yù)防,一種是使用純前端的方式,不用服務(wù)器端拼接后返回(不使用服務(wù)端渲染)。另一種是對(duì)需要插入到 HTML 中的代碼做好充分的轉(zhuǎn)義。對(duì)于 DOM 型的攻擊,主要是前端腳本的不可靠而造成的,對(duì)于數(shù)據(jù)獲取渲染和字符串拼接的時(shí)候應(yīng)該對(duì)可能出現(xiàn)的惡意代碼情況進(jìn)行判斷。
  • 使用 CSP ,CSP 的本質(zhì)是建立一個(gè)白名單,告訴瀏覽器哪些外部資源可以加載和執(zhí)行,從而防止惡意代碼的注入攻擊。
  1. CSP 指的是內(nèi)容安全策略,它的本質(zhì)是建立一個(gè)白名單,告訴瀏覽器哪些外部資源可以加載和執(zhí)行。我們只需要配置規(guī)則,如何攔截由瀏覽器自己來(lái)實(shí)現(xiàn)。
  2. 通常有兩種方式來(lái)開(kāi)啟 CSP,一種是設(shè)置 HTTP 首部中的 Content-Security-Policy,一種是設(shè)置 meta 標(biāo)簽的方式 ?<meta http-equiv="Content-Security-Policy">?
  • 對(duì)一些敏感信息進(jìn)行保護(hù),比如 cookie 使用 http-only,使得腳本無(wú)法獲取。也可以使用驗(yàn)證碼,避免腳本偽裝成用戶(hù)執(zhí)行一些操作。

3. 什么是 CSRF 攻擊?

(1)概念

CSRF 攻擊指的是跨站請(qǐng)求偽造攻擊,攻擊者誘導(dǎo)用戶(hù)進(jìn)入一個(gè)第三方網(wǎng)站,然后該網(wǎng)站向被攻擊網(wǎng)站發(fā)送跨站請(qǐng)求。如果用戶(hù)在被攻擊網(wǎng)站中保存了登錄狀態(tài),那么攻擊者就可以利用這個(gè)登錄狀態(tài),繞過(guò)后臺(tái)的用戶(hù)驗(yàn)證,冒充用戶(hù)向服務(wù)器執(zhí)行一些操作。

CSRF 攻擊的本質(zhì)是利用 cookie 會(huì)在同源請(qǐng)求中攜帶發(fā)送給服務(wù)器的特點(diǎn),以此來(lái)實(shí)現(xiàn)用戶(hù)的冒充。

(2)攻擊類(lèi)型

常見(jiàn)的 CSRF 攻擊有三種:

  • GET 類(lèi)型的 CSRF 攻擊,比如在網(wǎng)站中的一個(gè) img 標(biāo)簽里構(gòu)建一個(gè)請(qǐng)求,當(dāng)用戶(hù)打開(kāi)這個(gè)網(wǎng)站的時(shí)候就會(huì)自動(dòng)發(fā)起提交。
  • POST 類(lèi)型的 CSRF 攻擊,比如構(gòu)建一個(gè)表單,然后隱藏它,當(dāng)用戶(hù)進(jìn)入頁(yè)面時(shí),自動(dòng)提交這個(gè)表單。
  • 鏈接類(lèi)型的 CSRF 攻擊,比如在 a 標(biāo)簽的 href 屬性里構(gòu)建一個(gè)請(qǐng)求,然后誘導(dǎo)用戶(hù)去點(diǎn)擊。

4. 如何防御 CSRF 攻擊?

CSRF 攻擊可以使用以下方法來(lái)防護(hù):

  • 進(jìn)行同源檢測(cè),服務(wù)器根據(jù) http 請(qǐng)求頭中 origin 或者 referer 信息來(lái)判斷請(qǐng)求是否為允許訪(fǎng)問(wèn)的站點(diǎn),從而對(duì)請(qǐng)求進(jìn)行過(guò)濾。當(dāng) origin 或者 referer 信息都不存在的時(shí)候,直接阻止請(qǐng)求。這種方式的缺點(diǎn)是有些情況下 referer 可以被偽造,同時(shí)還會(huì)把搜索引擎的鏈接也給屏蔽了。所以一般網(wǎng)站會(huì)允許搜索引擎的頁(yè)面請(qǐng)求,但是相應(yīng)的頁(yè)面請(qǐng)求這種請(qǐng)求方式也可能被攻擊者給利用。(Referer 字段會(huì)告訴服務(wù)器該網(wǎng)頁(yè)是從哪個(gè)頁(yè)面鏈接過(guò)來(lái)的)
  • 使用 CSRF Token 進(jìn)行驗(yàn)證,服務(wù)器向用戶(hù)返回一個(gè)隨機(jī)數(shù) Token ,當(dāng)網(wǎng)站再次發(fā)起請(qǐng)求時(shí),在請(qǐng)求參數(shù)中加入服務(wù)器端返回的 token ,然后服務(wù)器對(duì)這個(gè) token 進(jìn)行驗(yàn)證。這種方法解決了使用 cookie 單一驗(yàn)證方式時(shí),可能會(huì)被冒用的問(wèn)題,但是這種方法存在一個(gè)缺點(diǎn)就是,我們需要給網(wǎng)站中的所有請(qǐng)求都添加上這個(gè) token,操作比較繁瑣。還有一個(gè)問(wèn)題是一般不會(huì)只有一臺(tái)網(wǎng)站服務(wù)器,如果請(qǐng)求經(jīng)過(guò)負(fù)載平衡轉(zhuǎn)移到了其他的服務(wù)器,但是這個(gè)服務(wù)器的 session 中沒(méi)有保留這個(gè) token 的話(huà),就沒(méi)有辦法驗(yàn)證了。這種情況可以通過(guò)改變 token 的構(gòu)建方式來(lái)解決。
  • 對(duì)Cookie 進(jìn)行雙重驗(yàn)證,服務(wù)器在用戶(hù)訪(fǎng)問(wèn)網(wǎng)站頁(yè)面時(shí),向請(qǐng)求域名注入一個(gè)Cookie,內(nèi)容為隨機(jī)字符串,然后當(dāng)用戶(hù)再次向服務(wù)器發(fā)送請(qǐng)求的時(shí)候,從 cookie 中取出這個(gè)字符串,添加到 URL 參數(shù)中,然后服務(wù)器通過(guò)對(duì) cookie 中的數(shù)據(jù)和參數(shù)中的數(shù)據(jù)進(jìn)行比較,來(lái)進(jìn)行驗(yàn)證。使用這種方式是利用了攻擊者只能利用 cookie,但是不能訪(fǎng)問(wèn)獲取 cookie 的特點(diǎn)。并且這種方法比 CSRF Token 的方法更加方便,并且不涉及到分布式訪(fǎng)問(wèn)的問(wèn)題。這種方法的缺點(diǎn)是如果網(wǎng)站存在 XSS 漏洞的,那么這種方式會(huì)失效。同時(shí)這種方式不能做到子域名的隔離。
  • 在設(shè)置 cookie 屬性的時(shí)候設(shè)置 Samesite ,限制 cookie 不能作為被第三方使用,從而可以避免被攻擊者利用。Samesite 一共有兩種模式,一種是嚴(yán)格模式,在嚴(yán)格模式下 cookie 在任何情況下都不可能作為第三方 Cookie 使用,在寬松模式下,cookie 可以被請(qǐng)求是 GET 請(qǐng)求,且會(huì)發(fā)生頁(yè)面跳轉(zhuǎn)的請(qǐng)求所使用。

5. 什么是中間人攻擊?如何防范中間人攻擊?

中間? (Man-in-the-middle attack, MITM) 是指攻擊者與通訊的兩端分別創(chuàng)建獨(dú)?的聯(lián)系, 并交換其所收到的數(shù)據(jù), 使通訊的兩端認(rèn)為他們正在通過(guò)?個(gè)私密的連接與對(duì)?直接對(duì)話(huà), 但事實(shí)上整個(gè)會(huì)話(huà)都被攻擊者完全控制。在中間?攻擊中,攻擊者可以攔截通訊雙?的通話(huà)并插?新的內(nèi)容。

攻擊過(guò)程如下:

  • 客戶(hù)端發(fā)送請(qǐng)求到服務(wù)端,請(qǐng)求被中間?截獲
  • 服務(wù)器向客戶(hù)端發(fā)送公鑰
  • 中間?截獲公鑰,保留在???上。然后???成?個(gè)偽造的公鑰,發(fā)給客戶(hù)端
  • 客戶(hù)端收到偽造的公鑰后,?成加密hash值發(fā)給服務(wù)器
  • 中間?獲得加密hash值,???的私鑰解密獲得真秘鑰,同時(shí)?成假的加密hash值,發(fā)給服務(wù)器
  • 服務(wù)器?私鑰解密獲得假密鑰,然后加密數(shù)據(jù)傳輸給客戶(hù)端

6. 有哪些可能引起前端安全的問(wèn)題?

  • 跨站腳本 (Cross-Site Scripting, XSS): ?種代碼注??式, 為了與 CSS 區(qū)分所以被稱(chēng)作 XSS。早期常?于?絡(luò)論壇, 起因是?站沒(méi)有對(duì)?戶(hù)的輸?進(jìn)?嚴(yán)格的限制, 使得攻擊者可以將腳本上傳到帖?讓其他?瀏覽到有惡意腳本的??, 其注??式很簡(jiǎn)單包括但不限于 JavaScript / CSS / Flash 等;
  • iframe的濫?: iframe中的內(nèi)容是由第三?來(lái)提供的,默認(rèn)情況下他們不受控制,他們可以在iframe中運(yùn)?JavaScirpt腳本、Flash插件、彈出對(duì)話(huà)框等等,這可能會(huì)破壞前端?戶(hù)體驗(yàn);
  • 跨站點(diǎn)請(qǐng)求偽造(Cross-Site Request Forgeries,CSRF): 指攻擊者通過(guò)設(shè)置好的陷阱,強(qiáng)制對(duì)已完成認(rèn)證的?戶(hù)進(jìn)??預(yù)期的個(gè)?信息或設(shè)定信息等某些狀態(tài)更新,屬于被動(dòng)攻擊
  • 惡意第三?庫(kù): ?論是后端服務(wù)器應(yīng)?還是前端應(yīng)?開(kāi)發(fā),絕?多數(shù)時(shí)候都是在借助開(kāi)發(fā)框架和各種類(lèi)庫(kù)進(jìn)?快速開(kāi)發(fā),?旦第三?庫(kù)被植?惡意代碼很容易引起安全問(wèn)題。

7. 網(wǎng)絡(luò)劫持有哪幾種,如何防范?

?絡(luò)劫持分為兩種:

(1)DNS劫持: (輸?京東被強(qiáng)制跳轉(zhuǎn)到淘寶這就屬于dns劫持)

  • DNS強(qiáng)制解析: 通過(guò)修改運(yùn)營(yíng)商的本地DNS記錄,來(lái)引導(dǎo)?戶(hù)流量到緩存服務(wù)器
  • 302跳轉(zhuǎn)的?式: 通過(guò)監(jiān)控?絡(luò)出?的流量,分析判斷哪些內(nèi)容是可以進(jìn)?劫持處理的,再對(duì)劫持的內(nèi)存發(fā)起302跳轉(zhuǎn)的回復(fù),引導(dǎo)?戶(hù)獲取內(nèi)容

(2)HTTP劫持: (訪(fǎng)問(wèn)?歌但是?直有貪玩藍(lán)?的?告),由于http明?傳輸,運(yùn)營(yíng)商會(huì)修改你的http響應(yīng)內(nèi)容(即加?告)

DNS劫持由于涉嫌違法,已經(jīng)被監(jiān)管起來(lái),現(xiàn)在很少會(huì)有DNS劫持,?http劫持依然?常盛?,最有效的辦法就是全站HTTPS,將HTTP加密,這使得運(yùn)營(yíng)商?法獲取明?,就?法劫持你的響應(yīng)內(nèi)容。

二、進(jìn)程與線(xiàn)程


1. 進(jìn)程與線(xiàn)程的概念

從本質(zhì)上說(shuō),進(jìn)程和線(xiàn)程都是 CPU 工作時(shí)間片的一個(gè)描述:

  • 進(jìn)程描述了 CPU 在運(yùn)行指令及加載和保存上下文所需的時(shí)間,放在應(yīng)用上來(lái)說(shuō)就代表了一個(gè)程序。
  • 線(xiàn)程是進(jìn)程中的更小單位,描述了執(zhí)行一段指令所需的時(shí)間。

進(jìn)程是資源分配的最小單位,線(xiàn)程是CPU調(diào)度的最小單位。

一個(gè)進(jìn)程就是一個(gè)程序的運(yùn)行實(shí)例。詳細(xì)解釋就是,啟動(dòng)一個(gè)程序的時(shí)候,操作系統(tǒng)會(huì)為該程序創(chuàng)建一塊內(nèi)存,用來(lái)存放代碼、運(yùn)行中的數(shù)據(jù)和一個(gè)執(zhí)行任務(wù)的主線(xiàn)程,我們把這樣的一個(gè)運(yùn)行環(huán)境叫進(jìn)程。進(jìn)程是運(yùn)行在虛擬內(nèi)存上的,虛擬內(nèi)存是用來(lái)解決用戶(hù)對(duì)硬件資源的無(wú)限需求和有限的硬件資源之間的矛盾的。從操作系統(tǒng)角度來(lái)看,虛擬內(nèi)存即交換文件;從處理器角度看,虛擬內(nèi)存即虛擬地址空間。

如果程序很多時(shí),內(nèi)存可能會(huì)不夠,操作系統(tǒng)為每個(gè)進(jìn)程提供一套獨(dú)立的虛擬地址空間,從而使得同一塊物理內(nèi)存在不同的進(jìn)程中可以對(duì)應(yīng)到不同或相同的虛擬地址,變相的增加了程序可以使用的內(nèi)存。

進(jìn)程和線(xiàn)程之間的關(guān)系有以下四個(gè)特點(diǎn):

(1)進(jìn)程中的任意一線(xiàn)程執(zhí)行出錯(cuò),都會(huì)導(dǎo)致整個(gè)進(jìn)程的崩潰。

(2)線(xiàn)程之間共享進(jìn)程中的數(shù)據(jù)。

(3)當(dāng)一個(gè)進(jìn)程關(guān)閉之后,操作系統(tǒng)會(huì)回收進(jìn)程所占用的內(nèi)存,**當(dāng)一個(gè)進(jìn)程退出時(shí),操作系統(tǒng)會(huì)回收該進(jìn)程所申請(qǐng)的所有資源;即使其中任意線(xiàn)程因?yàn)椴僮鞑划?dāng)導(dǎo)致內(nèi)存泄漏,當(dāng)進(jìn)程退出時(shí),這些內(nèi)存也會(huì)被正確回收。

(4)進(jìn)程之間的內(nèi)容相互隔離。**進(jìn)程隔離就是為了使操作系統(tǒng)中的進(jìn)程互不干擾,每一個(gè)進(jìn)程只能訪(fǎng)問(wèn)自己占有的數(shù)據(jù),也就避免出現(xiàn)進(jìn)程 A 寫(xiě)入數(shù)據(jù)到進(jìn)程 B 的情況。正是因?yàn)檫M(jìn)程之間的數(shù)據(jù)是嚴(yán)格隔離的,所以一個(gè)進(jìn)程如果崩潰了,或者掛起了,是不會(huì)影響到其他進(jìn)程的。如果進(jìn)程之間需要進(jìn)行數(shù)據(jù)的通信,這時(shí)候,就需要使用用于進(jìn)程間通信的機(jī)制了。

Chrome瀏覽器的架構(gòu)圖


從圖中可以看出,最新的 Chrome 瀏覽器包括:

  • 1 個(gè)瀏覽器主進(jìn)程
  • 1 個(gè) GPU 進(jìn)程
  • 1 個(gè)網(wǎng)絡(luò)進(jìn)程
  • 多個(gè)渲染進(jìn)程
  • 多個(gè)插件進(jìn)程

這些進(jìn)程的功能:

  • 瀏覽器進(jìn)程:主要負(fù)責(zé)界面顯示、用戶(hù)交互、子進(jìn)程管理,同時(shí)提供存儲(chǔ)等功能。
  • 渲染進(jìn)程:核心任務(wù)是將 HTML、CSS 和 JavaScript 轉(zhuǎn)換為用戶(hù)可以與之交互的網(wǎng)頁(yè),排版引擎 Blink 和 JavaScript 引擎 V8 都是運(yùn)行在該進(jìn)程中,默認(rèn)情況下,Chrome 會(huì)為每個(gè) Tab 標(biāo)簽創(chuàng)建一個(gè)渲染進(jìn)程。出于安全考慮,渲染進(jìn)程都是運(yùn)行在沙箱模式下。
  • GPU 進(jìn)程:其實(shí), GPU 的使用初衷是為了實(shí)現(xiàn) 3D CSS 的效果,只是隨后網(wǎng)頁(yè)、Chrome 的 UI 界面都選擇采用 GPU 來(lái)繪制,這使得 GPU 成為瀏覽器普遍的需求。最后,Chrome 在其多進(jìn)程架構(gòu)上也引入了 GPU 進(jìn)程。
  • 網(wǎng)絡(luò)進(jìn)程:主要負(fù)責(zé)頁(yè)面的網(wǎng)絡(luò)資源加載,之前是作為一個(gè)模塊運(yùn)行在瀏覽器進(jìn)程里面的,直至最近才獨(dú)立出來(lái),成為一個(gè)單獨(dú)的進(jìn)程。
  • 插件進(jìn)程:主要是負(fù)責(zé)插件的運(yùn)行,因插件易崩潰,所以需要通過(guò)插件進(jìn)程來(lái)隔離,以保證插件進(jìn)程崩潰不會(huì)對(duì)瀏覽器和頁(yè)面造成影響。

所以,打開(kāi)一個(gè)網(wǎng)頁(yè),最少需要四個(gè)進(jìn)程:1 個(gè)網(wǎng)絡(luò)進(jìn)程、1 個(gè)瀏覽器進(jìn)程、1 個(gè) GPU 進(jìn)程以及 1 個(gè)渲染進(jìn)程。如果打開(kāi)的頁(yè)面有運(yùn)行插件的話(huà),還需要再加上 1 個(gè)插件進(jìn)程。

雖然多進(jìn)程模型提升了瀏覽器的穩(wěn)定性、流暢性和安全性,但同樣不可避免地帶來(lái)了一些問(wèn)題:

  • 更高的資源占用:因?yàn)槊總€(gè)進(jìn)程都會(huì)包含公共基礎(chǔ)結(jié)構(gòu)的副本(如 JavaScript 運(yùn)行環(huán)境),這就意味著瀏覽器會(huì)消耗更多的內(nèi)存資源。
  • 更復(fù)雜的體系架構(gòu):瀏覽器各模塊之間耦合性高、擴(kuò)展性差等問(wèn)題,會(huì)導(dǎo)致現(xiàn)在的架構(gòu)已經(jīng)很難適應(yīng)新的需求了。

2. 進(jìn)程和線(xiàn)程的區(qū)別

  • 進(jìn)程可以看做獨(dú)立應(yīng)用,線(xiàn)程不能
  • 資源:進(jìn)程是cpu資源分配的最小單位(是能擁有資源和獨(dú)立運(yùn)行的最小單位);線(xiàn)程是cpu調(diào)度的最小單位(線(xiàn)程是建立在進(jìn)程的基礎(chǔ)上的一次程序運(yùn)行單位,一個(gè)進(jìn)程中可以有多個(gè)線(xiàn)程)。
  • 通信方面:線(xiàn)程間可以通過(guò)直接共享同一進(jìn)程中的資源,而進(jìn)程通信需要借助 進(jìn)程間通信。
  • 調(diào)度:進(jìn)程切換比線(xiàn)程切換的開(kāi)銷(xiāo)要大。線(xiàn)程是CPU調(diào)度的基本單位,線(xiàn)程的切換不會(huì)引起進(jìn)程切換,但某個(gè)進(jìn)程中的線(xiàn)程切換到另一個(gè)進(jìn)程中的線(xiàn)程時(shí),會(huì)引起進(jìn)程切換。
  • 系統(tǒng)開(kāi)銷(xiāo):由于創(chuàng)建或撤銷(xiāo)進(jìn)程時(shí),系統(tǒng)都要為之分配或回收資源,如內(nèi)存、I/O 等,其開(kāi)銷(xiāo)遠(yuǎn)大于創(chuàng)建或撤銷(xiāo)線(xiàn)程時(shí)的開(kāi)銷(xiāo)。同理,在進(jìn)行進(jìn)程切換時(shí),涉及當(dāng)前執(zhí)行進(jìn)程 CPU 環(huán)境還有各種各樣狀態(tài)的保存及新調(diào)度進(jìn)程狀態(tài)的設(shè)置,而線(xiàn)程切換時(shí)只需保存和設(shè)置少量寄存器內(nèi)容,開(kāi)銷(xiāo)較小。

3. 瀏覽器渲染進(jìn)程的線(xiàn)程有哪些

瀏覽器的渲染進(jìn)程的線(xiàn)程總共有五種:


(1)GUI渲染線(xiàn)程

負(fù)責(zé)渲染瀏覽器頁(yè)面,解析HTML、CSS,構(gòu)建DOM樹(shù)、構(gòu)建CSSOM樹(shù)、構(gòu)建渲染樹(shù)和繪制頁(yè)面;當(dāng)界面需要重繪或由于某種操作引發(fā)回流時(shí),該線(xiàn)程就會(huì)執(zhí)行。

注意:GUI渲染線(xiàn)程和JS引擎線(xiàn)程是互斥的,當(dāng)JS引擎執(zhí)行時(shí)GUI線(xiàn)程會(huì)被掛起,GUI更新會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎空閑時(shí)立即被執(zhí)行。

(2)JS引擎線(xiàn)程

JS引擎線(xiàn)程也稱(chēng)為JS內(nèi)核,負(fù)責(zé)處理Javascript腳本程序,解析Javascript腳本,運(yùn)行代碼;JS引擎線(xiàn)程一直等待著任務(wù)隊(duì)列中任務(wù)的到來(lái),然后加以處理,一個(gè)Tab頁(yè)中無(wú)論什么時(shí)候都只有一個(gè)JS引擎線(xiàn)程在運(yùn)行JS程序;

注意:GUI渲染線(xiàn)程與JS引擎線(xiàn)程的互斥關(guān)系,所以如果JS執(zhí)行的時(shí)間過(guò)長(zhǎng),會(huì)造成頁(yè)面的渲染不連貫,導(dǎo)致頁(yè)面渲染加載阻塞。

(3)事件觸發(fā)線(xiàn)程

事件觸發(fā)線(xiàn)程屬于瀏覽器而不是JS引擎,用來(lái)控制事件循環(huán);當(dāng)JS引擎執(zhí)行代碼塊如setTimeOut時(shí)(也可是來(lái)自瀏覽器內(nèi)核的其他線(xiàn)程,如鼠標(biāo)點(diǎn)擊、AJAX異步請(qǐng)求等),會(huì)將對(duì)應(yīng)任務(wù)添加到事件觸發(fā)線(xiàn)程中;當(dāng)對(duì)應(yīng)的事件符合觸發(fā)條件被觸發(fā)時(shí),該線(xiàn)程會(huì)把事件添加到待處理隊(duì)列的隊(duì)尾,等待JS引擎的處理;

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

(4)定時(shí)器觸發(fā)進(jìn)程

定時(shí)器觸發(fā)進(jìn)程即setInterval與setTimeout所在線(xiàn)程;瀏覽器定時(shí)計(jì)數(shù)器并不是由JS引擎計(jì)數(shù)的,因?yàn)镴S引擎是單線(xiàn)程的,如果處于阻塞線(xiàn)程狀態(tài)就會(huì)影響記計(jì)時(shí)的準(zhǔn)確性;因此使用單獨(dú)線(xiàn)程來(lái)計(jì)時(shí)并觸發(fā)定時(shí)器,計(jì)時(shí)完畢后,添加到事件隊(duì)列中,等待JS引擎空閑后執(zhí)行,所以定時(shí)器中的任務(wù)在設(shè)定的時(shí)間點(diǎn)不一定能夠準(zhǔn)時(shí)執(zhí)行,定時(shí)器只是在指定時(shí)間點(diǎn)將任務(wù)添加到事件隊(duì)列中;

注意:W3C在HTML標(biāo)準(zhǔn)中規(guī)定,定時(shí)器的定時(shí)時(shí)間不能小于4ms,如果是小于4ms,則默認(rèn)為4ms。

(5)異步http請(qǐng)求線(xiàn)程

  • XMLHttpRequest連接后通過(guò)瀏覽器新開(kāi)一個(gè)線(xiàn)程請(qǐng)求;
  • 檢測(cè)到狀態(tài)變更時(shí),如果設(shè)置有回調(diào)函數(shù),異步線(xiàn)程就產(chǎn)生狀態(tài)變更事件,將回調(diào)函數(shù)放入事件隊(duì)列中,等待JS引擎空閑后執(zhí)行;

4. 進(jìn)程之前的通信方式

(1)管道通信

管道是一種最基本的進(jìn)程間通信機(jī)制。管道就是操作系統(tǒng)在內(nèi)核中開(kāi)辟的一段緩沖區(qū),進(jìn)程1可以將需要交互的數(shù)據(jù)拷貝到這段緩沖區(qū),進(jìn)程2就可以讀取了。

管道的特點(diǎn):

  • 只能單向通信
  • 只能血緣關(guān)系的進(jìn)程進(jìn)行通信
  • 依賴(lài)于文件系統(tǒng)
  • 生命周期隨進(jìn)程
  • 面向字節(jié)流的服務(wù)
  • 管道內(nèi)部提供了同步機(jī)制

(2)消息隊(duì)列通信

消息隊(duì)列就是一個(gè)消息的列表。用戶(hù)可以在消息隊(duì)列中添加消息、讀取消息等。消息隊(duì)列提供了一種從一個(gè)進(jìn)程向另一個(gè)進(jìn)程發(fā)送一個(gè)數(shù)據(jù)塊的方法。 每個(gè)數(shù)據(jù)塊都被認(rèn)為含有一個(gè)類(lèi)型,接收進(jìn)程可以獨(dú)立地接收含有不同類(lèi)型的數(shù)據(jù)結(jié)構(gòu)??梢酝ㄟ^(guò)發(fā)送消息來(lái)避免命名管道的同步和阻塞問(wèn)題。但是消息隊(duì)列與命名管道一樣,每個(gè)數(shù)據(jù)塊都有一個(gè)最大長(zhǎng)度的限制。

使用消息隊(duì)列進(jìn)行進(jìn)程間通信,可能會(huì)收到數(shù)據(jù)塊最大長(zhǎng)度的限制約束等,這也是這種通信方式的缺點(diǎn)。如果頻繁的發(fā)生進(jìn)程間的通信行為,那么進(jìn)程需要頻繁地讀取隊(duì)列中的數(shù)據(jù)到內(nèi)存,相當(dāng)于間接地從一個(gè)進(jìn)程拷貝到另一個(gè)進(jìn)程,這需要花費(fèi)時(shí)間。

(3)信號(hào)量通信

共享內(nèi)存最大的問(wèn)題就是多進(jìn)程競(jìng)爭(zhēng)內(nèi)存的問(wèn)題,就像類(lèi)似于線(xiàn)程安全問(wèn)題。我們可以使用信號(hào)量來(lái)解決這個(gè)問(wèn)題。信號(hào)量的本質(zhì)就是一個(gè)計(jì)數(shù)器,用來(lái)實(shí)現(xiàn)進(jìn)程之間的互斥與同步。例如信號(hào)量的初始值是 1,然后 a 進(jìn)程來(lái)訪(fǎng)問(wèn)內(nèi)存1的時(shí)候,我們就把信號(hào)量的值設(shè)為 0,然后進(jìn)程b 也要來(lái)訪(fǎng)問(wèn)內(nèi)存1的時(shí)候,看到信號(hào)量的值為 0 就知道已經(jīng)有進(jìn)程在訪(fǎng)問(wèn)內(nèi)存1了,這個(gè)時(shí)候進(jìn)程 b 就會(huì)訪(fǎng)問(wèn)不了內(nèi)存1。所以說(shuō),信號(hào)量也是進(jìn)程之間的一種通信方式。

(4)信號(hào)通信

信號(hào)(Signals )是Unix系統(tǒng)中使用的最古老的進(jìn)程間通信的方法之一。操作系統(tǒng)通過(guò)信號(hào)來(lái)通知進(jìn)程系統(tǒng)中發(fā)生了某種預(yù)先規(guī)定好的事件(一組事件中的一個(gè)),它也是用戶(hù)進(jìn)程之間通信和同步的一種原始機(jī)制。

(5)共享內(nèi)存通信

共享內(nèi)存就是映射一段能被其他進(jìn)程所訪(fǎng)問(wèn)的內(nèi)存,這段共享內(nèi)存由一個(gè)進(jìn)程創(chuàng)建,但多個(gè)進(jìn)程都可以訪(fǎng)問(wèn)(使多個(gè)進(jìn)程可以訪(fǎng)問(wèn)同一塊內(nèi)存空間)。共享內(nèi)存是最快的 IPC 方式,它是針對(duì)其他進(jìn)程間通信方式運(yùn)行效率低而專(zhuān)門(mén)設(shè)計(jì)的。它往往與其他通信機(jī)制,如信號(hào)量,配合使用,來(lái)實(shí)現(xiàn)進(jìn)程間的同步和通信。

(6)套接字通信

上面我們說(shuō)的共享內(nèi)存、管道、信號(hào)量、消息隊(duì)列,他們都是多個(gè)進(jìn)程在一臺(tái)主機(jī)之間的通信,那兩個(gè)相隔幾千里的進(jìn)程能夠進(jìn)行通信嗎?答是必須的,這個(gè)時(shí)候 Socket 這家伙就派上用場(chǎng)了,例如我們平時(shí)通過(guò)瀏覽器發(fā)起一個(gè) http 請(qǐng)求,然后服務(wù)器給你返回對(duì)應(yīng)的數(shù)據(jù),這種就是采用 Socket 的通信方式了。

5. 僵尸進(jìn)程和孤兒進(jìn)程是什么?

  • 孤兒進(jìn)程:父進(jìn)程退出了,而它的一個(gè)或多個(gè)進(jìn)程還在運(yùn)行,那這些子進(jìn)程都會(huì)成為孤兒進(jìn)程。孤兒進(jìn)程將被init進(jìn)程(進(jìn)程號(hào)為1)所收養(yǎng),并由init進(jìn)程對(duì)它們完成狀態(tài)收集工作。
  • 僵尸進(jìn)程:子進(jìn)程比父進(jìn)程先結(jié)束,而父進(jìn)程又沒(méi)有釋放子進(jìn)程占用的資源,那么子進(jìn)程的進(jìn)程描述符仍然保存在系統(tǒng)中,這種進(jìn)程稱(chēng)之為僵死進(jìn)程。

6. 死鎖產(chǎn)生的原因? 如果解決死鎖的問(wèn)題?

所謂死鎖,是指多個(gè)進(jìn)程在運(yùn)行過(guò)程中因爭(zhēng)奪資源而造成的一種僵局,當(dāng)進(jìn)程處于這種僵持狀態(tài)時(shí),若無(wú)外力作用,它們都將無(wú)法再向前推進(jìn)。

系統(tǒng)中的資源可以分為兩類(lèi):

  • 可剝奪資源,是指某進(jìn)程在獲得這類(lèi)資源后,該資源可以再被其他進(jìn)程或系統(tǒng)剝奪,CPU和主存均屬于可剝奪性資源;
  • 不可剝奪資源,當(dāng)系統(tǒng)把這類(lèi)資源分配給某進(jìn)程后,再不能強(qiáng)行收回,只能在進(jìn)程用完后自行釋放,如磁帶機(jī)、打印機(jī)等。

產(chǎn)生死鎖的原因:

(1)競(jìng)爭(zhēng)資源

  • 產(chǎn)生死鎖中的競(jìng)爭(zhēng)資源之一指的是競(jìng)爭(zhēng)不可剝奪資源(例如:系統(tǒng)中只有一臺(tái)打印機(jī),可供進(jìn)程P1使用,假定P1已占用了打印機(jī),若P2繼續(xù)要求打印機(jī)打印將阻塞)
  • 產(chǎn)生死鎖中的競(jìng)爭(zhēng)資源另外一種資源指的是競(jìng)爭(zhēng)臨時(shí)資源(臨時(shí)資源包括硬件中斷、信號(hào)、消息、緩沖區(qū)內(nèi)的消息等),通常消息通信順序進(jìn)行不當(dāng),則會(huì)產(chǎn)生死鎖

(2)進(jìn)程間推進(jìn)順序非法

若P1保持了資源R1,P2保持了資源R2,系統(tǒng)處于不安全狀態(tài),因?yàn)檫@兩個(gè)進(jìn)程再向前推進(jìn),便可能發(fā)生死鎖。例如,當(dāng)P1運(yùn)行到P1:Request(R2)時(shí),將因R2已被P2占用而阻塞;當(dāng)P2運(yùn)行到P2:Request(R1)時(shí),也將因R1已被P1占用而阻塞,于是發(fā)生進(jìn)程死鎖

產(chǎn)生死鎖的必要條件:

  • 互斥條件:進(jìn)程要求對(duì)所分配的資源進(jìn)行排它性控制,即在一段時(shí)間內(nèi)某資源僅為一進(jìn)程所占用。
  • 請(qǐng)求和保持條件:當(dāng)進(jìn)程因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放。
  • 不剝奪條件:進(jìn)程已獲得的資源在未使用完之前,不能剝奪,只能在使用完時(shí)由自己釋放。
  • 環(huán)路等待條件:在發(fā)生死鎖時(shí),必然存在一個(gè)進(jìn)程——資源的環(huán)形鏈。

預(yù)防死鎖的方法:

  • 資源一次性分配:一次性分配所有資源,這樣就不會(huì)再有請(qǐng)求了(破壞請(qǐng)求條件)
  • 只要有一個(gè)資源得不到分配,也不給這個(gè)進(jìn)程分配其他的資源(破壞請(qǐng)保持條件)
  • 可剝奪資源:即當(dāng)某進(jìn)程獲得了部分資源,但得不到其它資源,則釋放已占有的資源(破壞不可剝奪條件)
  • 資源有序分配法:系統(tǒng)給每類(lèi)資源賦予一個(gè)編號(hào),每一個(gè)進(jìn)程按編號(hào)遞增的順序請(qǐng)求資源,釋放則相反(破壞環(huán)路等待條件)

7. 如何實(shí)現(xiàn)瀏覽器內(nèi)多個(gè)標(biāo)簽頁(yè)之間的通信?

實(shí)現(xiàn)多個(gè)標(biāo)簽頁(yè)之間的通信,本質(zhì)上都是通過(guò)中介者模式來(lái)實(shí)現(xiàn)的。因?yàn)闃?biāo)簽頁(yè)之間沒(méi)有辦法直接通信,因此我們可以找一個(gè)中介者,讓標(biāo)簽頁(yè)和中介者進(jìn)行通信,然后讓這個(gè)中介者來(lái)進(jìn)行消息的轉(zhuǎn)發(fā)。通信方法如下:

  • 使用 websocket 協(xié)議,因?yàn)?websocket 協(xié)議可以實(shí)現(xiàn)服務(wù)器推送,所以服務(wù)器就可以用來(lái)當(dāng)做這個(gè)中介者。標(biāo)簽頁(yè)通過(guò)向服務(wù)器發(fā)送數(shù)據(jù),然后由服務(wù)器向其他標(biāo)簽頁(yè)推送轉(zhuǎn)發(fā)。
  • 使用 ShareWorker 的方式,shareWorker 會(huì)在頁(yè)面存在的生命周期內(nèi)創(chuàng)建一個(gè)唯一的線(xiàn)程,并且開(kāi)啟多個(gè)頁(yè)面也只會(huì)使用同一個(gè)線(xiàn)程。這個(gè)時(shí)候共享線(xiàn)程就可以充當(dāng)中介者的角色。標(biāo)簽頁(yè)間通過(guò)共享一個(gè)線(xiàn)程,然后通過(guò)這個(gè)共享的線(xiàn)程來(lái)實(shí)現(xiàn)數(shù)據(jù)的交換。
  • 使用 localStorage 的方式,我們可以在一個(gè)標(biāo)簽頁(yè)對(duì) localStorage 的變化事件進(jìn)行監(jiān)聽(tīng),然后當(dāng)另一個(gè)標(biāo)簽頁(yè)修改數(shù)據(jù)的時(shí)候,我們就可以通過(guò)這個(gè)監(jiān)聽(tīng)事件來(lái)獲取到數(shù)據(jù)。這個(gè)時(shí)候 localStorage 對(duì)象就是充當(dāng)?shù)闹薪檎叩慕巧?/li>
  • 使用 postMessage 方法,如果我們能夠獲得對(duì)應(yīng)標(biāo)簽頁(yè)的引用,就可以使用postMessage 方法,進(jìn)行通信。

8. 對(duì)Service Worker的理解

Service Worker 是運(yùn)行在瀏覽器背后的獨(dú)立線(xiàn)程,一般可以用來(lái)實(shí)現(xiàn)緩存功能。使用 Service Worker的話(huà),傳輸協(xié)議必須為 HTTPS。因?yàn)?Service Worker 中涉及到請(qǐng)求攔截,所以必須使用 HTTPS 協(xié)議來(lái)保障安全。

Service Worker 實(shí)現(xiàn)緩存功能一般分為三個(gè)步驟:首先需要先注冊(cè) Service Worker,然后監(jiān)聽(tīng)到 install 事件以后就可以緩存需要的文件,那么在下次用戶(hù)訪(fǎng)問(wèn)的時(shí)候就可以通過(guò)攔截請(qǐng)求的方式查詢(xún)是否存在緩存,存在緩存的話(huà)就可以直接讀取緩存文件,否則就去請(qǐng)求數(shù)據(jù)。以下是這個(gè)步驟的實(shí)現(xiàn):

// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
      console.log('service worker 注冊(cè)成功')
    })
    .catch(function(err) {
      console.log('servcie worker 注冊(cè)失敗')
    })
}
// sw.js
// 監(jiān)聽(tīng) `install` 事件,回調(diào)中緩存所需文件
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll(['./index.html', './index.js'])
    })
  )
})
// 攔截所有請(qǐng)求事件
// 如果緩存中已經(jīng)有請(qǐng)求的數(shù)據(jù)就直接用緩存,否則去請(qǐng)求數(shù)據(jù)
self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response
      }
      console.log('fetch source')
    })
  )
})

打開(kāi)頁(yè)面,可以在開(kāi)發(fā)者工具中的 Application 看到 Service Worker 已經(jīng)啟動(dòng)了:

image

在 Cache 中也可以發(fā)現(xiàn)所需的文件已被緩存:

image

三、瀏覽器緩存


1. 對(duì)瀏覽器的緩存機(jī)制的理解

瀏覽器緩存的全過(guò)程:

  • 瀏覽器第一次加載資源,服務(wù)器返回 200,瀏覽器從服務(wù)器下載資源文件,并緩存資源文件與 response header,以供下次加載時(shí)對(duì)比使用;
  • 下一次加載資源時(shí),由于強(qiáng)制緩存優(yōu)先級(jí)較高,先比較當(dāng)前時(shí)間與上一次返回 200 時(shí)的時(shí)間差,如果沒(méi)有超過(guò) cache-control 設(shè)置的 max-age,則沒(méi)有過(guò)期,并命中強(qiáng)緩存,直接從本地讀取資源。如果瀏覽器不支持HTTP1.1,則使用 expires 頭判斷是否過(guò)期;
  • 如果資源已過(guò)期,則表明強(qiáng)制緩存沒(méi)有被命中,則開(kāi)始協(xié)商緩存,向服務(wù)器發(fā)送帶有 If-None-Match 和 If-Modified-Since 的請(qǐng)求;
  • 服務(wù)器收到請(qǐng)求后,優(yōu)先根據(jù) Etag 的值判斷被請(qǐng)求的文件有沒(méi)有做修改,Etag 值一致則沒(méi)有修改,命中協(xié)商緩存,返回 304;如果不一致則有改動(dòng),直接返回新的資源文件帶上新的 Etag 值并返回 200;
  • 如果服務(wù)器收到的請(qǐng)求沒(méi)有 Etag 值,則將 If-Modified-Since 和被請(qǐng)求文件的最后修改時(shí)間做比對(duì),一致則命中協(xié)商緩存,返回 304;不一致則返回新的 last-modified 和文件并返回 200;


很多網(wǎng)站的資源后面都加了版本號(hào),這樣做的目的是:每次升級(jí)了 JS 或 CSS 文件后,為了防止瀏覽器進(jìn)行緩存,強(qiáng)制改變版本號(hào),客戶(hù)端瀏覽器就會(huì)重新下載新的 JS 或 CSS 文件 ,以保證用戶(hù)能夠及時(shí)獲得網(wǎng)站的最新更新。

2. 瀏覽器資源緩存的位置有哪些?

資源緩存的位置一共有 3 種,按優(yōu)先級(jí)從高到低分別是:

  1. Service WorkerService Worker 運(yùn)行在 JavaScript 主線(xiàn)程之外,雖然由于脫離了瀏覽器窗體無(wú)法直接訪(fǎng)問(wèn) DOM,但是它可以完成離線(xiàn)緩存、消息推送、網(wǎng)絡(luò)代理等功能。它可以讓我們自由控制緩存哪些文件、如何匹配緩存、如何讀取緩存,并且緩存是持續(xù)性的。當(dāng) Service Worker 沒(méi)有命中緩存的時(shí)候,需要去調(diào)用 ?fetch ?函數(shù)獲取 數(shù)據(jù)。也就是說(shuō),如果沒(méi)有在 Service Worker 命中緩存,會(huì)根據(jù)緩存查找優(yōu)先級(jí)去查找數(shù)據(jù)。但是不管是從 Memory Cache 中還是從網(wǎng)絡(luò)請(qǐng)求中獲取的數(shù)據(jù),瀏覽器都會(huì)顯示是從 Service Worker 中獲取的內(nèi)容。
  2. Memory Cache:Memory Cache 就是內(nèi)存緩存,它的效率最快,但是內(nèi)存緩存雖然讀取高效,可是緩存持續(xù)性很短,會(huì)隨著進(jìn)程的釋放而釋放。一旦我們關(guān)閉 Tab 頁(yè)面,內(nèi)存中的緩存也就被釋放了。
  3. Disk Cache:Disk Cache 也就是存儲(chǔ)在硬盤(pán)中的緩存,讀取速度慢點(diǎn),但是什么都能存儲(chǔ)到磁盤(pán)中,比之 Memory Cache 勝在容量和存儲(chǔ)時(shí)效性上。在所有瀏覽器緩存中,Disk Cache 覆蓋面基本是最大的。它會(huì)根據(jù) HTTP Herder 中的字段判斷哪些資源需要緩存,哪些資源可以不請(qǐng)求直接使用,哪些資源已經(jīng)過(guò)期需要重新請(qǐng)求。并且即使在跨站點(diǎn)的情況下,相同地址的資源一旦被硬盤(pán)緩存下來(lái),就不會(huì)再次去請(qǐng)求數(shù)據(jù)。

Disk Cache:Push Cache 是 HTTP/2 中的內(nèi)容,當(dāng)以上三種緩存都沒(méi)有命中時(shí),它才會(huì)被使用。并且緩存時(shí)間也很短暫,只在會(huì)話(huà)(Session)中存在,一旦會(huì)話(huà)結(jié)束就被釋放。其具有以下特點(diǎn):

  • 所有的資源都能被推送,但是 Edge 和 Safari 瀏覽器兼容性不怎么好
  • 可以推送 ?no-cache? 和 ?no-store? 的資源
  • 一旦連接被關(guān)閉,Push Cache 就被釋放
  • 多個(gè)頁(yè)面可以使用相同的 HTTP/2 連接,也就是說(shuō)能使用同樣的緩存
  • Push Cache 中的緩存只能被使用一次
  • 瀏覽器可以拒絕接受已經(jīng)存在的資源推送
  • 可以給其他域名推送資源

3. 協(xié)商緩存和強(qiáng)緩存的區(qū)別

(1)強(qiáng)緩存

使用強(qiáng)緩存策略時(shí),如果緩存資源有效,則直接使用緩存資源,不必再向服務(wù)器發(fā)起請(qǐng)求。

強(qiáng)緩存策略可以通過(guò)兩種方式來(lái)設(shè)置,分別是 http 頭信息中的 Expires 屬性和 Cache-Control 屬性。

(1)服務(wù)器通過(guò)在響應(yīng)頭中添加 Expires 屬性,來(lái)指定資源的過(guò)期時(shí)間。在過(guò)期時(shí)間以?xún)?nèi),該資源可以被緩存使用,不必再向服務(wù)器發(fā)送請(qǐng)求。這個(gè)時(shí)間是一個(gè)絕對(duì)時(shí)間,它是服務(wù)器的時(shí)間,因此可能存在這樣的問(wèn)題,就是客戶(hù)端的時(shí)間和服務(wù)器端的時(shí)間不一致,或者用戶(hù)可以對(duì)客戶(hù)端時(shí)間進(jìn)行修改的情況,這樣就可能會(huì)影響緩存命中的結(jié)果。

(2)Expires 是 http1.0 中的方式,因?yàn)樗囊恍┤秉c(diǎn),在 HTTP 1.1 中提出了一個(gè)新的頭部屬性就是 Cache-Control 屬性,它提供了對(duì)資源的緩存的更精確的控制。它有很多不同的值,

Cache-Control可設(shè)置的字段:

  • ?public?:設(shè)置了該字段值的資源表示可以被任何對(duì)象(包括:發(fā)送請(qǐng)求的客戶(hù)端、代理服務(wù)器等等)緩存。這個(gè)字段值不常用,一般還是使用max-age=來(lái)精確控制;
  • ?private?:設(shè)置了該字段值的資源只能被用戶(hù)瀏覽器緩存,不允許任何代理服務(wù)器緩存。在實(shí)際開(kāi)發(fā)當(dāng)中,對(duì)于一些含有用戶(hù)信息的HTML,通常都要設(shè)置這個(gè)字段值,避免代理服務(wù)器(CDN)緩存;
  • ?no-cache?:設(shè)置了該字段需要先和服務(wù)端確認(rèn)返回的資源是否發(fā)生了變化,如果資源未發(fā)生變化,則直接使用緩存好的資源;
  • ?no-store?:設(shè)置了該字段表示禁止任何緩存,每次都會(huì)向服務(wù)端發(fā)起新的請(qǐng)求,拉取最新的資源;
  • ?max-age=?:設(shè)置緩存的最大有效期,單位為秒;
  • ?s-maxage=?:優(yōu)先級(jí)高于max-age=,僅適用于共享緩存(CDN),優(yōu)先級(jí)高于max-age或者Expires頭;
  • ?max-stale[=]?:設(shè)置了該字段表明客戶(hù)端愿意接收已經(jīng)過(guò)期的資源,但是不能超過(guò)給定的時(shí)間限制。

一般來(lái)說(shuō)只需要設(shè)置其中一種方式就可以實(shí)現(xiàn)強(qiáng)緩存策略,當(dāng)兩種方式一起使用時(shí),Cache-Control 的優(yōu)先級(jí)要高于 Expires。

no-cache和no-store很容易混淆:

  • no-cache 是指先要和服務(wù)器確認(rèn)是否有資源更新,在進(jìn)行判斷。也就是說(shuō)沒(méi)有強(qiáng)緩存,但是會(huì)有協(xié)商緩存;
  • no-store 是指不使用任何緩存,每次請(qǐng)求都直接從服務(wù)器獲取資源。

(2)協(xié)商緩存

如果命中強(qiáng)制緩存,我們無(wú)需發(fā)起新的請(qǐng)求,直接使用緩存內(nèi)容,如果沒(méi)有命中強(qiáng)制緩存,如果設(shè)置了協(xié)商緩存,這個(gè)時(shí)候協(xié)商緩存就會(huì)發(fā)揮作用了。

上面已經(jīng)說(shuō)到了,命中協(xié)商緩存的條件有兩個(gè):

  • ?max-age=xxx? 過(guò)期了
  • 值為 ?no-cache?

使用協(xié)商緩存策略時(shí),會(huì)先向服務(wù)器發(fā)送一個(gè)請(qǐng)求,如果資源沒(méi)有發(fā)生修改,則返回一個(gè) 304 狀態(tài),讓瀏覽器使用本地的緩存副本。如果資源發(fā)生了修改,則返回修改后的資源。

協(xié)商緩存也可以通過(guò)兩種方式來(lái)設(shè)置,分別是 http 頭信息中的 Etag 和 Last-Modified 屬性。

(1)服務(wù)器通過(guò)在響應(yīng)頭中添加 Last-Modified 屬性來(lái)指出資源最后一次修改的時(shí)間,當(dāng)瀏覽器下一次發(fā)起請(qǐng)求時(shí),會(huì)在請(qǐng)求頭中添加一個(gè) If-Modified-Since 的屬性,屬性值為上一次資源返回時(shí)的 Last-Modified 的值。當(dāng)請(qǐng)求發(fā)送到服務(wù)器后服務(wù)器會(huì)通過(guò)這個(gè)屬性來(lái)和資源的最后一次的修改時(shí)間來(lái)進(jìn)行比較,以此來(lái)判斷資源是否做了修改。如果資源沒(méi)有修改,那么返回 304 狀態(tài),讓客戶(hù)端使用本地的緩存。如果資源已經(jīng)被修改了,則返回修改后的資源。使用這種方法有一個(gè)缺點(diǎn),就是 Last-Modified 標(biāo)注的最后修改時(shí)間只能精確到秒級(jí),如果某些文件在1秒鐘以?xún)?nèi),被修改多次的話(huà),那么文件已將改變了但是 Last-Modified 卻沒(méi)有改變,這樣會(huì)造成緩存命中的不準(zhǔn)確。

(2)因?yàn)?Last-Modified 的這種可能發(fā)生的不準(zhǔn)確性,http 中提供了另外一種方式,那就是 Etag 屬性。服務(wù)器在返回資源的時(shí)候,在頭信息中添加了 Etag 屬性,這個(gè)屬性是資源生成的唯一標(biāo)識(shí)符,當(dāng)資源發(fā)生改變的時(shí)候,這個(gè)值也會(huì)發(fā)生改變。在下一次資源請(qǐng)求時(shí),瀏覽器會(huì)在請(qǐng)求頭中添加一個(gè) If-None-Match 屬性,這個(gè)屬性的值就是上次返回的資源的 Etag 的值。服務(wù)接收到請(qǐng)求后會(huì)根據(jù)這個(gè)值來(lái)和資源當(dāng)前的 Etag 的值來(lái)進(jìn)行比較,以此來(lái)判斷資源是否發(fā)生改變,是否需要返回資源。通過(guò)這種方式,比 Last-Modified 的方式更加精確。

當(dāng) Last-Modified 和 Etag 屬性同時(shí)出現(xiàn)的時(shí)候,Etag 的優(yōu)先級(jí)更高。使用協(xié)商緩存的時(shí)候,服務(wù)器需要考慮負(fù)載平衡的問(wèn)題,因此多個(gè)服務(wù)器上資源的 Last-Modified 應(yīng)該保持一致,因?yàn)槊總€(gè)服務(wù)器上 Etag 的值都不一樣,因此在考慮負(fù)載平衡時(shí),最好不要設(shè)置 Etag 屬性。

總結(jié):

強(qiáng)緩存策略和協(xié)商緩存策略在緩存命中時(shí)都會(huì)直接使用本地的緩存副本,區(qū)別只在于協(xié)商緩存會(huì)向服務(wù)器發(fā)送一次請(qǐng)求。它們緩存不命中時(shí),都會(huì)向服務(wù)器發(fā)送請(qǐng)求來(lái)獲取資源。在實(shí)際的緩存機(jī)制中,強(qiáng)緩存策略和協(xié)商緩存策略是一起合作使用的。瀏覽器首先會(huì)根據(jù)請(qǐng)求的信息判斷,強(qiáng)緩存是否命中,如果命中則直接使用資源。如果不命中則根據(jù)頭信息向服務(wù)器發(fā)起請(qǐng)求,使用協(xié)商緩存,如果協(xié)商緩存命中的話(huà),則服務(wù)器不返回資源,瀏覽器直接使用本地資源的副本,如果協(xié)商緩存不命中,則瀏覽器返回最新的資源給瀏覽器。

4. 為什么需要瀏覽器緩存?

對(duì)于瀏覽器的緩存,主要針對(duì)的是前端的靜態(tài)資源,最好的效果就是,在發(fā)起請(qǐng)求之后,拉取相應(yīng)的靜態(tài)資源,并保存在本地。如果服務(wù)器的靜態(tài)資源沒(méi)有更新,那么在下次請(qǐng)求的時(shí)候,就直接從本地讀取即可,如果服務(wù)器的靜態(tài)資源已經(jīng)更新,那么我們?cè)俅握?qǐng)求的時(shí)候,就到服務(wù)器拉取新的資源,并保存在本地。這樣就大大的減少了請(qǐng)求的次數(shù),提高了網(wǎng)站的性能。這就要用到瀏覽器的緩存策略了。

所謂的瀏覽器緩存指的是瀏覽器將用戶(hù)請(qǐng)求過(guò)的靜態(tài)資源,存儲(chǔ)到電腦本地磁盤(pán)中,當(dāng)瀏覽器再次訪(fǎng)問(wèn)時(shí),就可以直接從本地加載,不需要再去服務(wù)端請(qǐng)求了。

使用瀏覽器緩存,有以下優(yōu)點(diǎn):

  • 減少了服務(wù)器的負(fù)擔(dān),提高了網(wǎng)站的性能
  • 加快了客戶(hù)端網(wǎng)頁(yè)的加載速度
  • 減少了多余網(wǎng)絡(luò)數(shù)據(jù)傳輸

5. 點(diǎn)擊刷新按鈕或者按 F5、按 Ctrl+F5 (強(qiáng)制刷新)、地址欄回車(chē)有什么區(qū)別?

  • 點(diǎn)擊刷新按鈕或者按 F5:瀏覽器直接對(duì)本地的緩存文件過(guò)期,但是會(huì)帶上If-Modifed-Since,If-None-Match,這就意味著服務(wù)器會(huì)對(duì)文件檢查新鮮度,返回結(jié)果可能是 304,也有可能是 200。
  • 用戶(hù)按 Ctrl+F5(強(qiáng)制刷新):瀏覽器不僅會(huì)對(duì)本地文件過(guò)期,而且不會(huì)帶上 If-Modifed-Since,If-None-Match,相當(dāng)于之前從來(lái)沒(méi)有請(qǐng)求過(guò),返回結(jié)果是 200。
  • 地址欄回車(chē): 瀏覽器發(fā)起請(qǐng)求,按照正常流程,本地檢查是否過(guò)期,然后服務(wù)器檢查新鮮度,最后返回內(nèi)容。

四、瀏覽器組成


1. 對(duì)瀏覽器的理解

瀏覽器的主要功能是將用戶(hù)選擇的 web 資源呈現(xiàn)出來(lái),它需要從服務(wù)器請(qǐng)求資源,并將其顯示在瀏覽器窗口中,資源的格式通常是 HTML,也包括 PDF、image 及其他格式。用戶(hù)用 URI(Uniform Resource Identifier 統(tǒng)一資源標(biāo)識(shí)符)來(lái)指定所請(qǐng)求資源的位置。

HTML 和 CSS 規(guī)范中規(guī)定了瀏覽器解釋 html 文檔的方式,由 W3C 組織對(duì)這些規(guī)范進(jìn)行維護(hù),W3C 是負(fù)責(zé)制定 web 標(biāo)準(zhǔn)的組織。但是瀏覽器廠商紛紛開(kāi)發(fā)自己的擴(kuò)展,對(duì)規(guī)范的遵循并不完善,這為 web 開(kāi)發(fā)者帶來(lái)了嚴(yán)重的兼容性問(wèn)題。

瀏覽器可以分為兩部分,shell 和 內(nèi)核。其中 shell 的種類(lèi)相對(duì)比較多,內(nèi)核則比較少。也有一些瀏覽器并不區(qū)分外殼和內(nèi)核。從 Mozilla 將 Gecko 獨(dú)立出來(lái)后,才有了外殼和內(nèi)核的明確劃分。

  • shell 是指瀏覽器的外殼:例如菜單,工具欄等。主要是提供給用戶(hù)界面操作,參數(shù)設(shè)置等等。它是調(diào)用內(nèi)核來(lái)實(shí)現(xiàn)各種功能的。
  • 內(nèi)核是瀏覽器的核心。內(nèi)核是基于標(biāo)記語(yǔ)言顯示內(nèi)容的程序或模塊。

2. 對(duì)瀏覽器內(nèi)核的理解

瀏覽器內(nèi)核主要分成兩部分:

  • 渲染引擎的職責(zé)就是渲染,即在瀏覽器窗口中顯示所請(qǐng)求的內(nèi)容。默認(rèn)情況下,渲染引擎可以顯示 html、xml 文檔及圖片,它也可以借助插件顯示其他類(lèi)型數(shù)據(jù),例如使用 PDF 閱讀器插件,可以顯示 PDF 格式。
  • JS 引擎:解析和執(zhí)行 javascript 來(lái)實(shí)現(xiàn)網(wǎng)頁(yè)的動(dòng)態(tài)效果。

最開(kāi)始渲染引擎和 JS 引擎并沒(méi)有區(qū)分的很明確,后來(lái) JS 引擎越來(lái)越獨(dú)立,內(nèi)核就傾向于只指渲染引擎。

3. 常見(jiàn)的瀏覽器內(nèi)核比較

  • Trident:這種瀏覽器內(nèi)核是 IE 瀏覽器用的內(nèi)核,因?yàn)樵谠缙?IE 占有大量的市場(chǎng)份額,所以這種內(nèi)核比較流行,以前有很多網(wǎng)頁(yè)也是根據(jù)這個(gè)內(nèi)核的標(biāo)準(zhǔn)來(lái)編寫(xiě)的,但是實(shí)際上這個(gè)內(nèi)核對(duì)真正的網(wǎng)頁(yè)標(biāo)準(zhǔn)支持不是很好。但是由于 IE 的高市場(chǎng)占有率,微軟也很長(zhǎng)時(shí)間沒(méi)有更新 Trident 內(nèi)核,就導(dǎo)致了 Trident 內(nèi)核和 W3C 標(biāo)準(zhǔn)脫節(jié)。還有就是 Trident 內(nèi)核的大量 Bug 等安全問(wèn)題沒(méi)有得到解決,加上一些專(zhuān)家學(xué)者公開(kāi)自己認(rèn)為 IE 瀏覽器不安全的觀點(diǎn),使很多用戶(hù)開(kāi)始轉(zhuǎn)向其他瀏覽器。
  • Gecko:這是 Firefox 和 Flock 所采用的內(nèi)核,這個(gè)內(nèi)核的優(yōu)點(diǎn)就是功能強(qiáng)大、豐富,可以支持很多復(fù)雜網(wǎng)頁(yè)效果和瀏覽器擴(kuò)展接口,但是代價(jià)是也顯而易見(jiàn)就是要消耗很多的資源,比如內(nèi)存。
  • Presto:Opera 曾經(jīng)采用的就是 Presto 內(nèi)核,Presto 內(nèi)核被稱(chēng)為公認(rèn)的瀏覽網(wǎng)頁(yè)速度最快的內(nèi)核,這得益于它在開(kāi)發(fā)時(shí)的天生優(yōu)勢(shì),在處理 JS 腳本等腳本語(yǔ)言時(shí),會(huì)比其他的內(nèi)核快3倍左右,缺點(diǎn)就是為了達(dá)到很快的速度而丟掉了一部分網(wǎng)頁(yè)兼容性。
  • Webkit:Webkit 是 Safari 采用的內(nèi)核,它的優(yōu)點(diǎn)就是網(wǎng)頁(yè)瀏覽速度較快,雖然不及 Presto 但是也勝于 Gecko 和 Trident,缺點(diǎn)是對(duì)于網(wǎng)頁(yè)代碼的容錯(cuò)性不高,也就是說(shuō)對(duì)網(wǎng)頁(yè)代碼的兼容性較低,會(huì)使一些編寫(xiě)不標(biāo)準(zhǔn)的網(wǎng)頁(yè)無(wú)法正確顯示。WebKit 前身是 KDE 小組的 KHTML 引擎,可以說(shuō) WebKit 是 KHTML 的一個(gè)開(kāi)源的分支。
  • Blink:谷歌在 Chromium Blog 上發(fā)表博客,稱(chēng)將與蘋(píng)果的開(kāi)源瀏覽器核心 Webkit 分道揚(yáng)鑣,在 Chromium 項(xiàng)目中研發(fā) Blink 渲染引擎(即瀏覽器核心),內(nèi)置于 Chrome 瀏覽器之中。其實(shí) Blink 引擎就是 Webkit 的一個(gè)分支,就像 webkit 是KHTML 的分支一樣。Blink 引擎現(xiàn)在是谷歌公司與 Opera Software 共同研發(fā),上面提到過(guò)的,Opera 棄用了自己的 Presto 內(nèi)核,加入 Google 陣營(yíng),跟隨谷歌一起研發(fā) Blink。

4. 常見(jiàn)瀏覽器所用內(nèi)核

(1) IE 瀏覽器內(nèi)核:Trident 內(nèi)核,也是俗稱(chēng)的 IE 內(nèi)核;

(2) Chrome 瀏覽器內(nèi)核:統(tǒng)稱(chēng)為 Chromium 內(nèi)核或 Chrome 內(nèi)核,以前是 Webkit 內(nèi)核,現(xiàn)在是 Blink內(nèi)核;

(3) Firefox 瀏覽器內(nèi)核:Gecko 內(nèi)核,俗稱(chēng) Firefox 內(nèi)核;

(4) Safari 瀏覽器內(nèi)核:Webkit 內(nèi)核;

(5) Opera 瀏覽器內(nèi)核:最初是自己的 Presto 內(nèi)核,后來(lái)加入谷歌大軍,從 Webkit 又到了 Blink 內(nèi)核;

(6) 360瀏覽器、獵豹瀏覽器內(nèi)核:IE + Chrome 雙內(nèi)核;

(7) 搜狗、遨游、QQ 瀏覽器內(nèi)核:Trident(兼容模式)+ Webkit(高速模式);

(8) 百度瀏覽器、世界之窗內(nèi)核:IE 內(nèi)核;

(9) 2345瀏覽器內(nèi)核:好像以前是 IE 內(nèi)核,現(xiàn)在也是 IE + Chrome 雙內(nèi)核了;

(10)UC 瀏覽器內(nèi)核:這個(gè)眾口不一,UC 說(shuō)是他們自己研發(fā)的 U3 內(nèi)核,但好像還是基于 Webkit 和 Trident ,還有說(shuō)是基于火狐內(nèi)核。

5. 瀏覽器的主要組成部分

  • 用戶(hù)界面 - 包括地址欄、前進(jìn)/后退按鈕、書(shū)簽菜單等。除了瀏覽器主窗?顯示的您請(qǐng)求的??外,其他顯示的各個(gè)部分都屬于?戶(hù)界?。
  • 瀏覽器引擎 - 在?戶(hù)界?和呈現(xiàn)引擎之間傳送指令。
  • 呈現(xiàn)引擎 - 負(fù)責(zé)顯示請(qǐng)求的內(nèi)容。如果請(qǐng)求的內(nèi)容是 HTML,它就負(fù)責(zé)解析 HTML 和 CSS 內(nèi)容,并將解析后的內(nèi)容顯示在屏幕上。
  • 網(wǎng)絡(luò) - ?于?絡(luò)調(diào)?,?如 HTTP 請(qǐng)求。其接?與平臺(tái)?關(guān),并為所有平臺(tái)提供底層實(shí)現(xiàn)。
  • 用戶(hù)界面后端 - ?于繪制基本的窗??部件,?如組合框和窗?。其公開(kāi)了與平臺(tái)?關(guān)的通?接?,?在底層使?操作系統(tǒng)的?戶(hù)界??法。
  • JavaScript解釋器。?于解析和執(zhí)? JavaScript 代碼。
  • 數(shù)據(jù)存儲(chǔ) - 這是持久層。瀏覽器需要在硬盤(pán)上保存各種數(shù)據(jù),例如 Cookie。新的 HTML 規(guī)范 (HTML5) 定義了“?絡(luò)數(shù)據(jù)庫(kù)”,這是?個(gè)完整(但是輕便)的瀏覽器內(nèi)數(shù)據(jù)庫(kù)。

值得注意的是,和?多數(shù)瀏覽器不同,Chrome 瀏覽器的每個(gè)標(biāo)簽?都分別對(duì)應(yīng)?個(gè)呈現(xiàn)引擎實(shí)例。每個(gè)標(biāo)簽?都是?個(gè)獨(dú)?的進(jìn)程。

五、瀏覽器渲染原理


1. 瀏覽器的渲染過(guò)程

瀏覽器渲染主要有以下步驟:

  • 首先解析收到的文檔,根據(jù)文檔定義構(gòu)建一棵 DOM 樹(shù),DOM 樹(shù)是由 DOM 元素及屬性節(jié)點(diǎn)組成的。
  • 然后對(duì) CSS 進(jìn)行解析,生成 CSSOM 規(guī)則樹(shù)。
  • 根據(jù) DOM 樹(shù)和 CSSOM 規(guī)則樹(shù)構(gòu)建渲染樹(shù)。渲染樹(shù)的節(jié)點(diǎn)被稱(chēng)為渲染對(duì)象,渲染對(duì)象是一個(gè)包含有顏色和大小等屬性的矩形,渲染對(duì)象和 DOM 元素相對(duì)應(yīng),但這種對(duì)應(yīng)關(guān)系不是一對(duì)一的,不可見(jiàn)的 DOM 元素不會(huì)被插入渲染樹(shù)。還有一些 DOM元素對(duì)應(yīng)幾個(gè)可見(jiàn)對(duì)象,它們一般是一些具有復(fù)雜結(jié)構(gòu)的元素,無(wú)法用一個(gè)矩形來(lái)描述。
  • 當(dāng)渲染對(duì)象被創(chuàng)建并添加到樹(shù)中,它們并沒(méi)有位置和大小,所以當(dāng)瀏覽器生成渲染樹(shù)以后,就會(huì)根據(jù)渲染樹(shù)來(lái)進(jìn)行布局(也可以叫做回流)。這一階段瀏覽器要做的事情是要弄清楚各個(gè)節(jié)點(diǎn)在頁(yè)面中的確切位置和大小。通常這一行為也被稱(chēng)為“自動(dòng)重排”。
  • 布局階段結(jié)束后是繪制階段,遍歷渲染樹(shù)并調(diào)用渲染對(duì)象的 paint 方法將它們的內(nèi)容顯示在屏幕上,繪制使用 UI 基礎(chǔ)組件。

大致過(guò)程如圖所示:


注意:這個(gè)過(guò)程是逐步完成的,為了更好的用戶(hù)體驗(yàn),渲染引擎將會(huì)盡可能早的將內(nèi)容呈現(xiàn)到屏幕上,并不會(huì)等到所有的html 都解析完成之后再去構(gòu)建和布局 render 樹(shù)。它是解析完一部分內(nèi)容就顯示一部分內(nèi)容,同時(shí),可能還在通過(guò)網(wǎng)絡(luò)下載其余內(nèi)容。

2. 瀏覽器渲染優(yōu)化

(1)針對(duì)JavaScript:JavaScript既會(huì)阻塞HTML的解析,也會(huì)阻塞CSS的解析。因此我們可以對(duì)JavaScript的加載方式進(jìn)行改變,來(lái)進(jìn)行優(yōu)化:

(1)盡量將JavaScript文件放在body的最后

(2) body中間盡量不要寫(xiě) ?<script>?標(biāo)簽

(3)?<script>?標(biāo)簽的引入資源方式有三種,有一種就是我們常用的直接引入,還有兩種就是使用 async 屬性和 defer 屬性來(lái)異步引入,兩者都是去異步加載外部的JS文件,不會(huì)阻塞DOM的解析(盡量使用異步加載)。三者的區(qū)別如下:

  • script 立即停止頁(yè)面渲染去加載資源文件,當(dāng)資源加載完畢后立即執(zhí)行js代碼,js代碼執(zhí)行完畢后繼續(xù)渲染頁(yè)面;
  • async 是在下載完成之后,立即異步加載,加載好后立即執(zhí)行,多個(gè)帶async屬性的標(biāo)簽,不能保證加載的順序;
  • defer 是在下載完成之后,立即異步加載。加載好后,如果 DOM 樹(shù)還沒(méi)構(gòu)建好,則先等 DOM 樹(shù)解析好再執(zhí)行;如果DOM樹(shù)已經(jīng)準(zhǔn)備好,則立即執(zhí)行。多個(gè)帶defer屬性的標(biāo)簽,按照順序執(zhí)行。

(2)針對(duì)CSS:使用CSS有三種方式:使用link、@import、內(nèi)聯(lián)樣式,其中l(wèi)ink和@import都是導(dǎo)入外部樣式。它們之間的區(qū)別:

  • link:瀏覽器會(huì)派發(fā)一個(gè)新等線(xiàn)程(HTTP線(xiàn)程)去加載資源文件,與此同時(shí)GUI渲染線(xiàn)程會(huì)繼續(xù)向下渲染代碼
  • @import:GUI渲染線(xiàn)程會(huì)暫時(shí)停止渲染,去服務(wù)器加載資源文件,資源文件沒(méi)有返回之前不會(huì)繼續(xù)渲染(阻礙瀏覽器渲染)
  • style:GUI直接渲染

外部樣式如果長(zhǎng)時(shí)間沒(méi)有加載完畢,瀏覽器為了用戶(hù)體驗(yàn),會(huì)使用瀏覽器會(huì)默認(rèn)樣式,確保首次渲染的速度。所以CSS一般寫(xiě)在headr中,讓瀏覽器盡快發(fā)送請(qǐng)求去獲取css樣式。

所以,在開(kāi)發(fā)過(guò)程中,導(dǎo)入外部樣式使用link,而不用@import。如果css少,盡可能采用內(nèi)嵌樣式,直接寫(xiě)在style標(biāo)簽中。

(3)針對(duì)DOM樹(shù)、CSSOM樹(shù):

可以通過(guò)以下幾種方式來(lái)減少渲染的時(shí)間:

  • HTML文件的代碼層級(jí)盡量不要太深
  • 使用語(yǔ)義化的標(biāo)簽,來(lái)避免不標(biāo)準(zhǔn)語(yǔ)義化的特殊處理
  • 減少CSSD代碼的層級(jí),因?yàn)檫x擇器是從右向左進(jìn)行解析的

(4)減少回流與重繪:

  • 操作DOM時(shí),盡量在低層級(jí)的DOM節(jié)點(diǎn)進(jìn)行操作
  • 不要使用 ?table?布局, 一個(gè)小的改動(dòng)可能會(huì)使整個(gè) ?table?進(jìn)行重新布局
  • 使用CSS的表達(dá)式
  • 不要頻繁操作元素的樣式,對(duì)于靜態(tài)頁(yè)面,可以修改類(lèi)名,而不是樣式。
  • 使用absolute或者fixed,使元素脫離文檔流,這樣他們發(fā)生變化就不會(huì)影響其他元素
  • 避免頻繁操作DOM,可以創(chuàng)建一個(gè)文檔片段 ?documentFragment?,在它上面應(yīng)用所有DOM操作,最后再把它添加到文檔中
  • 將元素先設(shè)置 ?display: none?,操作結(jié)束后再把它顯示出來(lái)。因?yàn)樵赿isplay屬性為none的元素上進(jìn)行的DOM操作不會(huì)引發(fā)回流和重繪。
  • 將DOM的多個(gè)讀操作(或者寫(xiě)操作)放在一起,而不是讀寫(xiě)操作穿插著寫(xiě)。這得益于瀏覽器的渲染隊(duì)列機(jī)制。

瀏覽器針對(duì)頁(yè)面的回流與重繪,進(jìn)行了自身的優(yōu)化——渲染隊(duì)列

瀏覽器會(huì)將所有的回流、重繪的操作放在一個(gè)隊(duì)列中,當(dāng)隊(duì)列中的操作到了一定的數(shù)量或者到了一定的時(shí)間間隔,瀏覽器就會(huì)對(duì)隊(duì)列進(jìn)行批處理。這樣就會(huì)讓多次的回流、重繪變成一次回流重繪。

將多個(gè)讀操作(或者寫(xiě)操作)放在一起,就會(huì)等所有的讀操作進(jìn)入隊(duì)列之后執(zhí)行,這樣,原本應(yīng)該是觸發(fā)多次回流,變成了只觸發(fā)一次回流。

3. 渲染過(guò)程中遇到 JS 文件如何處理?

JavaScript 的加載、解析與執(zhí)行會(huì)阻塞文檔的解析,也就是說(shuō),在構(gòu)建 DOM 時(shí),HTML 解析器若遇到了 JavaScript,那么它會(huì)暫停文檔的解析,將控制權(quán)移交給 JavaScript 引擎,等 JavaScript 引擎運(yùn)行完畢,瀏覽器再?gòu)闹袛嗟牡胤交謴?fù)繼續(xù)解析文檔。也就是說(shuō),如果想要首屏渲染的越快,就越不應(yīng)該在首屏就加載 JS 文件,這也是都建議將 script 標(biāo)簽放在 body 標(biāo)簽底部的原因。當(dāng)然在當(dāng)下,并不是說(shuō) script 標(biāo)簽必須放在底部,因?yàn)槟憧梢越o script 標(biāo)簽添加 defer 或者 async 屬性。

4. 什么是文檔的預(yù)解析?

Webkit 和 Firefox 都做了這個(gè)優(yōu)化,當(dāng)執(zhí)行 JavaScript 腳本時(shí),另一個(gè)線(xiàn)程解析剩下的文檔,并加載后面需要通過(guò)網(wǎng)絡(luò)加載的資源。這種方式可以使資源并行加載從而使整體速度更快。需要注意的是,預(yù)解析并不改變 DOM 樹(shù),它將這個(gè)工作留給主解析過(guò)程,自己只解析外部資源的引用,比如外部腳本、樣式表及圖片。

5. CSS 如何阻塞文檔解析?

理論上,既然樣式表不改變 DOM 樹(shù),也就沒(méi)有必要停下文檔的解析等待它們。然而,存在一個(gè)問(wèn)題,JavaScript 腳本執(zhí)行時(shí)可能在文檔的解析過(guò)程中請(qǐng)求樣式信息,如果樣式還沒(méi)有加載和解析,腳本將得到錯(cuò)誤的值,顯然這將會(huì)導(dǎo)致很多問(wèn)題。所以如果瀏覽器尚未完成 CSSOM 的下載和構(gòu)建,而我們卻想在此時(shí)運(yùn)行腳本,那么瀏覽器將延遲 JavaScript 腳本執(zhí)行和文檔的解析,直至其完成 CSSOM 的下載和構(gòu)建。也就是說(shuō),在這種情況下,瀏覽器會(huì)先下載和構(gòu)建 CSSOM,然后再執(zhí)行 JavaScript,最后再繼續(xù)文檔的解析。

6. 如何優(yōu)化關(guān)鍵渲染路徑?

為盡快完成首次渲染,我們需要最大限度減小以下三種可變因素:

(1)關(guān)鍵資源的數(shù)量。

(2)關(guān)鍵路徑長(zhǎng)度。

(3)關(guān)鍵字節(jié)的數(shù)量。

關(guān)鍵資源是可能阻止網(wǎng)頁(yè)首次渲染的資源。這些資源越少,瀏覽器的工作量就越小,對(duì) CPU 以及其他資源的占用也就越少。同樣,關(guān)鍵路徑長(zhǎng)度受所有關(guān)鍵資源與其字節(jié)大小之間依賴(lài)關(guān)系圖的影響:某些資源只能在上一資源處理完畢之后才能開(kāi)始下載,并且資源越大,下載所需的往返次數(shù)就越多。最后,瀏覽器需要下載的關(guān)鍵字節(jié)越少,處理內(nèi)容并讓其出現(xiàn)在屏幕上的速度就越快。要減少字節(jié)數(shù),我們可以減少資源數(shù)(將它們刪除或設(shè)為非關(guān)鍵資源),此外還要壓縮和優(yōu)化各項(xiàng)資源,確保最大限度減小傳送大小。

優(yōu)化關(guān)鍵渲染路徑的常規(guī)步驟如下:

(1)對(duì)關(guān)鍵路徑進(jìn)行分析和特性描述:資源數(shù)、字節(jié)數(shù)、長(zhǎng)度。

(2)最大限度減少關(guān)鍵資源的數(shù)量:刪除它們,延遲它們的下載,將它們標(biāo)記為異步等。

(3)優(yōu)化關(guān)鍵字節(jié)數(shù)以縮短下載時(shí)間(往返次數(shù))。

(4)優(yōu)化其余關(guān)鍵資源的加載順序:您需要盡早下載所有關(guān)鍵資產(chǎn),以縮短關(guān)鍵路徑長(zhǎng)度

7. 什么情況會(huì)阻塞渲染?

首先渲染的前提是生成渲染樹(shù),所以 HTML 和 CSS 肯定會(huì)阻塞渲染。如果你想渲染的越快,你越應(yīng)該降低一開(kāi)始需要渲染的文件大小,并且扁平層級(jí),優(yōu)化選擇器。然后當(dāng)瀏覽器在解析到 script 標(biāo)簽時(shí),會(huì)暫停構(gòu)建 DOM,完成后才會(huì)從暫停的地方重新開(kāi)始。也就是說(shuō),如果你想首屏渲染的越快,就越不應(yīng)該在首屏就加載 JS 文件,這也是都建議將 script 標(biāo)簽放在 body 標(biāo)簽底部的原因。

當(dāng)然在當(dāng)下,并不是說(shuō) script 標(biāo)簽必須放在底部,因?yàn)槟憧梢越o script 標(biāo)簽添加 defer 或者 async 屬性。當(dāng) script 標(biāo)簽加上 defer 屬性以后,表示該 JS 文件會(huì)并行下載,但是會(huì)放到 HTML 解析完成后順序執(zhí)行,所以對(duì)于這種情況你可以把 script 標(biāo)簽放在任意位置。對(duì)于沒(méi)有任何依賴(lài)的 JS 文件可以加上 async 屬性,表示 JS 文件下載和解析不會(huì)阻塞渲染。

六、瀏覽器本地存儲(chǔ)


1. 瀏覽器本地存儲(chǔ)方式及使用場(chǎng)景

(1)Cookie

Cookie是最早被提出來(lái)的本地存儲(chǔ)方式,在此之前,服務(wù)端是無(wú)法判斷網(wǎng)絡(luò)中的兩個(gè)請(qǐng)求是否是同一用戶(hù)發(fā)起的,為解決這個(gè)問(wèn)題,Cookie就出現(xiàn)了。Cookie的大小只有4kb,它是一種純文本文件,每次發(fā)起HTTP請(qǐng)求都會(huì)攜帶Cookie。

Cookie的特性:

  • Cookie一旦創(chuàng)建成功,名稱(chēng)就無(wú)法修改
  • Cookie是無(wú)法跨域名的,也就是說(shuō)a域名和b域名下的cookie是無(wú)法共享的,這也是由Cookie的隱私安全性決定的,這樣就能夠阻止非法獲取其他網(wǎng)站的Cookie
  • 每個(gè)域名下Cookie的數(shù)量不能超過(guò)20個(gè),每個(gè)Cookie的大小不能超過(guò)4kb
  • 有安全問(wèn)題,如果Cookie被攔截了,那就可獲得session的所有信息,即使加密也于事無(wú)補(bǔ),無(wú)需知道cookie的意義,只要轉(zhuǎn)發(fā)cookie就能達(dá)到目的
  • Cookie在請(qǐng)求一個(gè)新的頁(yè)面的時(shí)候都會(huì)被發(fā)送過(guò)去

如果需要域名之間跨域共享Cookie,有兩種方法:

  1. 使用Nginx反向代理
  2. 在一個(gè)站點(diǎn)登陸之后,往其他網(wǎng)站寫(xiě)Cookie。服務(wù)端的Session存儲(chǔ)到一個(gè)節(jié)點(diǎn),Cookie存儲(chǔ)sessionId

Cookie的使用場(chǎng)景:

  • 最常見(jiàn)的使用場(chǎng)景就是Cookie和session結(jié)合使用,我們將sessionId存儲(chǔ)到Cookie中,每次發(fā)請(qǐng)求都會(huì)攜帶這個(gè)sessionId,這樣服務(wù)端就知道是誰(shuí)發(fā)起的請(qǐng)求,從而響應(yīng)相應(yīng)的信息。
  • 可以用來(lái)統(tǒng)計(jì)頁(yè)面的點(diǎn)擊次數(shù)

(2)LocalStorage

LocalStorage是HTML5新引入的特性,由于有的時(shí)候我們存儲(chǔ)的信息較大,Cookie就不能滿(mǎn)足我們的需求,這時(shí)候LocalStorage就派上用場(chǎng)了。

LocalStorage的優(yōu)點(diǎn):

  • 在大小方面,LocalStorage的大小一般為5MB,可以?xún)?chǔ)存更多的信息
  • LocalStorage是持久儲(chǔ)存,并不會(huì)隨著頁(yè)面的關(guān)閉而消失,除非主動(dòng)清理,不然會(huì)永久存在
  • 僅儲(chǔ)存在本地,不像Cookie那樣每次HTTP請(qǐng)求都會(huì)被攜帶

LocalStorage的缺點(diǎn):

  • 存在瀏覽器兼容問(wèn)題,IE8以下版本的瀏覽器不支持
  • 如果瀏覽器設(shè)置為隱私模式,那我們將無(wú)法讀取到LocalStorage
  • LocalStorage受到同源策略的限制,即端口、協(xié)議、主機(jī)地址有任何一個(gè)不相同,都不會(huì)訪(fǎng)問(wèn)

LocalStorage的常用API:

// 保存數(shù)據(jù)到 localStorage
localStorage.setItem('key', 'value');

// 從 localStorage 獲取數(shù)據(jù)
let data = localStorage.getItem('key');

// 從 localStorage 刪除保存的數(shù)據(jù)
localStorage.removeItem('key');

// 從 localStorage 刪除所有保存的數(shù)據(jù)
localStorage.clear();

// 獲取某個(gè)索引的Key
localStorage.key(index)

LocalStorage的使用場(chǎng)景:

  • 有些網(wǎng)站有換膚的功能,這時(shí)候就可以將換膚的信息存儲(chǔ)在本地的LocalStorage中,當(dāng)需要換膚的時(shí)候,直接操作LocalStorage即可
  • 在網(wǎng)站中的用戶(hù)瀏覽信息也會(huì)存儲(chǔ)在LocalStorage中,還有網(wǎng)站的一些不常變動(dòng)的個(gè)人信息等也可以存儲(chǔ)在本地的LocalStorage中

(3)SessionStorage

SessionStorage和LocalStorage都是在HTML5才提出來(lái)的存儲(chǔ)方案,SessionStorage 主要用于臨時(shí)保存同一窗口(或標(biāo)簽頁(yè))的數(shù)據(jù),刷新頁(yè)面時(shí)不會(huì)刪除,關(guān)閉窗口或標(biāo)簽頁(yè)之后將會(huì)刪除這些數(shù)據(jù)。

SessionStorage與LocalStorage對(duì)比:

  • SessionStorage和LocalStorage都在本地進(jìn)行數(shù)據(jù)存儲(chǔ);
  • SessionStorage也有同源策略的限制,但是SessionStorage有一條更加嚴(yán)格的限制,SessionStorage只有在同一瀏覽器的同一窗口下才能夠共享
  • LocalStorage和SessionStorage都不能被爬蟲(chóng)爬取;

SessionStorage的常用API:

// 保存數(shù)據(jù)到 sessionStorage
sessionStorage.setItem('key', 'value');

// 從 sessionStorage 獲取數(shù)據(jù)
let data = sessionStorage.getItem('key');

// 從 sessionStorage 刪除保存的數(shù)據(jù)
sessionStorage.removeItem('key');

// 從 sessionStorage 刪除所有保存的數(shù)據(jù)
sessionStorage.clear();

// 獲取某個(gè)索引的Key
sessionStorage.key(index)

SessionStorage的使用場(chǎng)景

  • 由于SessionStorage具有時(shí)效性,所以可以用來(lái)存儲(chǔ)一些網(wǎng)站的游客登錄的信息,還有臨時(shí)的瀏覽記錄的信息。當(dāng)關(guān)閉網(wǎng)站之后,這些信息也就隨之消除了。

2. Cookie有哪些字段,作用分別是什么

Cookie由以下字段組成:

  • Name:cookie的名稱(chēng)
  • Value:cookie的值,對(duì)于認(rèn)證cookie,value值包括web服務(wù)器所提供的訪(fǎng)問(wèn)令牌;
  • Size: cookie的大小
  • Path:可以訪(fǎng)問(wèn)此cookie的頁(yè)面路徑。 比如domain是abc.com,path是 ?/test?,那么只有 ?/test?路徑下的頁(yè)面可以讀取此cookie。
  • Secure: 指定是否使用HTTPS安全協(xié)議發(fā)送Cookie。使用HTTPS安全協(xié)議,可以保護(hù)Cookie在瀏覽器和Web服務(wù)器間的傳輸過(guò)程中不被竊取和篡改。該方法也可用于Web站點(diǎn)的身份鑒別,即在HTTPS的連接建立階段,瀏覽器會(huì)檢查Web網(wǎng)站的SSL證書(shū)的有效性。但是基于兼容性的原因(比如有些網(wǎng)站使用自簽署的證書(shū))在檢測(cè)到SSL證書(shū)無(wú)效時(shí),瀏覽器并不會(huì)立即終止用戶(hù)的連接請(qǐng)求,而是顯示安全風(fēng)險(xiǎn)信息,用戶(hù)仍可以選擇繼續(xù)訪(fǎng)問(wèn)該站點(diǎn)。
  • Domain:可以訪(fǎng)問(wèn)該cookie的域名,Cookie 機(jī)制并未遵循嚴(yán)格的同源策略,允許一個(gè)子域可以設(shè)置或獲取其父域的 Cookie。當(dāng)需要實(shí)現(xiàn)單點(diǎn)登錄方案時(shí),Cookie 的上述特性非常有用,然而也增加了 Cookie受攻擊的危險(xiǎn),比如攻擊者可以借此發(fā)動(dòng)會(huì)話(huà)定置攻擊。因而,瀏覽器禁止在 Domain 屬性中設(shè)置.org、.com 等通用頂級(jí)域名、以及在國(guó)家及地區(qū)頂級(jí)域下注冊(cè)的二級(jí)域名,以減小攻擊發(fā)生的范圍。
  • HTTP: 該字段包含 ?HTTPOnly ?屬性 ,該屬性用來(lái)設(shè)置cookie能否通過(guò)腳本來(lái)訪(fǎng)問(wèn),默認(rèn)為空,即可以通過(guò)腳本訪(fǎng)問(wèn)。在客戶(hù)端是不能通過(guò)js代碼去設(shè)置一個(gè)httpOnly類(lèi)型的cookie的,這種類(lèi)型的cookie只能通過(guò)服務(wù)端來(lái)設(shè)置。該屬性用于防止客戶(hù)端腳本通過(guò) ?document.cookie?屬性訪(fǎng)問(wèn)Cookie,有助于保護(hù)Cookie不被跨站腳本攻擊竊取或篡改。但是,HTTPOnly的應(yīng)用仍存在局限性,一些瀏覽器可以阻止客戶(hù)端腳本對(duì)Cookie的讀操作,但允許寫(xiě)操作;此外大多數(shù)瀏覽器仍允許通過(guò)XMLHTTP對(duì)象讀取HTTP響應(yīng)中的Set-Cookie頭。
  • Expires/Max-size : 此cookie的超時(shí)時(shí)間。若設(shè)置其值為一個(gè)時(shí)間,那么當(dāng)?shù)竭_(dá)此時(shí)間后,此cookie失效。不設(shè)置的話(huà)默認(rèn)值是Session,意思是cookie會(huì)和session一起失效。當(dāng)瀏覽器關(guān)閉(不是瀏覽器標(biāo)簽頁(yè),而是整個(gè)瀏覽器) 后,此cookie失效。

總結(jié):

服務(wù)器端可以使用 Set-Cookie 的響應(yīng)頭部來(lái)配置 cookie 信息。一條cookie 包括了5個(gè)屬性值 expires、domain、path、secure、HttpOnly。其中 expires 指定了 cookie 失效的時(shí)間,domain 是域名、path是路徑,domain 和 path 一起限制了 cookie 能夠被哪些 url 訪(fǎng)問(wèn)。secure 規(guī)定了 cookie 只能在確保安全的情況下傳輸,HttpOnly 規(guī)定了這個(gè) cookie 只能被服務(wù)器訪(fǎng)問(wèn),不能使用 js 腳本訪(fǎng)問(wèn)。

3. Cookie、LocalStorage、SessionStorage區(qū)別

瀏覽器端常用的存儲(chǔ)技術(shù)是 cookie 、localStorage 和 sessionStorage。

  • cookie:其實(shí)最開(kāi)始是服務(wù)器端用于記錄用戶(hù)狀態(tài)的一種方式,由服務(wù)器設(shè)置,在客戶(hù)端存儲(chǔ),然后每次發(fā)起同源請(qǐng)求時(shí),發(fā)送給服務(wù)器端。cookie 最多能存儲(chǔ) 4 k 數(shù)據(jù),它的生存時(shí)間由 expires 屬性指定,并且 cookie 只能被同源的頁(yè)面訪(fǎng)問(wèn)共享。
  • sessionStorage:html5 提供的一種瀏覽器本地存儲(chǔ)的方法,它借鑒了服務(wù)器端 session 的概念,代表的是一次會(huì)話(huà)中所保存的數(shù)據(jù)。它一般能夠存儲(chǔ) 5M 或者更大的數(shù)據(jù),它在當(dāng)前窗口關(guān)閉后就失效了,并且 sessionStorage 只能被同一個(gè)窗口的同源頁(yè)面所訪(fǎng)問(wèn)共享。
  • localStorage:html5 提供的一種瀏覽器本地存儲(chǔ)的方法,它一般也能夠存儲(chǔ) 5M 或者更大的數(shù)據(jù)。它和 sessionStorage 不同的是,除非手動(dòng)刪除它,否則它不會(huì)失效,并且 localStorage 也只能被同源頁(yè)面所訪(fǎng)問(wèn)共享。

上面幾種方式都是存儲(chǔ)少量數(shù)據(jù)的時(shí)候的存儲(chǔ)方式,當(dāng)需要在本地存儲(chǔ)大量數(shù)據(jù)的時(shí)候,我們可以使用瀏覽器的 indexDB 這是瀏覽器提供的一種本地的數(shù)據(jù)庫(kù)存儲(chǔ)機(jī)制。它不是關(guān)系型數(shù)據(jù)庫(kù),它內(nèi)部采用對(duì)象倉(cāng)庫(kù)的形式存儲(chǔ)數(shù)據(jù),它更接近 NoSQL 數(shù)據(jù)庫(kù)。

4. 前端儲(chǔ)存的?式有哪些?

  • cookies: 在HTML5標(biāo)準(zhǔn)前本地儲(chǔ)存的主要?式,優(yōu)點(diǎn)是兼容性好,請(qǐng)求頭?帶cookie?便,缺點(diǎn)是??只有4k,?動(dòng)請(qǐng)求頭加?cookie浪費(fèi)流量,每個(gè)domain限制20個(gè)cookie,使?起來(lái)麻煩,需要??封裝;
  • localStorage:HTML5加?的以鍵值對(duì)(Key-Value)為標(biāo)準(zhǔn)的?式,優(yōu)點(diǎn)是操作?便,永久性?xún)?chǔ)存(除??動(dòng)刪除),??為5M,兼容IE8+ ;
  • sessionStorage:與localStorage基本類(lèi)似,區(qū)別是sessionStorage當(dāng)??關(guān)閉后會(huì)被清理,?且與cookie、localStorage不同,他不能在所有同源窗?中共享,是會(huì)話(huà)級(jí)別的儲(chǔ)存?式;
  • Web SQL:2010年被W3C廢棄的本地?cái)?shù)據(jù)庫(kù)數(shù)據(jù)存儲(chǔ)?案,但是主流瀏覽器(?狐除外)都已經(jīng)有了相關(guān)的實(shí)現(xiàn),web sql類(lèi)似于SQLite,是真正意義上的關(guān)系型數(shù)據(jù)庫(kù),?sql進(jìn)?操作,當(dāng)我們?JavaScript時(shí)要進(jìn)?轉(zhuǎn)換,較為繁瑣;
  • IndexedDB: 是被正式納?HTML5標(biāo)準(zhǔn)的數(shù)據(jù)庫(kù)儲(chǔ)存?案,它是NoSQL數(shù)據(jù)庫(kù),?鍵值對(duì)進(jìn)?儲(chǔ)存,可以進(jìn)?快速讀取操作,?常適合web場(chǎng)景,同時(shí)?JavaScript進(jìn)?操作會(huì)?常便。

5. IndexedDB有哪些特點(diǎn)?

IndexedDB 具有以下特點(diǎn):

  • 鍵值對(duì)儲(chǔ)存:IndexedDB 內(nèi)部采用對(duì)象倉(cāng)庫(kù)(object store)存放數(shù)據(jù)。所有類(lèi)型的數(shù)據(jù)都可以直接存入,包括 JavaScript 對(duì)象。對(duì)象倉(cāng)庫(kù)中,數(shù)據(jù)以"鍵值對(duì)"的形式保存,每一個(gè)數(shù)據(jù)記錄都有對(duì)應(yīng)的主鍵,主鍵是獨(dú)一無(wú)二的,不能有重復(fù),否則會(huì)拋出一個(gè)錯(cuò)誤。
  • 異步:IndexedDB 操作時(shí)不會(huì)鎖死瀏覽器,用戶(hù)依然可以進(jìn)行其他操作,這與 LocalStorage 形成對(duì)比,后者的操作是同步的。異步設(shè)計(jì)是為了防止大量數(shù)據(jù)的讀寫(xiě),拖慢網(wǎng)頁(yè)的表現(xiàn)。
  • 支持事務(wù):IndexedDB 支持事務(wù)(transaction),這意味著一系列操作步驟之中,只要有一步失敗,整個(gè)事務(wù)就都取消,數(shù)據(jù)庫(kù)回滾到事務(wù)發(fā)生之前的狀態(tài),不存在只改寫(xiě)一部分?jǐn)?shù)據(jù)的情況。
  • 同源限制:IndexedDB 受到同源限制,每一個(gè)數(shù)據(jù)庫(kù)對(duì)應(yīng)創(chuàng)建它的域名。網(wǎng)頁(yè)只能訪(fǎng)問(wèn)自身域名下的數(shù)據(jù)庫(kù),而不能訪(fǎng)問(wèn)跨域的數(shù)據(jù)庫(kù)。
  • 儲(chǔ)存空間大:IndexedDB 的儲(chǔ)存空間比 LocalStorage 大得多,一般來(lái)說(shuō)不少于 250MB,甚至沒(méi)有上限。
  • 支持二進(jìn)制儲(chǔ)存:IndexedDB 不僅可以?xún)?chǔ)存字符串,還可以?xún)?chǔ)存二進(jìn)制數(shù)據(jù)(ArrayBuffer 對(duì)象和 Blob 對(duì)象)。

七、瀏覽器同源策略


1. 什么是同源策略

跨域問(wèn)題其實(shí)就是瀏覽器的同源策略造成的。

同源策略限制了從同一個(gè)源加載的文檔或腳本如何與另一個(gè)源的資源進(jìn)行交互。這是瀏覽器的一個(gè)用于隔離潛在惡意文件的重要的安全機(jī)制。同源指的是:協(xié)議、端口號(hào)、域名必須一致。

下表給出了與 URL http://store.company.com/dir/page.html 的源進(jìn)行對(duì)比的示例:

URL 是否跨域 原因
http://store.company.com/dir/page.html 同源 完全相同
http://store.company.com/dir/inner/another.html 同源 只有路徑不同
https://store.company.com/secure.html 跨域 協(xié)議不同
http://store.company.com:81/dir/etc.html 跨域 端口不同 ( http:// 默認(rèn)端口是80)
http://news.company.com/dir/other.html 跨域 主機(jī)不同

同源策略:protocol(協(xié)議)、domain(域名)、port(端口)三者必須一致。

同源政策主要限制了三個(gè)方面:

  • 當(dāng)前域下的 js 腳本不能夠訪(fǎng)問(wèn)其他域下的 cookie、localStorage 和 indexDB。
  • 當(dāng)前域下的 js 腳本不能夠操作訪(fǎng)問(wèn)操作其他域下的 DOM。
  • 當(dāng)前域下 ajax 無(wú)法發(fā)送跨域請(qǐng)求。

同源政策的目的主要是為了保證用戶(hù)的信息安全,它只是對(duì) js 腳本的一種限制,并不是對(duì)瀏覽器的限制,對(duì)于一般的 img、或者script 腳本請(qǐng)求都不會(huì)有跨域的限制,這是因?yàn)檫@些操作都不會(huì)通過(guò)響應(yīng)結(jié)果來(lái)進(jìn)行可能出現(xiàn)安全問(wèn)題的操作。

2. 如何解決跨越問(wèn)題

(1)CORS

下面是MDN對(duì)于CORS的定義:

跨域資源共享(CORS) 是一種機(jī)制,它使用額外的 HTTP 頭來(lái)告訴瀏覽器 讓運(yùn)行在一個(gè) origin (domain)上的Web應(yīng)用被準(zhǔn)許訪(fǎng)問(wèn)來(lái)自不同源服務(wù)器上的指定的資源。當(dāng)一個(gè)資源從與該資源本身所在的服務(wù)器不同的域、協(xié)議或端口請(qǐng)求一個(gè)資源時(shí),資源會(huì)發(fā)起一個(gè)跨域HTTP 請(qǐng)求。

CORS需要瀏覽器和服務(wù)器同時(shí)支持,整個(gè)CORS過(guò)程都是瀏覽器完成的,無(wú)需用戶(hù)參與。因此實(shí)現(xiàn)CORS的關(guān)鍵就是服務(wù)器,只要服務(wù)器實(shí)現(xiàn)了CORS請(qǐng)求,就可以跨源通信了。

瀏覽器將CORS分為簡(jiǎn)單請(qǐng)求非簡(jiǎn)單請(qǐng)求

簡(jiǎn)單請(qǐng)求不會(huì)觸發(fā)CORS預(yù)檢請(qǐng)求。若該請(qǐng)求滿(mǎn)足以下兩個(gè)條件,就可以看作是簡(jiǎn)單請(qǐng)求:

1)請(qǐng)求方法是以下三種方法之一:

  • HEAD
  • GET
  • POST

2)HTTP的頭信息不超出以下幾種字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三個(gè)值application/x-www-form-urlencoded、multipart/form-data、text/plain

若不滿(mǎn)足以上條件,就屬于非簡(jiǎn)單請(qǐng)求了。

(1)簡(jiǎn)單請(qǐng)求過(guò)程:

對(duì)于簡(jiǎn)單請(qǐng)求,瀏覽器會(huì)直接發(fā)出CORS請(qǐng)求,它會(huì)在請(qǐng)求的頭信息中增加一個(gè)Orign字段,該字段用來(lái)說(shuō)明本次請(qǐng)求來(lái)自哪個(gè)源(協(xié)議+端口+域名),服務(wù)器會(huì)根據(jù)這個(gè)值來(lái)決定是否同意這次請(qǐng)求。如果Orign指定的域名在許可范圍之內(nèi),服務(wù)器返回的響應(yīng)就會(huì)多出以下信息頭:

Access-Control-Allow-Origin: http://api.bob.com  // 和Orign一直
Access-Control-Allow-Credentials: true   // 表示是否允許發(fā)送Cookie
Access-Control-Expose-Headers: FooBar   // 指定返回其他字段的值
Content-Type: text/html; charset=utf-8   // 表示文檔類(lèi)型

如果Orign指定的域名不在許可范圍之內(nèi),服務(wù)器會(huì)返回一個(gè)正常的HTTP回應(yīng),瀏覽器發(fā)現(xiàn)沒(méi)有上面的Access-Control-Allow-Origin頭部信息,就知道出錯(cuò)了。這個(gè)錯(cuò)誤無(wú)法通過(guò)狀態(tài)碼識(shí)別,因?yàn)榉祷氐臓顟B(tài)碼可能是200。

在簡(jiǎn)單請(qǐng)求中,在服務(wù)器內(nèi),至少需要設(shè)置字段:Access-Control-Allow-Origin

(2)非簡(jiǎn)單請(qǐng)求過(guò)程

非簡(jiǎn)單請(qǐng)求是對(duì)服務(wù)器有特殊要求的請(qǐng)求,比如請(qǐng)求方法為DELETE或者PUT等。非簡(jiǎn)單請(qǐng)求的CORS請(qǐng)求會(huì)在正式通信之前進(jìn)行一次HTTP查詢(xún)請(qǐng)求,稱(chēng)為預(yù)檢請(qǐng)求。

瀏覽器會(huì)詢(xún)問(wèn)服務(wù)器,當(dāng)前所在的網(wǎng)頁(yè)是否在服務(wù)器允許訪(fǎng)問(wèn)的范圍內(nèi),以及可以使用哪些HTTP請(qǐng)求方式和頭信息字段,只有得到肯定的回復(fù),才會(huì)進(jìn)行正式的HTTP請(qǐng)求,否則就會(huì)報(bào)錯(cuò)。

預(yù)檢請(qǐng)求使用的請(qǐng)求方法是OPTIONS,表示這個(gè)請(qǐng)求是來(lái)詢(xún)問(wèn)的。他的頭信息中的關(guān)鍵字段是Orign,表示請(qǐng)求來(lái)自哪個(gè)源。除此之外,頭信息中還包括兩個(gè)字段:

  • Access-Control-Request-Method:該字段是必須的,用來(lái)列出瀏覽器的CORS請(qǐng)求會(huì)用到哪些HTTP方法。
  • Access-Control-Request-Headers: 該字段是一個(gè)逗號(hào)分隔的字符串,指定瀏覽器CORS請(qǐng)求會(huì)額外發(fā)送的頭信息字段。

服務(wù)器在收到瀏覽器的預(yù)檢請(qǐng)求之后,會(huì)根據(jù)頭信息的三個(gè)字段來(lái)進(jìn)行判斷,如果返回的頭信息在中有Access-Control-Allow-Origin這個(gè)字段就是允許跨域請(qǐng)求,如果沒(méi)有,就是不同意這個(gè)預(yù)檢請(qǐng)求,就會(huì)報(bào)錯(cuò)。

服務(wù)器回應(yīng)的CORS的字段如下:

Access-Control-Allow-Origin: http://api.bob.com  // 允許跨域的源地址
Access-Control-Allow-Methods: GET, POST, PUT // 服務(wù)器支持的所有跨域請(qǐng)求的方法
Access-Control-Allow-Headers: X-Custom-Header  // 服務(wù)器支持的所有頭信息字段
Access-Control-Allow-Credentials: true   // 表示是否允許發(fā)送Cookie
Access-Control-Max-Age: 1728000  // 用來(lái)指定本次預(yù)檢請(qǐng)求的有效期,單位為秒

只要服務(wù)器通過(guò)了預(yù)檢請(qǐng)求,在以后每次的CORS請(qǐng)求都會(huì)自帶一個(gè)Origin頭信息字段。服務(wù)器的回應(yīng),也都會(huì)有一個(gè)Access-Control-Allow-Origin頭信息字段。

在非簡(jiǎn)單請(qǐng)求中,至少需要設(shè)置以下字段:

'Access-Control-Allow-Origin'  
'Access-Control-Allow-Methods'
'Access-Control-Allow-Headers'
減少OPTIONS請(qǐng)求次數(shù):

OPTIONS請(qǐng)求次數(shù)過(guò)多就會(huì)損耗頁(yè)面加載的性能,降低用戶(hù)體驗(yàn)度。所以盡量要減少OPTIONS請(qǐng)求次數(shù),可以后端在請(qǐng)求的返回頭部添加:Access-Control-Max-Age:number。它表示預(yù)檢請(qǐng)求的返回結(jié)果可以被緩存多久,單位是秒。該字段只對(duì)完全一樣的URL的緩存設(shè)置生效,所以設(shè)置了緩存時(shí)間,在這個(gè)時(shí)間范圍內(nèi),再次發(fā)送請(qǐng)求就不需要進(jìn)行預(yù)檢請(qǐng)求了。

CORS中Cookie相關(guān)問(wèn)題:

在CORS請(qǐng)求中,如果想要傳遞Cookie,就要滿(mǎn)足以下三個(gè)條件:

  • 在請(qǐng)求中設(shè)置?withCredentials?

默認(rèn)情況下在跨域請(qǐng)求,瀏覽器是不帶 cookie 的。但是我們可以通過(guò)設(shè)置 withCredentials 來(lái)進(jìn)行傳遞 cookie.

// 原生 xml 的設(shè)置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// axios 設(shè)置方式
axios.defaults.withCredentials = true;
  • Access-Control-Allow-Credentials 設(shè)置為 ?true?
  • Access-Control-Allow-Origin 設(shè)置為 ?false?

(2)JSONP

jsonp的原理就是利用 <script>標(biāo)簽沒(méi)有跨域限制,通過(guò) <script>標(biāo)簽src屬性,發(fā)送帶有callback參數(shù)的GET請(qǐng)求,服務(wù)端將接口返回?cái)?shù)據(jù)拼湊到callback函數(shù)中,返回給瀏覽器,瀏覽器解析執(zhí)行,從而前端拿到callback函數(shù)返回的數(shù)據(jù)。

1)原生JS實(shí)現(xiàn):

<script>
    var script = document.createElement('script');
    script.type = 'text/javascript';
    // 傳參一個(gè)回調(diào)函數(shù)名給后端,方便后端返回時(shí)執(zhí)行這個(gè)在前端定義的回調(diào)函數(shù)
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
    document.head.appendChild(script);
    // 回調(diào)執(zhí)行函數(shù)
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
 </script>

服務(wù)端返回如下(返回時(shí)即執(zhí)行全局函數(shù)):

handleCallback({"success": true, "user": "admin"})

2)Vue axios實(shí)現(xiàn):

this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})

后端node.js代碼:

var querystring = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
    var params = querystring.parse(req.url.split('?')[1]);
    var fn = params.callback;
    // jsonp返回設(shè)置
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.write(fn + '(' + JSON.stringify(params) + ')');
    res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');

JSONP的缺點(diǎn):

  • 具有局限性, 僅支持get方法
  • 不安全,可能會(huì)遭受XSS攻擊

(3)postMessage 跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是為數(shù)不多可以跨域操作的window屬性之一,它可用于解決以下方面的問(wèn)題:

  • 頁(yè)面和其打開(kāi)的新窗口的數(shù)據(jù)傳遞
  • 多窗口之間消息傳遞
  • 頁(yè)面與嵌套的iframe消息傳遞
  • 上面三個(gè)場(chǎng)景的跨域數(shù)據(jù)傳遞

用法:postMessage(data,origin)方法接受兩個(gè)參數(shù):

  • data: html5規(guī)范支持任意基本類(lèi)型或可復(fù)制的對(duì)象,但部分瀏覽器只支持字符串,所以傳參時(shí)最好用JSON.stringify()序列化。
  • origin: 協(xié)議+主機(jī)+端口號(hào),也可以設(shè)置為"*",表示可以傳遞給任意窗口,如果要指定和當(dāng)前窗口同源的話(huà)設(shè)置為"/"。

1)a.html:(domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" rel="external nofollow"  rel="external nofollow"  style="display:none;"></iframe>
<script>     
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2傳送跨域數(shù)據(jù)
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };
    // 接受domain2返回?cái)?shù)據(jù)
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

2)b.html:(domain2.com/b.html)

<script>
    // 接收domain1的數(shù)據(jù)
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);
        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;
            // 處理后再發(fā)回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

(4)nginx代理跨域

nginx代理跨域,實(shí)質(zhì)和CORS跨域原理一樣,通過(guò)配置文件設(shè)置請(qǐng)求響應(yīng)頭Access-Control-Allow-Origin…等字段。

1)nginx配置解決iconfont跨域

瀏覽器跨域訪(fǎng)問(wèn)js、css、img等常規(guī)靜態(tài)資源被同源策略許可,但iconfont字體文件(eot|otf|ttf|woff|svg)例外,此時(shí)可在nginx的靜態(tài)資源服務(wù)器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}

2)nginx反向代理接口跨域

跨域問(wèn)題:同源策略?xún)H是針對(duì)瀏覽器的安全策略。服務(wù)器端調(diào)用HTTP接口只是使用HTTP協(xié)議,不需要同源策略,也就不存在跨域問(wèn)題。

實(shí)現(xiàn)思路:通過(guò)Nginx配置一個(gè)代理服務(wù)器域名與domain1相同,端口不同)做跳板機(jī),反向代理訪(fǎng)問(wèn)domain2接口,并且可以順便修改cookie中domain信息,方便當(dāng)前域cookie寫(xiě)入,實(shí)現(xiàn)跨域訪(fǎng)問(wèn)。

nginx具體配置:

#proxy服務(wù)器
server {
    listen       81;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;
        # 當(dāng)用webpack-dev-server等中間件代理接口訪(fǎng)問(wèn)nignx時(shí),此時(shí)無(wú)瀏覽器參與,故沒(méi)有同源限制,下面的跨域配置可不啟用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #當(dāng)前端只跨域不帶cookie時(shí),可為*
        add_header Access-Control-Allow-Credentials true;
    }
}

(5)nodejs 中間件代理跨域

node中間件實(shí)現(xiàn)跨域代理,原理大致與nginx相同,都是通過(guò)啟一個(gè)代理服務(wù)器,實(shí)現(xiàn)數(shù)據(jù)的轉(zhuǎn)發(fā),也可以通過(guò)設(shè)置cookieDomainRewrite參數(shù)修改響應(yīng)頭中cookie中域名,實(shí)現(xiàn)當(dāng)前域的cookie寫(xiě)入,方便接口登錄認(rèn)證。

1)非vue框架的跨域

使用node + express + http-proxy-middleware搭建一個(gè)proxy服務(wù)器。

  • 前端代碼:
var xhr = new XMLHttpRequest();
// 前端開(kāi)關(guān):瀏覽器是否讀寫(xiě)cookie
xhr.withCredentials = true;
// 訪(fǎng)問(wèn)http-proxy-middleware代理服務(wù)器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
  • 中間件服務(wù)器代碼:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
    // 代理跨域目標(biāo)接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,
    // 修改響應(yīng)頭信息,實(shí)現(xiàn)跨域并允許帶cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },
    // 修改響應(yīng)信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以為false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');

2)vue框架的跨域

node + vue + webpack + webpack-dev-server搭建的項(xiàng)目,跨域請(qǐng)求接口,直接修改webpack.config.js配置。開(kāi)發(fā)環(huán)境下,vue渲染服務(wù)和接口代理服務(wù)都是webpack-dev-server同一個(gè),所以頁(yè)面與代理接口之間不再跨域。

webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目標(biāo)接口
            changeOrigin: true,
            secure: false,  // 當(dāng)代理某些https服務(wù)報(bào)錯(cuò)時(shí)用
            cookieDomainRewrite: 'www.domain1.com'  // 可以為false,表示不修改
        }],
        noInfo: true
    }
}

(6)document.domain + iframe跨域

此方案僅限主域相同,子域不同的跨域應(yīng)用場(chǎng)景。實(shí)現(xiàn)原理:兩個(gè)頁(yè)面都通過(guò)js強(qiáng)制設(shè)置document.domain為基礎(chǔ)主域,就實(shí)現(xiàn)了同域。

1)父窗口:(domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html" rel="external nofollow" ></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

2)子窗口:(child.domain.com/a.html)

<script>
    document.domain = 'domain.com';
    // 獲取父窗口中變量
    console.log('get js data from parent ---> ' + window.parent.user);
</script>

(7)location.hash + iframe跨域

實(shí)現(xiàn)原理:a欲與b跨域相互通信,通過(guò)中間頁(yè)c來(lái)實(shí)現(xiàn)。 三個(gè)頁(yè)面,不同域之間利用iframe的location.hash傳值,相同域之間直接js訪(fǎng)問(wèn)來(lái)通信。

具體實(shí)現(xiàn):A域:a.html -> B域:b.html -> A域:c.html,a與b不同域只能通過(guò)hash值單向通信,b與c也不同域也只能單向通信,但c與a同域,所以c可通過(guò)parent.parent訪(fǎng)問(wèn)a頁(yè)面所有對(duì)象。

1)a.html:(domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" rel="external nofollow"  rel="external nofollow"  style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');
    // 向b.html傳hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
  
    // 開(kāi)放給同域c.html的回調(diào)方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

2)b.html:(.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" rel="external nofollow"  style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');
    // 監(jiān)聽(tīng)a.html傳來(lái)的hash值,再傳給c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

3)c.html:(http://www.domain1.com/c.html)

<script>
    // 監(jiān)聽(tīng)b.html傳來(lái)的hash值
    window.onhashchange = function () {
        // 再通過(guò)操作同域a.html的js回調(diào),將結(jié)果傳回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

(8)window.name + iframe跨域

window.name屬性的獨(dú)特之處:name值在不同的頁(yè)面(甚至不同域名)加載后依舊存在,并且可以支持非常長(zhǎng)的 name 值(2MB)。

1)a.html:(domain1.com/a.html)

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');
    // 加載跨域頁(yè)面
    iframe.src = url;
    // onload事件會(huì)觸發(fā)2次,第1次加載跨域頁(yè),并留存數(shù)據(jù)于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy頁(yè))成功后,讀取同域window.name中數(shù)據(jù)
            callback(iframe.contentWindow.name);
            destoryFrame();
        } else if (state === 0) {
            // 第1次onload(跨域頁(yè))成功后,切換到同域代理頁(yè)面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };
    document.body.appendChild(iframe);
    // 獲取數(shù)據(jù)以后銷(xiāo)毀這個(gè)iframe,釋放內(nèi)存;這也保證了安全(不被其他域frame js訪(fǎng)問(wèn))
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};
// 請(qǐng)求跨域b頁(yè)面數(shù)據(jù)
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});

2)proxy.html:(domain1.com/proxy.html)

中間代理頁(yè),與a.html同域,內(nèi)容為空即可。

3)b.html:(domain2.com/b.html)

<script>
    window.name = 'This is domain2 data!';
</script>

通過(guò)iframe的src屬性由外域轉(zhuǎn)向本地域,跨域數(shù)據(jù)即由iframe的window.name從外域傳遞到本地域。這個(gè)就巧妙地繞過(guò)了瀏覽器的跨域訪(fǎng)問(wèn)限制,但同時(shí)它又是安全操作。

(9)WebSocket協(xié)議跨域

WebSocket protocol是HTML5一種新的協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信,同時(shí)允許跨域通訊,是server push技術(shù)的一種很好的實(shí)現(xiàn)。

原生WebSocket API使用起來(lái)不太方便,我們使用Socket.io,它很好地封裝了webSocket接口,提供了更簡(jiǎn)單、靈活的接口,也對(duì)不支持webSocket的瀏覽器提供了向下兼容。

1)前端代碼:

<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js" rel="external nofollow" ></script>
<script>
var socket = io('http://www.domain2.com:8080');
// 連接成功處理
socket.on('connect', function() {
    // 監(jiān)聽(tīng)服務(wù)端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });
    // 監(jiān)聽(tīng)服務(wù)端關(guān)閉
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});
document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

2)Nodejs socket后臺(tái):

var http = require('http');
var socket = require('socket.io');
// 啟http服務(wù)
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 監(jiān)聽(tīng)socket連接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });
    // 斷開(kāi)處理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

3. 正向代理和反向代理的區(qū)別

  • 正向代理:

客戶(hù)端想獲得一個(gè)服務(wù)器的數(shù)據(jù),但是因?yàn)榉N種原因無(wú)法直接獲取。于是客戶(hù)端設(shè)置了一個(gè)代理服務(wù)器,并且指定目標(biāo)服務(wù)器,之后代理服務(wù)器向目標(biāo)服務(wù)器轉(zhuǎn)交請(qǐng)求并將獲得的內(nèi)容發(fā)送給客戶(hù)端。這樣本質(zhì)上起到了對(duì)真實(shí)服務(wù)器隱藏真實(shí)客戶(hù)端的目的。實(shí)現(xiàn)正向代理需要修改客戶(hù)端,比如修改瀏覽器配置。

  • 反向代理:

服務(wù)器為了能夠?qū)⒐ぷ髫?fù)載分不到多個(gè)服務(wù)器來(lái)提高網(wǎng)站性能 (負(fù)載均衡)等目的,當(dāng)其受到請(qǐng)求后,會(huì)首先根據(jù)轉(zhuǎn)發(fā)規(guī)則來(lái)確定請(qǐng)求應(yīng)該被轉(zhuǎn)發(fā)到哪個(gè)服務(wù)器上,然后將請(qǐng)求轉(zhuǎn)發(fā)到對(duì)應(yīng)的真實(shí)服務(wù)器上。這樣本質(zhì)上起到了對(duì)客戶(hù)端隱藏真實(shí)服務(wù)器的作用。

一般使用反向代理后,需要通過(guò)修改 DNS 讓域名解析到代理服務(wù)器 IP,這時(shí)瀏覽器無(wú)法察覺(jué)到真正服務(wù)器的存在,當(dāng)然也就不需要修改配置了。

兩者區(qū)別如圖示:


正向代理和反向代理的結(jié)構(gòu)是一樣的,都是 client-proxy-server 的結(jié)構(gòu),它們主要的區(qū)別就在于中間這個(gè) proxy 是哪一方設(shè)置的。在正向代理中,proxy 是 client 設(shè)置的,用來(lái)隱藏 client;而在反向代理中,proxy 是 server 設(shè)置的,用來(lái)隱藏 server。

4. Nginx的概念及其工作原理

Nginx 是一款輕量級(jí)的 Web 服務(wù)器,也可以用于反向代理、負(fù)載平衡和 HTTP 緩存等。Nginx 使用異步事件驅(qū)動(dòng)的方法來(lái)處理請(qǐng)求,是一款面向性能設(shè)計(jì)的 HTTP 服務(wù)器。

傳統(tǒng)的 Web 服務(wù)器如 Apache 是 process-based 模型的,而 Nginx 是基于event-driven模型的。正是這個(gè)主要的區(qū)別帶給了 Nginx 在性能上的優(yōu)勢(shì)。

Nginx 架構(gòu)的最頂層是一個(gè) master process,這個(gè) master process 用于產(chǎn)生其他的 worker process,這一點(diǎn)和Apache 非常像,但是 Nginx 的 worker process 可以同時(shí)處理大量的HTTP請(qǐng)求,而每個(gè) Apache process 只能處理一個(gè)。

八、瀏覽器事件機(jī)制


1. 事件是什么?事件模型?

事件是用戶(hù)操作網(wǎng)頁(yè)時(shí)發(fā)生的交互動(dòng)作,比如 click/move, 事件除了用戶(hù)觸發(fā)的動(dòng)作外,還可以是文檔加載,窗口滾動(dòng)和大小調(diào)整。事件被封裝成一個(gè) event 對(duì)象,包含了該事件發(fā)生時(shí)的所有相關(guān)信息( event 的屬性)以及可以對(duì)事件進(jìn)行的操作( event 的方法)。

事件是用戶(hù)操作網(wǎng)頁(yè)時(shí)發(fā)生的交互動(dòng)作或者網(wǎng)頁(yè)本身的一些操作,現(xiàn)代瀏覽器一共有三種事件模型:

  • DOM0 級(jí)事件模型,這種模型不會(huì)傳播,所以沒(méi)有事件流的概念,但是現(xiàn)在有的瀏覽器支持以冒泡的方式實(shí)現(xiàn),它可以在網(wǎng)頁(yè)中直接定義監(jiān)聽(tīng)函數(shù),也可以通過(guò) js 屬性來(lái)指定監(jiān)聽(tīng)函數(shù)。所有瀏覽器都兼容這種方式。直接在dom對(duì)象上注冊(cè)事件名稱(chēng),就是DOM0寫(xiě)法。
  • IE 事件模型,在該事件模型中,一次事件共有兩個(gè)過(guò)程,事件處理階段和事件冒泡階段。事件處理階段會(huì)首先執(zhí)行目標(biāo)元素綁定的監(jiān)聽(tīng)事件。然后是事件冒泡階段,冒泡指的是事件從目標(biāo)元素冒泡到 document,依次檢查經(jīng)過(guò)的節(jié)點(diǎn)是否綁定了事件監(jiān)聽(tīng)函數(shù),如果有則執(zhí)行。這種模型通過(guò)attachEvent 來(lái)添加監(jiān)聽(tīng)函數(shù),可以添加多個(gè)監(jiān)聽(tīng)函數(shù),會(huì)按順序依次執(zhí)行。
  • DOM2 級(jí)事件模型,在該事件模型中,一次事件共有三個(gè)過(guò)程,第一個(gè)過(guò)程是事件捕獲階段。捕獲指的是事件從 document 一直向下傳播到目標(biāo)元素,依次檢查經(jīng)過(guò)的節(jié)點(diǎn)是否綁定了事件監(jiān)聽(tīng)函數(shù),如果有則執(zhí)行。后面兩個(gè)階段和 IE 事件模型的兩個(gè)階段相同。這種事件模型,事件綁定的函數(shù)是addEventListener,其中第三個(gè)參數(shù)可以指定事件是否在捕獲階段執(zhí)行。

2. 如何阻止事件冒泡

  • 普通瀏覽器使用:event.stopPropagation()
  • IE瀏覽器使用:event.cancelBubble = true;

3. 對(duì)事件委托的理解

(1)事件委托的概念

事件委托本質(zhì)上是利用了瀏覽器事件冒泡的機(jī)制。因?yàn)槭录诿芭葸^(guò)程中會(huì)上傳到父節(jié)點(diǎn),父節(jié)點(diǎn)可以通過(guò)事件對(duì)象獲取到目標(biāo)節(jié)點(diǎn),因此可以把子節(jié)點(diǎn)的監(jiān)聽(tīng)函數(shù)定義在父節(jié)點(diǎn)上,由父節(jié)點(diǎn)的監(jiān)聽(tīng)函數(shù)統(tǒng)一處理多個(gè)子元素的事件,這種方式稱(chēng)為事件委托(事件代理)。

使用事件委托可以不必要為每一個(gè)子元素都綁定一個(gè)監(jiān)聽(tīng)事件,這樣減少了內(nèi)存上的消耗。并且使用事件代理還可以實(shí)現(xiàn)事件的動(dòng)態(tài)綁定,比如說(shuō)新增了一個(gè)子節(jié)點(diǎn),并不需要單獨(dú)地為它添加一個(gè)監(jiān)聽(tīng)事件,它綁定的事件會(huì)交給父元素中的監(jiān)聽(tīng)函數(shù)來(lái)處理。

(2)事件委托的特點(diǎn)

  • 減少內(nèi)存消耗

如果有一個(gè)列表,列表之中有大量的列表項(xiàng),需要在點(diǎn)擊列表項(xiàng)的時(shí)候響應(yīng)一個(gè)事件:

<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  ......
  <li>item n</li>
</ul>

如果給每個(gè)列表項(xiàng)一一都綁定一個(gè)函數(shù),那對(duì)于內(nèi)存消耗是非常大的,效率上需要消耗很多性能。因此,比較好的方法就是把這個(gè)點(diǎn)擊事件綁定到他的父層,也就是 ul 上,然后在執(zhí)行事件時(shí)再去匹配判斷目標(biāo)元素,所以事件委托可以減少大量的內(nèi)存消耗,節(jié)約效率。

  • 動(dòng)態(tài)綁定事件

給上述的例子中每個(gè)列表項(xiàng)都綁定事件,在很多時(shí)候,需要通過(guò) AJAX 或者用戶(hù)操作動(dòng)態(tài)的增加或者去除列表項(xiàng)元素,那么在每一次改變的時(shí)候都需要重新給新增的元素綁定事件,給即將刪去的元素解綁事件;如果用了事件委托就沒(méi)有這種麻煩了,因?yàn)槭录墙壎ㄔ诟笇拥?,和目?biāo)元素的增減是沒(méi)有關(guān)系的,執(zhí)行到目標(biāo)元素是在真正響應(yīng)執(zhí)行事件函數(shù)的過(guò)程中去匹配的,所以使用事件在動(dòng)態(tài)綁定事件的情況下是可以減少很多重復(fù)工作的。

// 來(lái)實(shí)現(xiàn)把 #list 下的 li 元素的事件代理委托到它的父層元素也就是 #list 上:
// 給父層元素綁定事件
document.getElementById('list').addEventListener('click', function (e) {
  // 兼容性處理
  var event = e || window.event;
  var target = event.target || event.srcElement;
  // 判斷是否匹配目標(biāo)元素
  if (target.nodeName.toLocaleLowerCase === 'li') {
    console.log('the content is: ', target.innerHTML);
  }
});

在上述代碼中, target 元素則是在 #list 元素之下具體被點(diǎn)擊的元素,然后通過(guò)判斷 target 的一些屬性(比如:nodeName,id 等等)可以更精確地匹配到某一類(lèi) #list li 元素之上;

(3)局限性

當(dāng)然,事件委托也是有局限的。比如 focus、blur 之類(lèi)的事件沒(méi)有事件冒泡機(jī)制,所以無(wú)法實(shí)現(xiàn)事件委托;mousemove、mouseout 這樣的事件,雖然有事件冒泡,但是只能不斷通過(guò)位置去計(jì)算定位,對(duì)性能消耗高,因此也是不適合于事件委托的。

當(dāng)然事件委托不是只有優(yōu)點(diǎn),它也是有缺點(diǎn)的,事件委托會(huì)影響頁(yè)面性能,主要影響因素有:

  • 元素中,綁定事件委托的次數(shù);
  • 點(diǎn)擊的最底層元素,到綁定事件元素之間的 ?DOM?層數(shù);

在必須使用事件委托的地方,可以進(jìn)行如下的處理:

  • 只在必須的地方,使用事件委托,比如:?ajax?的局部刷新區(qū)域
  • 盡量的減少綁定的層級(jí),不在 ?body?元素上,進(jìn)行綁定
  • 減少綁定的次數(shù),如果可以,那么把多個(gè)事件的綁定,合并到一次事件委托中去,由這個(gè)事件委托的回調(diào),來(lái)進(jìn)行分發(fā)。

4. 事件委托的使用場(chǎng)景

場(chǎng)景:給頁(yè)面的所有的a標(biāo)簽添加click事件,代碼如下:

document.addEventListener("click", function(e) {
    if (e.target.nodeName == "A")
        console.log("a");
}, false);

但是這些a標(biāo)簽可能包含一些像span、img等元素,如果點(diǎn)擊到了這些a標(biāo)簽中的元素,就不會(huì)觸發(fā)click事件,因?yàn)槭录壎ㄉ显赼標(biāo)簽元素上,而觸發(fā)這些內(nèi)部的元素時(shí),e.target指向的是觸發(fā)click事件的元素(span、img等其他元素)。

這種情況下就可以使用事件委托來(lái)處理,將事件綁定在a標(biāo)簽的內(nèi)部元素上,當(dāng)點(diǎn)擊它的時(shí)候,就會(huì)逐級(jí)向上查找,知道找到a標(biāo)簽為止,代碼如下:

document.addEventListener("click", function(e) {
    var node = e.target;
    while (node.parentNode.nodeName != "BODY") {
        if (node.nodeName == "A") {
            console.log("a");
            break;
        }
        node = node.parentNode;
    }
}, false);

5. 同步和異步的區(qū)別

  • 同步指的是當(dāng)一個(gè)進(jìn)程在執(zhí)行某個(gè)請(qǐng)求時(shí),如果這個(gè)請(qǐng)求需要等待一段時(shí)間才能返回,那么這個(gè)進(jìn)程會(huì)一直等待下去,直到消息返回為止再繼續(xù)向下執(zhí)行。
  • 異步指的是當(dāng)一個(gè)進(jìn)程在執(zhí)行某個(gè)請(qǐng)求時(shí),如果這個(gè)請(qǐng)求需要等待一段時(shí)間才能返回,這個(gè)時(shí)候進(jìn)程會(huì)繼續(xù)往下執(zhí)行,不會(huì)阻塞等待消息的返回,當(dāng)消息返回時(shí)系統(tǒng)再通知進(jìn)程進(jìn)行處理。

6. 對(duì)事件循環(huán)的理解

因?yàn)?js 是單線(xiàn)程運(yùn)行的,在代碼執(zhí)行時(shí),通過(guò)將不同函數(shù)的執(zhí)行上下文壓入執(zhí)行棧中來(lái)保證代碼的有序執(zhí)行。在執(zhí)行同步代碼時(shí),如果遇到異步事件,js 引擎并不會(huì)一直等待其返回結(jié)果,而是會(huì)將這個(gè)事件掛起,繼續(xù)執(zhí)行執(zhí)行棧中的其他任務(wù)。當(dāng)異步事件執(zhí)行完畢后,再將異步事件對(duì)應(yīng)的回調(diào)加入到一個(gè)任務(wù)隊(duì)列中等待執(zhí)行。任務(wù)隊(duì)列可以分為宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列,當(dāng)當(dāng)前執(zhí)行棧中的事件執(zhí)行完畢后,js 引擎首先會(huì)判斷微任務(wù)隊(duì)列中是否有任務(wù)可以執(zhí)行,如果有就將微任務(wù)隊(duì)首的事件壓入棧中執(zhí)行。當(dāng)微任務(wù)隊(duì)列中的任務(wù)都執(zhí)行完成后再去執(zhí)行宏任務(wù)隊(duì)列中的任務(wù)。

image

Event Loop 執(zhí)行順序如下所示:

  • 首先執(zhí)行同步代碼,這屬于宏任務(wù)
  • 當(dāng)執(zhí)行完所有同步代碼后,執(zhí)行棧為空,查詢(xún)是否有異步代碼需要執(zhí)行
  • 執(zhí)行所有微任務(wù)
  • 當(dāng)執(zhí)行完所有微任務(wù)后,如有必要會(huì)渲染頁(yè)面
  • 然后開(kāi)始下一輪 Event Loop,執(zhí)行宏任務(wù)中的異步代碼

7. 宏任務(wù)和微任務(wù)分別有哪些

  • 微任務(wù)包括: promise 的回調(diào)、node 中的 process.nextTick 、對(duì) Dom 變化監(jiān)聽(tīng)的 MutationObserver。
  • 宏任務(wù)包括: script 腳本的執(zhí)行、setTimeout ,setInterval ,setImmediate 一類(lèi)的定時(shí)事件,還有如 I/O 操作、UI 渲染等。

8. 什么是執(zhí)行棧

可以把執(zhí)行棧認(rèn)為是一個(gè)存儲(chǔ)函數(shù)調(diào)用的棧結(jié)構(gòu),遵循先進(jìn)后出的原則。


當(dāng)開(kāi)始執(zhí)行 JS 代碼時(shí),根據(jù)先進(jìn)后出的原則,后執(zhí)行的函數(shù)會(huì)先彈出棧,可以看到,foo 函數(shù)后執(zhí)行,當(dāng)執(zhí)行完畢后就從棧中彈出了。

平時(shí)在開(kāi)發(fā)中,可以在報(bào)錯(cuò)中找到執(zhí)行棧的痕跡:

function foo() {
  throw new Error('error')
}
function bar() {
  foo()
}
bar()


可以看到報(bào)錯(cuò)在 foo 函數(shù),foo 函數(shù)又是在 bar 函數(shù)中調(diào)用的。當(dāng)使用遞歸時(shí),因?yàn)闂?纱娣诺暮瘮?shù)是有限制的,一旦存放了過(guò)多的函數(shù)且沒(méi)有得到釋放的話(huà),就會(huì)出現(xiàn)爆棧的問(wèn)題

function bar() {
  bar()
}
bar()


9. Node 中的 Event Loop 和瀏覽器中的有什么區(qū)別?process.nextTick 執(zhí)行順序?

Node 中的 Event Loop 和瀏覽器中的是完全不相同的東西。

Node 的 Event Loop 分為 6 個(gè)階段,它們會(huì)按照順序反復(fù)運(yùn)行。每當(dāng)進(jìn)入某一個(gè)階段的時(shí)候,都會(huì)從對(duì)應(yīng)的回調(diào)隊(duì)列中取出函數(shù)去執(zhí)行。當(dāng)隊(duì)列為空或者執(zhí)行的回調(diào)函數(shù)數(shù)量到達(dá)系統(tǒng)設(shè)定的閾值,就會(huì)進(jìn)入下一階段。


(1)Timers(計(jì)時(shí)器階段):初次進(jìn)入事件循環(huán),會(huì)從計(jì)時(shí)器階段開(kāi)始。此階段會(huì)判斷是否存在過(guò)期的計(jì)時(shí)器回調(diào)(包含 setTimeout 和 setInterval),如果存在則會(huì)執(zhí)行所有過(guò)期的計(jì)時(shí)器回調(diào),執(zhí)行完畢后,如果回調(diào)中觸發(fā)了相應(yīng)的微任務(wù),會(huì)接著執(zhí)行所有微任務(wù),執(zhí)行完微任務(wù)后再進(jìn)入 Pending callbacks 階段。

(2)Pending callbacks:執(zhí)行推遲到下一個(gè)循環(huán)迭代的I / O回調(diào)(系統(tǒng)調(diào)用相關(guān)的回調(diào))。

(3)Idle/Prepare:僅供內(nèi)部使用。

(4)Poll(輪詢(xún)階段):

  • 當(dāng)回調(diào)隊(duì)列不為空時(shí):會(huì)執(zhí)行回調(diào),若回調(diào)中觸發(fā)了相應(yīng)的微任務(wù),這里的微任務(wù)執(zhí)行時(shí)機(jī)和其他地方有所不同,不會(huì)等到所有回調(diào)執(zhí)行完畢后才執(zhí)行,而是針對(duì)每一個(gè)回調(diào)執(zhí)行完畢后,就執(zhí)行相應(yīng)微任務(wù)。執(zhí)行完所有的回調(diào)后,變?yōu)橄旅娴那闆r。
  • 當(dāng)回調(diào)隊(duì)列為空時(shí)(沒(méi)有回調(diào)或所有回調(diào)執(zhí)行完畢):但如果存在有計(jì)時(shí)器(setTimeout、setInterval和setImmediate)沒(méi)有執(zhí)行,會(huì)結(jié)束輪詢(xún)階段,進(jìn)入 Check 階段。否則會(huì)阻塞并等待任何正在執(zhí)行的I/O操作完成,并馬上執(zhí)行相應(yīng)的回調(diào),直到所有回調(diào)執(zhí)行完畢。

(5)Check(查詢(xún)階段):會(huì)檢查是否存在 setImmediate 相關(guān)的回調(diào),如果存在則執(zhí)行所有回調(diào),執(zhí)行完畢后,如果回調(diào)中觸發(fā)了相應(yīng)的微任務(wù),會(huì)接著執(zhí)行所有微任務(wù),執(zhí)行完微任務(wù)后再進(jìn)入 Close callbacks 階段。

(6)Close callbacks:執(zhí)行一些關(guān)閉回調(diào),比如socket.on('close', ...)等。

下面來(lái)看一個(gè)例子,首先在有些情況下,定時(shí)器的執(zhí)行順序其實(shí)是隨機(jī)

setTimeout(() => {
    console.log('setTimeout')
}, 0)
setImmediate(() => {
    console.log('setImmediate')
})

對(duì)于以上代碼來(lái)說(shuō),setTimeout 可能執(zhí)行在前,也可能執(zhí)行在后

  • 首先 ?setTimeout(fn, 0) === setTimeout(fn, 1)?,這是由源碼決定的
  • 進(jìn)入事件循環(huán)也是需要成本的,如果在準(zhǔn)備時(shí)候花費(fèi)了大于 1ms 的時(shí)間,那么在 timer 階段就會(huì)直接執(zhí)行 ?setTimeout ?回調(diào)
  • 那么如果準(zhǔn)備時(shí)間花費(fèi)小于 1ms,那么就是 ?setImmediate ?回調(diào)先執(zhí)行了

當(dāng)然在某些情況下,他們的執(zhí)行順序一定是固定的,比如以下代碼:

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})

在上述代碼中,setImmediate 永遠(yuǎn)先執(zhí)行。因?yàn)閮蓚€(gè)代碼寫(xiě)在 IO 回調(diào)中,IO 回調(diào)是在 poll 階段執(zhí)行,當(dāng)回調(diào)執(zhí)行完畢后隊(duì)列為空,發(fā)現(xiàn)存在 setImmediate 回調(diào),所以就直接跳轉(zhuǎn)到 check 階段去執(zhí)行回調(diào)了。

上面都是 macrotask 的執(zhí)行情況,對(duì)于 microtask 來(lái)說(shuō),它會(huì)在以上每個(gè)階段完成前清空 microtask 隊(duì)列,下圖中的 Tick 就代表了 microtask


setTimeout(() => {
  console.log('timer21')
}, 0)
Promise.resolve().then(function() {
  console.log('promise1')
})

對(duì)于以上代碼來(lái)說(shuō),其實(shí)和瀏覽器中的輸出是一樣的,microtask 永遠(yuǎn)執(zhí)行在 macrotask 前面。

最后來(lái)看 Node 中的 process.nextTick,這個(gè)函數(shù)其實(shí)是獨(dú)立于 Event Loop 之外的,它有一個(gè)自己的隊(duì)列,當(dāng)每個(gè)階段完成后,如果存在 nextTick 隊(duì)列,就會(huì)清空隊(duì)列中的所有回調(diào)函數(shù),并且優(yōu)先于其他 microtask 執(zhí)行。

setTimeout(() => {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)
process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})

對(duì)于以上代碼,永遠(yuǎn)都是先把 nextTick 全部打印出來(lái)。

順序

//macro-task:script(全部的代碼) setInterval setTimeout setImmediate I/O
//micro-task:process.nextTick  Promise

10. 事件觸發(fā)的過(guò)程是怎樣的

事件觸發(fā)有三個(gè)階段:

  • ?window ?往事件觸發(fā)處傳播,遇到注冊(cè)的捕獲事件會(huì)觸發(fā)
  • 傳播到事件觸發(fā)處時(shí)觸發(fā)注冊(cè)的事件
  • 從事件觸發(fā)處往 ?window ?傳播,遇到注冊(cè)的冒泡事件會(huì)觸發(fā)

事件觸發(fā)一般來(lái)說(shuō)會(huì)按照上面的順序進(jìn)行,但是也有特例,如果給一個(gè) body 中的子節(jié)點(diǎn)同時(shí)注冊(cè)冒泡和捕獲事件,事件觸發(fā)會(huì)按照注冊(cè)的順序執(zhí)行。

// 以下會(huì)先打印冒泡然后是捕獲
node.addEventListener(
  'click',
  event => {
    console.log('冒泡')
  },
  false
)
node.addEventListener(
  'click',
  event => {
    console.log('捕獲 ')
  },
  true
)

通常使用 addEventListener 注冊(cè)事件,該函數(shù)的第三個(gè)參數(shù)可以是布爾值,也可以是對(duì)象。對(duì)于布爾值 useCapture 參數(shù)來(lái)說(shuō),該參數(shù)默認(rèn)值為 false ,useCapture 決定了注冊(cè)的事件是捕獲事件還是冒泡事件。對(duì)于對(duì)象參數(shù)來(lái)說(shuō),可以使用以下幾個(gè)屬性:

  • ?capture?:布爾值,和 ?useCapture ?作用一樣
  • ?once?:布爾值,值為 ?true ?表示該回調(diào)只會(huì)調(diào)用一次,調(diào)用后會(huì)移除監(jiān)聽(tīng)
  • ?passive?:布爾值,表示永遠(yuǎn)不會(huì)調(diào)用 ?preventDefault?

一般來(lái)說(shuō),如果只希望事件只觸發(fā)在目標(biāo)上,這時(shí)候可以使用 stopPropagation 來(lái)阻止事件的進(jìn)一步傳播。通常認(rèn)為 stopPropagation 是用來(lái)阻止事件冒泡的,其實(shí)該函數(shù)也可以阻止捕獲事件。

stopImmediatePropagation 同樣也能實(shí)現(xiàn)阻止事件,但是還能阻止該事件目標(biāo)執(zhí)行別的注冊(cè)事件。

node.addEventListener(
  'click',
  event => {
    event.stopImmediatePropagation()
    console.log('冒泡')
  },
  false
)
// 點(diǎn)擊 node 只會(huì)執(zhí)行上面的函數(shù),該函數(shù)不會(huì)執(zhí)行
node.addEventListener(
  'click',
  event => {
    console.log('捕獲 ')
  },
  true
)

九、瀏覽器垃圾回收機(jī)制


1. V8的垃圾回收機(jī)制是怎樣的

V8 實(shí)現(xiàn)了準(zhǔn)確式 GC,GC 算法采用了分代式垃圾回收機(jī)制。因此,V8 將內(nèi)存(堆)分為新生代和老生代兩部分。

(1)新生代算法

新生代中的對(duì)象一般存活時(shí)間較短,使用 Scavenge GC 算法。

在新生代空間中,內(nèi)存空間分為兩部分,分別為 From 空間和 To 空間。在這兩個(gè)空間中,必定有一個(gè)空間是使用的,另一個(gè)空間是空閑的。新分配的對(duì)象會(huì)被放入 From 空間中,當(dāng) From 空間被占滿(mǎn)時(shí),新生代 GC 就會(huì)啟動(dòng)了。算法會(huì)檢查 From 空間中存活的對(duì)象并復(fù)制到 To 空間中,如果有失活的對(duì)象就會(huì)銷(xiāo)毀。當(dāng)復(fù)制完成后將 From 空間和 To 空間互換,這樣 GC 就結(jié)束了。

(2)老生代算法

老生代中的對(duì)象一般存活時(shí)間較長(zhǎng)且數(shù)量也多,使用了兩個(gè)算法,分別是標(biāo)記清除算法和標(biāo)記壓縮算法。

先來(lái)說(shuō)下什么情況下對(duì)象會(huì)出現(xiàn)在老生代空間中:

  • 新生代中的對(duì)象是否已經(jīng)經(jīng)歷過(guò)一次 Scavenge 算法,如果經(jīng)歷過(guò)的話(huà),會(huì)將對(duì)象從新生代空間移到老生代空間中。
  • To 空間的對(duì)象占比大小超過(guò) 25 %。在這種情況下,為了不影響到內(nèi)存分配,會(huì)將對(duì)象從新生代空間移到老生代空間中。

老生代中的空間很復(fù)雜,有如下幾個(gè)空間

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不變的對(duì)象空間
  NEW_SPACE,   // 新生代用于 GC 復(fù)制算法的空間
  OLD_SPACE,   // 老生代常駐對(duì)象空間
  CODE_SPACE,  // 老生代代碼對(duì)象空間
  MAP_SPACE,   // 老生代 map 對(duì)象
  LO_SPACE,    // 老生代大空間對(duì)象
  NEW_LO_SPACE,  // 新生代大空間對(duì)象
  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

在老生代中,以下情況會(huì)先啟動(dòng)標(biāo)記清除算法:

  • 某一個(gè)空間沒(méi)有分塊的時(shí)候
  • 空間中被對(duì)象超過(guò)一定限制
  • 空間不能保證新生代中的對(duì)象移動(dòng)到老生代中

在這個(gè)階段中,會(huì)遍歷堆中所有的對(duì)象,然后標(biāo)記活的對(duì)象,在標(biāo)記完成后,銷(xiāo)毀所有沒(méi)有被標(biāo)記的對(duì)象。在標(biāo)記大型對(duì)內(nèi)存時(shí),可能需要幾百毫秒才能完成一次標(biāo)記。這就會(huì)導(dǎo)致一些性能上的問(wèn)題。為了解決這個(gè)問(wèn)題,2011 年,V8 從 stop-the-world 標(biāo)記切換到增量標(biāo)志。在增量標(biāo)記期間,GC 將標(biāo)記工作分解為更小的模塊,可以讓 JS 應(yīng)用邏輯在模塊間隙執(zhí)行一會(huì),從而不至于讓?xiě)?yīng)用出現(xiàn)停頓情況。但在 2018 年,GC 技術(shù)又有了一個(gè)重大突破,這項(xiàng)技術(shù)名為并發(fā)標(biāo)記。該技術(shù)可以讓 GC 掃描和標(biāo)記對(duì)象時(shí),同時(shí)允許 JS 運(yùn)行。

清除對(duì)象后會(huì)造成堆內(nèi)存出現(xiàn)碎片的情況,當(dāng)碎片超過(guò)一定限制后會(huì)啟動(dòng)壓縮算法。在壓縮過(guò)程中,將活的對(duì)象向一端移動(dòng),直到所有對(duì)象都移動(dòng)完成然后清理掉不需要的內(nèi)存。

2. 哪些操作會(huì)造成內(nèi)存泄漏?

  • 第一種情況是由于使用未聲明的變量,而意外的創(chuàng)建了一個(gè)全局變量,而使這個(gè)變量一直留在內(nèi)存中無(wú)法被回收。
  • 第二種情況是設(shè)置了 setInterval 定時(shí)器,而忘記取消它,如果循環(huán)函數(shù)有對(duì)外部變量的引用的話(huà),那么這個(gè)變量會(huì)被一直留在內(nèi)存中,而無(wú)法被回收。
  • 第三種情況是獲取一個(gè) DOM 元素的引用,而后面這個(gè)元素被刪除,由于我們一直保留了對(duì)這個(gè)元素的引用,所以它也無(wú)法被回收。
  • 第四種情況是不合理的使用閉包,從而導(dǎo)致某些變量一直被留在內(nèi)存當(dāng)中。


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)