做爲一個合格的前端工程師,瀏覽器相關的工做原理是咱們進行性能優化的基石,我以前也強調過知識體系的重要性,這部分原理性的內容就是知識體系中的重要部分,必須緊緊掌握才能面對瞬息萬變的實際場景,針對性地給出實際方案,而不是背誦各類開發軍規和性能優化的條例,這樣很難發現真正的問題所在, 更沒法真正地解決問題。javascript
內容會涵蓋瀏覽器工做原理
、瀏覽器安全
和性能監控和分析
。文章會分上下兩次來發,今天這一篇是整個系列的上篇。html
緩存是性能優化中很是重要的一環,瀏覽器的緩存機制對開發也是很是重要的知識點。接下來以三個部分來把瀏覽器的緩存機制說清楚:前端
瀏覽器中的緩存做用分爲兩種狀況,一種是須要發送HTTP
請求,一種是不須要發送。html5
首先是檢查強緩存,這個階段不須要
發送HTTP請求。java
如何來檢查呢?經過相應的字段來進行,可是提及這個字段就有點門道了。git
在HTTP/1.0
和HTTP/1.1
當中,這個字段是不同的。在早期,也就是HTTP/1.0
時期,使用的是Expires,而HTTP/1.1
使用的是Cache-Control。讓咱們首先來看看Expires。github
Expires
即過時時間,存在於服務端返回的響應頭中,告訴瀏覽器在這個過時時間以前能夠直接從緩存裏面獲取數據,無需再次請求。好比下面這樣:web
Expires: Wed, 22 Nov 2019 08:41:00 GMT
複製代碼
表示資源在2019年11月22號8點41分
過時,過時了就得向服務端發請求。算法
這個方式看上去沒什麼問題,合情合理,但其實潛藏了一個坑,那就是服務器的時間和瀏覽器的時間可能並不一致,那服務器返回的這個過時時間可能就是不許確的。所以這種方式很快在後來的HTTP1.1版本中被拋棄了。chrome
在HTTP1.1中,採用了一個很是關鍵的字段:Cache-Control
。這個字段也是存在於
它和Expires
本質的不一樣在於它並無採用具體的過時時間點
這個方式,而是採用過時時長來控制緩存,對應的字段是max-age。好比這個例子:
Cache-Control:max-age=3600
複製代碼
表明這個響應返回後在 3600 秒,也就是一個小時以內能夠直接使用緩存。
若是你以爲它只有max-age
一個屬性的話,那就大錯特錯了。
它其實能夠組合很是多的指令,完成更多場景的緩存判斷, 將一些關鍵的屬性列舉以下: public: 客戶端和代理服務器均可以緩存。由於一個請求可能要通過不一樣的代理服務器
最後纔到達目標服務器,那麼結果就是不只僅瀏覽器能夠緩存數據,中間的任何代理節點均可以進行緩存。
private: 這種狀況就是隻有瀏覽器能緩存了,中間的代理服務器不能緩存。
no-cache: 跳過當前的強緩存,發送HTTP請求,即直接進入協商緩存階段
。
no-store:很是粗暴,不進行任何形式的緩存。
s-maxage:這和max-age
長得比較像,可是區別在於s-maxage是針對代理服務器的緩存時間。
值得注意的是,當Expires和Cache-Control同時存在的時候,Cache-Control會優先考慮。
固然,還存在一種狀況,當資源緩存時間超時了,也就是強緩存
失效了,接下來怎麼辦?沒錯,這樣就進入到第二級屏障——協商緩存了。
強緩存失效以後,瀏覽器在請求頭中攜帶相應的緩存tag
來向服務器發請求,由服務器根據這個tag,來決定是否使用緩存,這就是協商緩存。
具體來講,這樣的緩存tag分爲兩種: Last-Modified 和 ETag。這二者各有優劣,並不存在誰對誰有絕對的優點
,跟上面強緩存的兩個 tag 不同。
即最後修改時間。在瀏覽器第一次給服務器發送請求後,服務器會在響應頭中加上這個字段。
瀏覽器接收到後,若是再次請求,會在請求頭中攜帶If-Modified-Since
字段,這個字段的值也就是服務器傳來的最後修改時間。
服務器拿到請求頭中的If-Modified-Since
的字段後,其實會和這個服務器中該資源的最後修改時間
對比:
ETag
是服務器根據當前文件的內容,給文件生成的惟一標識,只要裏面的內容有改動,這個值就會變。服務器經過響應頭
把這個值給瀏覽器。
瀏覽器接收到ETag
的值,會在下次請求時,將這個值做爲If-None-Match這個字段的內容,並放到請求頭中,而後發給服務器。
服務器接收到If-None-Match後,會跟服務器上該資源的ETag進行比對:
精準度
上,ETag
優於Last-Modified
。優於 ETag 是按照內容給資源上標識,所以能準確感知資源的變化。而 Last-Modified 就不同了,它在一些特殊的狀況並不能準確感知資源變化,主要有兩種狀況:Last-Modified
優於ETag
,也很簡單理解,Last-Modified
僅僅只是記錄一個時間點,而 Etag
須要根據文件的具體內容生成哈希值。另外,若是兩種方式都支持的話,服務器會優先考慮ETag
。
前面咱們已經提到,當強緩存
命中或者協商緩存中服務器返回304的時候,咱們直接從緩存中獲取資源。那這些資源究竟緩存在什麼位置呢?
瀏覽器中的緩存位置一共有四種,按優先級從高到低排列分別是:
Service Worker 借鑑了 Web Worker的 思路,即讓 JS 運行在主線程以外,因爲它脫離了瀏覽器的窗體,所以沒法直接訪問DOM
。雖然如此,但它仍然能幫助咱們完成不少有用的功能,好比離線緩存
、消息推送
和網絡代理
等功能。其中的離線緩存
就是 Service Worker Cache。
Service Worker 同時也是 PWA 的重要實現機制,關於它的細節和特性,咱們將會在後面的 PWA 的分享中詳細介紹。
Memory Cache指的是內存緩存,從效率上講它是最快的。可是從存活時間來說又是最短的,當渲染進程結束後,內存緩存也就不存在了。
Disk Cache就是存儲在磁盤中的緩存,從存取效率上講是比內存緩存慢的,可是他的優點在於存儲容量和存儲時長。稍微有些計算機基礎的應該很好理解,就不展開了。
好,如今問題來了,既然二者各有優劣,那瀏覽器如何決定將資源放進內存仍是硬盤呢?主要策略以下:
即推送緩存,這是瀏覽器緩存的最後一道防線。它是 HTTP/2
中的內容,雖然如今應用的並不普遍,但隨着 HTTP/2 的推廣,它的應用愈來愈普遍。關於 Push Cache,有很是多的內容能夠挖掘,不過這已經不是本文的重點,你們能夠參考這篇擴展文章。
對瀏覽器的緩存機制來作個簡要的總結:
首先經過 Cache-Control
驗證強緩存是否可用
Last-Modified
或者ETag
字段檢查資源是否更新
瀏覽器的本地存儲主要分爲Cookie
、WebStorage
和IndexedDB
, 其中WebStorage
又能夠分爲localStorage
和sessionStorage
。接下來咱們就來一一分析這些本地存儲方案。
Cookie
最開始被設計出來其實並非來作本地存儲的,而是爲了彌補HTTP
在狀態管理上的不足。
HTTP
協議是一個無狀態協議,客戶端向服務器發請求,服務器返回響應,故事就這樣結束了,可是下次發請求如何讓服務端知道客戶端是誰呢?
這種背景下,就產生了 Cookie
.
Cookie 本質上就是瀏覽器裏面存儲的一個很小的文本文件,內部以鍵值對的方式來存儲(在chrome開發者面板的Application
這一欄能夠看到)。向同一個域名下發送請求,都會攜帶相同的 Cookie,服務器拿到 Cookie 進行解析,便能拿到客戶端的狀態。
Cookie 的做用很好理解,就是用來作狀態存儲的,但它也是有諸多致命的缺陷的:
容量缺陷。Cookie 的體積上限只有4KB
,只能用來存儲少許的信息。
性能缺陷。Cookie 緊跟域名,無論域名下面的某一個地址需不須要這個 Cookie ,請求都會攜帶上完整的 Cookie,這樣隨着請求數的增多,其實會形成巨大的性能浪費的,由於請求攜帶了不少沒必要要的內容。
安全缺陷。因爲 Cookie 以純文本的形式在瀏覽器和服務器中傳遞,很容易被非法用戶截獲,而後進行一系列的篡改,在 Cookie 的有效期內從新發送給服務器,這是至關危險的。另外,在HttpOnly
爲 false 的狀況下,Cookie 信息能直接經過 JS 腳原本讀取。
localStorage
有一點跟Cookie
同樣,就是針對一個域名,即在同一個域名下,會存儲相同的一段localStorage。
不過它相對Cookie
仍是有至關多的區別的:
容量。localStorage 的容量上限爲5M,相比於Cookie
的 4K 大大增長。固然這個 5M 是針對一個域名的,所以對於一個域名是持久存儲的。
只存在客戶端,默認不參與與服務端的通訊。這樣就很好地避免了 Cookie 帶來的性能問題和安全問題。
接口封裝。經過localStorage
暴露在全局,並經過它的 setItem
和 getItem
等方法進行操做,很是方便。
接下來咱們來具體看看如何來操做localStorage
。
let obj = { name: "sanyuan", age: 18 };
localStorage.setItem("name", "sanyuan");
localStorage.setItem("info", JSON.stringify(obj));
複製代碼
接着進入相同的域名時就能拿到相應的值:
let name = localStorage.getItem("name");
let info = JSON.parse(localStorage.getItem("info"));
複製代碼
從這裏能夠看出,localStorage
其實存儲的都是字符串,若是是存儲對象須要調用JSON
的stringify
方法,而且用JSON.parse
來解析成對象。
利用localStorage
的較大容量和持久特性,能夠利用localStorage
存儲一些內容穩定的資源,好比官網的logo
,存儲Base64
格式的圖片資源,所以利用localStorage
sessionStorage
如下方面和localStorage
一致:
sessionStorage
名字有所變化,存儲方式、操做方式均和localStorage
同樣。但sessionStorage
和localStorage
有一個本質的區別,那就是前者只是會話級別的存儲,並非持久化存儲。會話結束,也就是頁面關閉,這部分sessionStorage
就不復存在了。
sessionStorage
就再合適不過了。事實上微博就採起了這樣的存儲方式。IndexedDB
是運行在瀏覽器中的非關係型數據庫
, 本質上是數據庫,毫不是和剛纔WebStorage的 5M 一個量級,理論上這個容量是沒有上限的。
關於它的使用,本文側重原理,並且 MDN 上的教程文檔已經很是詳盡,這裏就不作贅述了,感興趣能夠看一下使用文檔。
接着咱們來分析一下IndexedDB
的一些重要特性,除了擁有數據庫自己的特性,好比支持事務
,存儲二進制數據
,還有這樣一些特性須要格外注意:
對象倉庫
存放數據,在這個對象倉庫中數據採用鍵值對的方式來存儲。瀏覽器中各類本地存儲和緩存技術的發展,給前端應用帶來了大量的機會,PWA 也正是依託了這些優秀的存儲方案才得以發展起來。從新梳理一下這些本地存儲方案:
cookie
並不適合存儲,並且存在很是多的缺陷。Web Storage
包括localStorage
和sessionStorage
, 默認不會參與和服務器的通訊。IndexedDB
爲運行在瀏覽器上的非關係型數據庫,爲大型數據的存儲提供了接口。這是一個能夠無限難的問題。出這個題目的目的就是爲了考察你的 web 基礎深刻到什麼程度。因爲水平和篇幅有限,在這裏我將把其中一些重要的過程給你們梳理一遍,相信能在絕大部分的狀況下給出一個比較驚豔的答案。
這裏我提早聲明,因爲是一個綜合性很是強的問題,可能會在某一個點上深挖出很是多的細節,我我的以爲學習是一個按部就班的過程,在明白了總體過程後再去本身研究這些細節,會對整個知識體系有更深的理解。同時,關於延申出來的細節點我都有參考資料,看完這篇以後不妨再去深刻學習一下,擴展知識面。
好,正題開始。
此時此刻,你在瀏覽器地址欄輸入了百度的網址:
https://www.baidu.com/
複製代碼
瀏覽器會構建請求行:
// 請求方法是GET,路徑爲根路徑,HTTP協議版本爲1.1
GET / HTTP/1.1
複製代碼
先檢查強緩存,若是命中直接使用,不然進入下一步。關於強緩存,若是不清楚能夠參考上一篇文章。
因爲咱們輸入的是域名,而數據包是經過IP地址
傳給對方的。所以咱們須要獲得域名對應的IP地址
。這個過程須要依賴一個服務系統,這個系統將域名和 IP 一一映射,咱們將這個系統就叫作DNS(域名系統)。獲得具體 IP 的過程就是DNS
解析。
固然,值得注意的是,瀏覽器提供了DNS數據緩存功能。即若是一個域名已經解析過,那會把解析的結果緩存下來,下次處理直接走緩存,不須要通過 DNS解析
。
另外,若是不指定端口的話,默認採用對應的 IP 的 80 端口。
這裏要提醒一點,Chrome 在同一個域名下要求同時最多隻能有 6 個 TCP 鏈接,超過 6 個的話剩下的請求就得等待。
假設如今不須要等待,咱們進入了 TCP 鏈接的創建階段。首先解釋一下什麼是 TCP:
TCP(Transmission Control Protocol,傳輸控制協議)是一種面向鏈接的、可靠的、基於字節流的傳輸層通訊協議。
創建 TCP鏈接
經歷了下面三個階段:
確認
, 若是發送方沒有接到這個確認
的消息,就斷定爲數據包丟失,並從新發送該數據包。固然,發送的過程當中還有一個優化策略,就是把大的數據包拆成一個個小包
,依次傳輸到接收方,接收方按照這個小包的順序把它們組裝
成完整數據包。讀到這裏,你應該明白 TCP 鏈接經過什麼手段來保證數據傳輸的可靠性,一是三次握手
確認鏈接,二是數據包校驗
保證數據到達接收方,三是經過四次揮手
斷開鏈接。
固然,若是再深刻地問,好比爲何要三次握手,兩次不行嗎?第三次握手失敗了怎麼辦?爲何要四次揮手等等這一系列的問題,涉及計算機網絡的基礎知識,比較底層,可是也是很是重要的細節,但願你能好好研究一下,另外這裏有一篇不錯的文章,點擊進入相應的推薦文章,相信這篇文章能給你啓發。
如今TCP鏈接
創建完畢,瀏覽器能夠和服務器開始通訊,即開始發送 HTTP 請求。瀏覽器發 HTTP 請求要攜帶三樣東西:請求行、請求頭和請求體。
首先,瀏覽器會向服務器發送請求行,關於請求行, 咱們在這一部分的第一步就構建完了,貼一下內容:
// 請求方法是GET,路徑爲根路徑,HTTP協議版本爲1.1
GET / HTTP/1.1
複製代碼
結構很簡單,由請求方法、請求URI和HTTP版本協議組成。
同時也要帶上請求頭,好比咱們以前說的Cache-Control、If-Modified-Since、If-None-Match都由可能被放入請求頭中做爲緩存的標識信息。固然了還有一些其餘的屬性,列舉以下:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Cookie: /* 省略cookie信息 */
Host: www.baidu.com
Pragma: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1
複製代碼
最後是請求體,請求體只有在POST
方法下存在,常見的場景是表單提交。
HTTP 請求到達服務器,服務器進行對應的處理。最後要把數據傳給瀏覽器,也就是返回網絡響應。
跟請求部分相似,網絡響應具備三個部分:響應行、響應頭和響應體。
響應行相似下面這樣:
HTTP/1.1 200 OK
複製代碼
由HTTP協議版本
、狀態碼
和狀態描述
組成。
響應頭包含了服務器及其返回數據的一些信息, 服務器生成數據的時間、返回的數據類型以及對即將寫入的Cookie信息。
舉例以下:
Cache-Control: no-cache
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html;charset=utf-8
Date: Wed, 04 Dec 2019 12:29:13 GMT
Server: apache
Set-Cookie: rsv_i=f9a0SIItKqzv7kqgAAgphbGyRts3RwTg%2FLyU3Y5Eh5LwyfOOrAsvdezbay0QqkDqFZ0DfQXby4wXKT8Au8O7ZT9UuMsBq2k; path=/; domain=.baidu.com
複製代碼
響應完成以後怎麼辦?TCP 鏈接就斷開了嗎?
不必定。這時候要判斷Connection
字段, 若是請求頭或響應頭中包含Connection: Keep-Alive,表示創建了持久鏈接,這樣TCP
鏈接會一直保持,以後請求統一站點的資源會複用這個鏈接。
不然斷開TCP
鏈接, 請求-響應流程結束。
到此,咱們來總結一下主要內容,也就是瀏覽器端的網絡請求過程:
完成了網絡請求和響應,若是響應頭中Content-Type
的值是text/html
,那麼接下來就是瀏覽器的解析
和渲染
工做了。
首先來介紹解析部分,主要分爲如下幾個步驟:
DOM
樹樣式
計算佈局樹
(Layout Tree
)因爲瀏覽器沒法直接理解HTML字符串
,所以將這一系列的字節流轉換爲一種有意義而且方便操做的數據結構,這種數據結構就是DOM樹
。DOM樹
本質上是一個以document
爲根節點的多叉樹。
那經過什麼樣的方式來進行解析呢?
首先,咱們應該清楚把握一點: HTML 的文法並非上下文無關文法
。
這裏,有必要討論一下什麼是上下文無關文法
。
在計算機科學的編譯原理學科中,有很是明確的定義:
若一個形式文法G = (N, Σ, P, S) 的產生式規則都取以下的形式:V->w,則叫上下文無關語法。其中 V∈N ,w∈(N∪Σ)* 。
其中把 G = (N, Σ, P, S) 中各個參量的意義解釋一下:
通俗一點講,上下文無關的文法
就是說這個文法中全部產生式的左邊都是一個非終結符。
看到這裏,若是還有一點懵圈,我舉個例子你就明白了。
好比:
A -> B
複製代碼
這個文法中,每一個產生式左邊都會有一個非終結符,這就是上下文無關的文法
。在這種狀況下,xBy
必定是能夠規約出xAy
的。
咱們下面看看看一個反例:
aA -> B
Aa -> B
複製代碼
這種狀況就是否是上下文無關的文法
,當遇到B
的時候,咱們不知道到底能不能規約出A
,取決於左邊或者右邊是否有a
存在,也就是說和上下文有關。
關於它爲何是非上下文無關文法
,首先須要讓你們注意的是,規範的 HTML 語法,是符合上下文無關文法
的,可以體現它非上下文無關
的是不標準的語法。在此我僅舉一個反例便可證實。
好比解析器掃描到form
標籤的時候,上下文無關文法的處理方式是直接建立對應 form 的 DOM 對象,而真實的 HTML5 場景中卻不是這樣,解析器會查看 form
的上下文,若是這個 form
標籤的父標籤也是 form
, 那麼直接跳過當前的 form
標籤,不然才建立 DOM 對象。
常規的編程語言都是上下文無關的,而HTML卻相反,也正是它非上下文無關的特性,決定了HTML Parser
並不能使用常規編程語言的解析器來完成,須要另闢蹊徑。
HTML5 規範詳細地介紹瞭解析算法。這個算法分爲兩個階段:
對應的兩個過程就是詞法分析和語法分析。
這個算法輸入爲HTML文本
,輸出爲HTML標記
,也成爲標記生成器。其中運用有限自動狀態機來完成。即在噹噹前狀態下,接收一個或多個字符,就會更新到下一個狀態。
<html>
<body>
Hello sanyuan
</body>
</html>
複製代碼
經過一個簡單的例子來演示一下標記化
的過程。
遇到<
, 狀態爲標記打開。
接收[a-z]
的字符,會進入標記名稱狀態。
這個狀態一直保持,直到遇到>
,表示標記名稱記錄完成,這時候變爲數據狀態。
接下來遇到body
標籤作一樣的處理。
這個時候html
和body
的標記都記錄好了。
如今來到<body>中的>,進入數據狀態,以後保持這樣狀態接收後面的字符hello sanyuan。
接着接收 </body> 中的<
,回到標記打開, 接收下一個/
後,這時候會建立一個end tag
的token。
隨後進入標記名稱狀態, 遇到>
回到數據狀態。
接着以一樣的樣式處理 </body>。
以前提到過,DOM 樹是一個以document
爲根節點的多叉樹。所以解析器首先會建立一個document
對象。標記生成器會把每一個標記的信息發送給建樹器。建樹器接收到相應的標記時,會建立對應的 DOM 對象。建立這個DOM對象
後會作兩件事情:
DOM對象
加入 DOM 樹中。閉合標籤
意思對應)元素的棧中。仍是拿下面這個例子說:
<html>
<body>
Hello sanyuan
</body>
</html>
複製代碼
首先,狀態爲初始化狀態。
接收到標記生成器傳來的html
標籤,這時候狀態變爲before html狀態。同時建立一個HTMLHtmlElement
的 DOM 元素, 將其加到document
根對象上,並進行壓棧操做。
接着狀態自動變爲before head, 此時從標記生成器那邊傳來body
,表示並無head
, 這時候建樹器會自動建立一個HTMLHeadElement並將其加入到DOM樹
中。
如今進入到in head狀態, 而後直接跳到after head。
如今標記生成器傳來了body
標記,建立HTMLBodyElement, 插入到DOM
樹中,同時壓入開放標記棧。
接着狀態變爲in body,而後來接收後面一系列的字符: Hello sanyuan。接收到第一個字符的時候,會建立一個Text節點並把字符插入其中,而後把Text節點插入到 DOM 樹中body元素
的下面。隨着不斷接收後面的字符,這些字符會附在Text節點上。
如今,標記生成器傳過來一個body
的結束標記,進入到after body狀態。
標記生成器最後傳過來一個html
的結束標記, 進入到after after body的狀態,表示解析過程到此結束。
講到HTML5
規範,就不得不說它強大的寬容策略, 容錯能力很是強,雖然你們褒貶不一,不過我想做爲一名資深的前端工程師,有必要知道HTML Parser
在容錯方面作了哪些事情。
接下來是 WebKit 中一些經典的容錯示例,發現有其餘的也歡迎來補充。
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
複製代碼
所有換爲<br>的形式。
<table>
<table>
<tr><td>inner table</td></tr>
</table>
<tr><td>outer table</td></tr>
</table>
複製代碼
WebKit
會自動轉換爲:
<table>
<tr><td>outer table</td></tr>
</table>
<table>
<tr><td>inner table</td></tr>
</table>
複製代碼
這時候直接忽略裏面的form
。
關於CSS樣式,它的來源通常是三種:
首先,瀏覽器是沒法直接識別 CSS 樣式文本的,所以渲染引擎接收到 CSS 文本以後第一件事情就是將其轉化爲一個結構化的對象,即styleSheets。
這個格式化的過程過於複雜,並且對於不一樣的瀏覽器會有不一樣的優化策略,這裏就不展開了。
在瀏覽器控制檯可以經過document.styleSheets
來查看這個最終的結構。固然,這個結構包含了以上三種CSS來源,爲後面的樣式操做提供了基礎。
有一些 CSS 樣式的數值並不容易被渲染引擎所理解,所以須要在計算樣式以前將它們標準化,如em
->px
,red
->#ff0000
,bold
->700
等等。
樣式已經被格式化
和標準化
,接下來就能夠計算每一個節點的具體樣式信息了。
其實計算的方式也並不複雜,主要就是兩個規則: 繼承和層疊。
每一個子節點都會默認繼承父節點的樣式屬性,若是父節點中沒有找到,就會採用瀏覽器默認樣式,也叫UserAgent樣式
。這就是繼承規則,很是容易理解。
而後是層疊規則,CSS 最大的特色在於它的層疊性,也就是最終的樣式取決於各個屬性共同做用的效果,甚至有不少詭異的層疊現象,看過《CSS世界》的同窗應該對此深有體會,具體的層疊規則屬於深刻 CSS 語言的範疇,這裏就不過多介紹了。
不過值得注意的是,在計算完樣式以後,全部的樣式值會被掛在到window.computedStyle
當中,也就是能夠經過JS來獲取計算後的樣式,很是方便。
如今已經生成了DOM樹
和DOM樣式
,接下來要作的就是經過瀏覽器的佈局系統肯定元素的位置
,也就是要生成一棵佈局樹
(Layout Tree)。
佈局樹生成的大體工做以下:
佈局樹中
。值得注意的是,這棵佈局樹值包含可見元素,對於 head
標籤和設置了display: none
的元素,將不會被放入其中。
有人說首先會生成Render Tree
,也就是渲染樹,其實這仍是 16 年以前的事情,如今 Chrome 團隊已經作了大量的重構,已經沒有生成Render Tree
的過程了。而佈局樹的信息已經很是完善,徹底擁有Render Tree
的功能。
之因此不講佈局的細節,是由於它過於複雜,一一介紹會顯得文章過於臃腫,不過大部分狀況下咱們只須要知道它所作的工做是什麼便可,若是想深刻其中的原理,知道它是如何來作的,我強烈推薦你去讀一讀人人FED團隊的文章從Chrome源碼看瀏覽器如何layout佈局。
梳理一下這一節的主要脈絡:
上一節介紹了瀏覽器解析
的過程,其中包含構建DOM
、樣式計算
和構建佈局樹
。
接下來就來拆解下一個過程——渲染
。分爲如下幾個步驟:
圖層樹
(Layer Tree
)繪製列表
圖塊
並柵格化
若是你以爲如今DOM節點
也有了,樣式和位置信息也都有了,能夠開始繪製頁面了,那你就錯了。
由於你考慮掉了另一些複雜的場景,好比3D動畫如何呈現出變換效果,當元素含有層疊上下文時如何控制顯示和隱藏等等。
爲了解決如上所述的問題,瀏覽器在構建完佈局樹
以後,還會對特定的節點進行分層,構建一棵圖層樹
(Layer Tree
)。
那這棵圖層樹是根據什麼來構建的呢?
通常狀況下,節點的圖層會默認屬於父親節點的圖層(這些圖層也稱爲合成層)。那何時會提高爲一個單獨的合成層呢?
有兩種狀況須要分別討論,一種是顯式合成,一種是隱式合成。
下面是顯式合成
的狀況:
1、 擁有層疊上下文的節點。
層疊上下文也基本上是有一些特定的CSS屬性建立的,通常有如下狀況:
2、須要剪裁的地方。
好比一個div,你只給他設置 100 * 100 像素的大小,而你在裏面放了很是多的文字,那麼超出的文字部分就須要被剪裁。固然若是出現了滾動條,那麼滾動條會被單獨提高爲一個圖層。
接下來是隱式合成
,簡單來講就是層疊等級低
的節點被提高爲單獨的圖層以後,那麼全部層疊等級比它高
的節點都會成爲一個單獨的圖層。
這個隱式合成其實隱藏着巨大的風險,若是在一個大型應用中,當一個z-index
比較低的元素被提高爲單獨圖層以後,層疊在它上面的的元素通通都會被提高爲單獨的圖層,可能會增長上千個圖層,大大增長內存的壓力,甚至直接讓頁面崩潰。這就是層爆炸的原理。這裏有一個具體的例子,點擊打開。
值得注意的是,當須要repaint
時,只須要repaint
自己,而不會影響到其餘的層。
接下來渲染引擎會將圖層的繪製拆分紅一個個繪製指令,好比先畫背景、再描繪邊框......而後將這些指令按順序組合成一個待繪製列表,至關於給後面的繪製操做作了一波計劃。
這裏我以百度首頁爲例,你們能夠在 Chrome 開發者工具中在設置欄中展開 more tools
, 而後選擇Layers
面板,就能看到下面的繪製列表:
如今開始繪製操做,實際上在渲染進程中繪製操做是由專門的線程來完成的,這個線程叫合成線程。
繪製列表準備好了以後,渲染進程的主線程會給合成線程
發送commit
消息,把繪製列表提交給合成線程。接下來就是合成線程一展宏圖的時候啦。
首先,考慮到視口就這麼大,當頁面很是大的時候,要滑很長時間才能滑到底,若是要一口氣所有繪製出來是至關浪費性能的。所以,合成線程要作的第一件事情就是將圖層分塊。這些塊的大小通常不會特別大,一般是 256 * 256 或者 512 * 512 這個規格。這樣能夠大大加速頁面的首屏展現。
由於後面圖塊數據要進入 GPU 內存,考慮到瀏覽器內存上傳到 GPU 內存的操做比較慢,即便是繪製一部分圖塊,也可能會耗費大量時間。針對這個問題,Chrome 採用了一個策略: 在首次合成圖塊時只採用一個低分辨率的圖片,這樣首屏展現的時候只是展現出低分辨率的圖片,這個時候繼續進行合成操做,當正常的圖塊內容繪製完畢後,會將當前低分辨率的圖塊內容替換。這也是 Chrome 底層優化首屏加載速度的一個手段。
順便提醒一點,渲染進程中專門維護了一個柵格化線程池,專門負責把圖塊轉換爲位圖數據。
而後合成線程會選擇視口附近的圖塊,把它交給柵格化線程池生成位圖。
生成位圖的過程實際上都會使用 GPU 進行加速,生成的位圖最後發送給合成線程
。
柵格化操做完成後,合成線程會生成一個繪製命令,即"DrawQuad",併發送給瀏覽器進程。
瀏覽器進程中的viz組件
接收到這個命令,根據這個命令,把頁面內容繪製到內存,也就是生成了頁面,而後把這部份內存發送給顯卡。爲何發給顯卡呢?我想有必要先聊一聊顯示器顯示圖像的原理。
不管是 PC 顯示器仍是手機屏幕,都有一個固定的刷新頻率,通常是 60 HZ,即 60 幀,也就是一秒更新 60 張圖片,一張圖片停留的時間約爲 16.7 ms。而每次更新的圖片都來自顯卡的前緩衝區。而顯卡接收到瀏覽器進程傳來的頁面後,會合成相應的圖像,並將圖像保存到後緩衝區,而後系統自動將前緩衝區
和後緩衝區
對換位置,如此循環更新。
看到這裏你也就是明白,當某個動畫大量佔用內存的時候,瀏覽器生成圖像的時候會變慢,圖像傳送給顯卡就會不及時,而顯示器仍是以不變的頻率刷新,所以會出現卡頓,也就是明顯的掉幀現象。
到這裏,咱們算是把整個過程給走通了,如今從新來梳理一下頁面渲染的流程。
咱們首先來回顧一下渲染流水線
的流程:
接下來,咱們未來以此爲依據來介紹重繪和迴流,以及讓更新視圖的另一種方式——合成。
首先介紹迴流
。迴流
也叫重排
。
簡單來講,就是當咱們對 DOM 結構的修改引起 DOM 幾何尺寸變化的時候,會發生迴流
的過程。
具體一點,有如下的操做會觸發迴流:
一個 DOM 元素的幾何屬性變化,常見的幾何屬性有width
、height
、padding
、margin
、left
、top
、border
等等, 這個很好理解。
使 DOM 節點發生增減
或者移動
。
讀寫 offset
族、scroll
族和client
族屬性的時候,瀏覽器爲了獲取這些值,須要進行迴流操做。
調用 window.getComputedStyle
方法。
依照上面的渲染流水線,觸發迴流的時候,若是 DOM 結構發生改變,則從新渲染 DOM 樹,而後將後面的流程(包括主線程以外的任務)所有走一遍。
至關於將解析和合成的過程從新又走了一篇,開銷是很是大的。
當 DOM 的修改致使了樣式的變化,而且沒有影響幾何屬性的時候,會致使重繪
(repaint
)。
因爲沒有致使 DOM 幾何屬性的變化,所以元素的位置信息不須要更新,從而省去佈局的過程。流程以下:
跳過了生成佈局樹
和建圖層樹
的階段,直接生成繪製列表,而後繼續進行分塊、生成位圖等後面一系列操做。
能夠看到,重繪不必定致使迴流,但迴流必定發生了重繪。
還有一種狀況,是直接合成。好比利用 CSS3 的transform
、opacity
、filter
這些屬性就能夠實現合成的效果,也就是你們常說的GPU加速。
在合成的狀況下,會直接跳過佈局和繪製流程,直接進入非主線程
處理的部分,即直接交給合成線程
處理。交給它處理有兩大好處:
可以充分發揮GPU
的優點。合成線程生成位圖的過程當中會調用線程池,並在其中使用GPU
進行加速生成,而GPU 是擅長處理位圖數據的。
沒有佔用主線程的資源,即便主線程卡住了,效果依然可以流暢地展現。
知道上面的原理以後,對於開發過程有什麼指導意義呢?
class
的方式。createDocumentFragment
進行批量的 DOM 操做。tranform
, 任何能夠實現合成效果的 CSS 屬性都能用will-change
來聲明。這裏有一個實際的例子,一行will-change: tranform
拯救一個項目,點擊直達。XSS
全稱是 Cross Site Scripting
(即跨站腳本
),爲了和 CSS 區分,故叫它XSS
。XSS 攻擊是指瀏覽器中執行惡意腳本(不管是跨域仍是同域),從而拿到用戶的信息並進行操做。
這些操做通常能夠完成下面這些事情:
Cookie
。一般狀況,XSS 攻擊的實現有三種方式——存儲型、反射型和文檔型。原理都比較簡單,先來一一介紹一下。
存儲型
,顧名思義就是將惡意腳本存儲了起來,確實,存儲型的 XSS 將腳本存儲到了服務端的數據庫,而後在客戶端執行這些腳本,從而達到攻擊的效果。
常見的場景是留言評論區提交一段腳本代碼,若是先後端沒有作好轉義的工做,那評論內容存到了數據庫,在頁面渲染過程當中直接執行
, 至關於執行一段未知邏輯的 JS 代碼,是很是恐怖的。這就是存儲型的 XSS 攻擊。
反射型XSS
指的是惡意腳本做爲網絡請求的一部分。
好比我輸入:
http://sanyuan.com?q=<script>alert("你完蛋了")</script>
複製代碼
這楊,在服務器端會拿到q
參數,而後將內容返回給瀏覽器端,瀏覽器將這些內容做爲HTML的一部分解析,發現是一個腳本,直接執行,這樣就被攻擊了。
之因此叫它反射型
, 是由於惡意腳本是經過做爲網絡請求的參數,通過服務器,而後再反射到HTML文檔中,執行解析。和存儲型
不同的是,服務器並不會存儲這些惡意腳本。
文檔型的 XSS 攻擊並不會通過服務端,而是做爲中間人的角色,在數據傳輸過程劫持到網絡數據包,而後修改裏面的 html 文檔!
這樣的劫持方式包括WIFI路由器劫持
或者本地惡意軟件
等。
明白了三種XSS
攻擊的原理,咱們能發現一個共同點: 都是讓惡意腳本直接能在瀏覽器中執行。
那麼要防範它,就是要避免這些腳本代碼的執行。
爲了完成這一點,必須作到一個信念,兩個利用。
千萬不要相信任何用戶的輸入!
不管是在前端和服務端,都要對用戶的輸入進行轉碼或者過濾。
如:
<script>alert('你完蛋了')</script>
複製代碼
轉碼後變爲:
<script>alert('你完蛋了')</script>
複製代碼
這樣的代碼在 html 解析的過程當中是沒法執行的。
固然也能夠利用關鍵詞過濾的方式,將 script 標籤給刪除。那麼如今的內容只剩下:
複製代碼
什麼也沒有了:)
CSP,即瀏覽器中的內容安全策略,它的核心思想就是服務器決定瀏覽器加載哪些資源,具體來講能夠完成如下功能:
不少 XSS 攻擊腳本都是用來竊取Cookie, 而設置 Cookie 的 HttpOnly 屬性後,JavaScript 便沒法讀取 Cookie 的值。這樣也能很好的防範 XSS 攻擊。
XSS
攻擊是指瀏覽器中執行惡意腳本, 而後拿到用戶的信息進行操做。主要分爲存儲型
、反射型
和文檔型
。防範的措施包括:
CSRF(Cross-site request forgery), 即跨站請求僞造,指的是黑客誘導用戶點擊連接,打開黑客的網站,而後黑客利用用戶目前的登陸狀態發起跨站請求。
舉個例子, 你在某個論壇點擊了黑客精心挑選的小姐姐圖片,你點擊後,進入了一個新的頁面。
那麼恭喜你,被攻擊了:)
你可能會比較好奇,怎麼忽然就被攻擊了呢?接下來咱們就來拆解一下當你點擊了連接以後,黑客在背後作了哪些事情。
可能會作三樣事情。列舉以下:
黑客網頁裏面可能有一段這樣的代碼:
<img src="https://xxx.com/info?user=hhh&count=100">
複製代碼
進入頁面後自動發送 get 請求,值得注意的是,這個請求會自動帶上關於 xxx.com 的 cookie 信息(這裏是假定你已經在 xxx.com 中登陸過)。
假如服務器端沒有相應的驗證機制,它可能認爲發請求的是一個正常的用戶,由於攜帶了相應的 cookie,而後進行相應的各類操做,能夠是轉帳匯款以及其餘的惡意操做。
黑客可能本身填了一個表單,寫了一段自動提交的腳本。
<form id='hacker-form' action="https://xxx.com/info" method="POST">
<input type="hidden" name="user" value="hhh" />
<input type="hidden" name="count" value="100" />
</form>
<script>document.getElementById('hacker-form').submit();</script>
複製代碼
一樣也會攜帶相應的用戶 cookie 信息,讓服務器誤覺得是一個正常的用戶在操做,讓各類惡意的操做變爲可能。
在黑客的網站上,可能會放上一個連接,驅使你來點擊:
<a href="https://xxx/info?user=hhh&count=100" taget="_blank">點擊進入修仙世界</a>
複製代碼
點擊後,自動發送 get 請求,接下來和自動發 GET 請求
部分同理。
這就是CSRF
攻擊的原理。和XSS
攻擊對比,CSRF 攻擊並不須要將惡意代碼注入用戶當前頁面的html
文檔中,而是跳轉到新的頁面,利用服務器的驗證漏洞和用戶以前的登陸狀態來模擬用戶進行操做。
CSRF攻擊
中重要的一環就是自動發送目標站點下的 Cookie
,而後就是這一份 Cookie 模擬了用戶的身份。所以在Cookie
上面下文章是防範的不二之選。
剛好,在 Cookie 當中有一個關鍵的字段,能夠對請求中 Cookie 的攜帶做一些限制,這個字段就是SameSite
。
SameSite
能夠設置爲三個值,Strict
、Lax
和None
。
a. 在Strict
模式下,瀏覽器徹底禁止第三方請求攜帶Cookie。好比請求sanyuan.com
網站只能在sanyuan.com
域名當中請求才能攜帶 Cookie,在其餘網站請求都不能。
b. 在Lax
模式,就寬鬆一點了,可是隻能在 get 方法提交表單
況或者a 標籤發送 get 請求
的狀況下能夠攜帶 Cookie,其餘狀況均不能。
c. 在None
模式下,也就是默認模式,請求會自動攜帶上 Cookie。
這就須要要用到請求頭中的兩個字段: Origin和Referer。
其中,Origin只包含域名信息,而Referer包含了具體
的 URL 路徑。
固然,這二者都是能夠僞造的,經過 Ajax 中自定義請求頭便可,安全性略差。
Django
做爲 Python 的一門後端框架,若是是用它開發過的同窗就知道,在它的模板(template)中, 開發表單時,常常會附上這樣一行代碼:
{% csrf_token %}
複製代碼
這就是CSRF Token
的典型應用。那它的原理是怎樣的呢?
首先,瀏覽器向服務器發送請求時,服務器生成一個字符串,將其植入到返回的頁面中。
而後瀏覽器若是要發送請求,就必須帶上這個字符串,而後服務器來驗證是否合法,若是不合法則不予響應。這個字符串也就是CSRF Token
,一般第三方站點沒法拿到這個 token, 所以也就是被服務器給拒絕。
CSRF(Cross-site request forgery), 即跨站請求僞造,指的是黑客誘導用戶點擊連接,打開黑客的網站,而後黑客利用用戶目前的登陸狀態發起跨站請求。
CSRF
攻擊通常會有三種方式:
防範措施: 利用 Cookie 的 SameSite 屬性
、驗證來源站點
和CSRF Token
。
談到HTTPS
, 就不得不談到與之相對的HTTP
。HTTP
的特性是明文傳輸,所以在傳輸的每個環節,數據都有可能被第三方竊取或者篡改,具體來講,HTTP 數據通過 TCP 層,而後通過WIFI路由器
、運營商
和目標服務器
,這些環節中均可能被中間人拿到數據並進行篡改,也就是咱們常說的中間人攻擊。
爲了防範這樣一類攻擊,咱們不得已要引入新的加密方案,即 HTTPS。
HTTPS
並非一個新的協議, 而是一個增強版的HTTP
。其原理是在HTTP
和TCP
之間創建了一箇中間層,當HTTP
和TCP
通訊時並非像之前那樣直接通訊,直接通過了一箇中間層進行加密,將加密後的數據包傳給TCP
, 響應的,TCP
必須將數據包解密,才能傳給上面的HTTP
。這個中間層也叫安全層
。安全層
的核心就是對數據加解密
。
接下來咱們就來剖析一下HTTPS
的加解密是如何實現的。
首先須要理解對稱加密
和非對稱加密
的概念,而後討論二者應用後的效果如何。
對稱加密
是最簡單的方式,指的是加密
和解密
用的是一樣的密鑰。
而對於非對稱加密
,若是有 A、 B 兩把密鑰,若是用 A 加密過的數據包只能用 B 解密,反之,若是用 B 加密過的數據包只能用 A 解密。
接着咱們來談談瀏覽器
和服務器
進行協商加解密的過程。
首先,瀏覽器會給服務器發送一個隨機數client_random
和一個加密的方法列表。
服務器接收後給瀏覽器返回另外一個隨機數server_random
和加密方法。
如今,二者擁有三樣相同的憑證: client_random
、server_random
和加密方法。
接着用這個加密方法將兩個隨機數混合起來生成密鑰,這個密鑰就是瀏覽器和服務端通訊的暗號
。
若是用對稱加密
的方式,那麼第三方能夠在中間獲取到client_random
、server_random
和加密方法,因爲這個加密方法同時能夠解密,因此中間人能夠成功對暗號進行解密,拿到數據,很容易就將這種加密方式破解了。
既然對稱加密
這麼不堪一擊,咱們就來試一試非對稱
加密。在這種加密方式中,服務器手裏有兩把鑰匙,一把是公鑰
,也就是說每一個人都能拿到,是公開的,另外一把是私鑰
,這把私鑰只有服務器本身知道。
好,如今開始傳輸。
瀏覽器把client_random
和加密方法列表傳過來,服務器接收到,把server_random
、加密方法
和公鑰
傳給瀏覽器。
如今二者擁有相同的client_random
、server_random
和加密方法。而後瀏覽器用公鑰將client_random
和server_random
加密,生成與服務器通訊的暗號
。
這時候因爲是非對稱加密,公鑰加密過的數據只能用私鑰
解密,所以中間人就算拿到瀏覽器傳來的數據,因爲他沒有私鑰,照樣沒法解密,保證了數據的安全性。
這難道必定就安全嗎?聰明的小夥伴早就發現了端倪。回到非對稱加密
的定義,公鑰加密的數據能夠用私鑰解密,那私鑰加密的數據也能夠用公鑰解密呀!
服務器的數據只能用私鑰進行加密(由於若是它用公鑰那麼瀏覽器也無法解密啦),中間人一旦拿到公鑰,那麼就能夠對服務端傳來的數據進行解密了,就這樣又被破解了。
能夠發現,對稱加密和非對稱加密,單獨應用任何一個,都會存在安全隱患。那咱們能不能把二者結合,進一步保證安全呢?
實際上是能夠的,演示一下整個流程:
client_random
和加密方法列表。server_random
、加密方法以及公鑰。pre_random
, 而且用公鑰加密,傳給服務器。(敲黑板!重點操做!)pre_random
。如今瀏覽器和服務器有三樣相同的憑證:client_random
、server_random
和pre_random
。而後二者用相同的加密方法混合這三個隨機數,生成最終的密鑰
。
而後瀏覽器和服務器儘管用同樣的密鑰進行通訊,即便用對稱加密
。
這個最終的密鑰是很難被中間人拿到的,爲何呢? 由於中間人沒有私鑰,從而拿不到pre_random,也就沒法生成最終的密鑰了。
回頭比較一下和單純的使用非對稱加密, 這種方式作了什麼改進呢?本質上是防止了私鑰加密的數據外傳。單獨使用非對稱加密,最大的漏洞在於服務器傳數據給瀏覽器只能用私鑰
加密,這是危險產生的根源。利用對稱和非對稱
加密結合的方式,就防止了這一點,從而保證了安全。
儘管經過二者加密方式的結合,可以很好地實現加密傳輸,但實際上仍是存在一些問題。黑客若是採用 DNS 劫持,將目標地址替換成黑客服務器的地址,而後黑客本身造一份公鑰和私鑰,照樣能進行數據傳輸。而對於瀏覽器用戶而言,他是不知道本身正在訪問一個危險的服務器的。
事實上HTTPS
在上述結合對稱和非對稱加密
的基礎上,又添加了數字證書認證
的步驟。其目的就是讓服務器證實本身的身份。
爲了獲取這個證書,服務器運營者須要向第三方認證機構獲取受權,這個第三方機構也叫CA
(Certificate Authority
), 認證經過後 CA 會給服務器頒發數字證書。
這個數字證書有兩個做用:
這個驗證的過程發生在何時呢?
當服務器傳送server_random
、加密方法的時候,順便會帶上數字證書
(包含了公鑰
), 接着瀏覽器接收以後就會開始驗證數字證書。若是驗證經過,那麼後面的過程照常進行,不然拒絕執行。
如今咱們來梳理一下HTTPS
最終的加解密過程:
瀏覽器拿到數字證書後,如何來對證書進行認證呢?
首先,會讀取證書中的明文內容。CA 進行數字證書的簽名時會保存一個 Hash 函數,來這個函數來計算明文內容獲得信息A
,而後用公鑰解密明文內容獲得信息B
,兩份信息作比對,一致則表示認證合法。
固然有時候對於瀏覽器而言,它不知道哪些 CA 是值得信任的,所以會繼續查找 CA 的上級 CA,以一樣的信息比對方式驗證上級 CA 的合法性。通常根級的 CA 會內置在操做系統當中,固然若是向上找沒有找到根級的 CA,那麼將被視爲不合法。
HTTPS並非一個新的協議, 它在HTTP
和TCP
的傳輸中創建了一個安全層,利用對稱加密
和非對稱機密
結合數字證書認證的方式,讓傳輸過程的安全性大大提升。
節流的核心思想: 若是在定時器的時間範圍內再次觸發,則不予理睬,等當前定時器完成
,才能啓動下一個定時器任務。這就比如公交車,10 分鐘一趟,10 分鐘內有多少人在公交站等我無論,10 分鐘一到我就要發車走人!
代碼以下:
function throttle(fn, interval) {
let flag = true;
return funtion(...args) {
let context = this;
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(context, args);
flag = true;
}, interval);
};
};
複製代碼
寫成下面的方式也是表達同樣的意思:
const throttle = function(fn, interval) {
let last = 0;
return function (...args) {
let context = this;
let now = +new Date();
// 還沒到時間
if(now - last < interval) return;
last = now;
fn.apply(this, args)
}
}
複製代碼
核心思想: 每次事件觸發則刪除原來的定時器,創建新的定時器。跟王者榮耀的回城功能相似,你反覆觸發回城功能,那麼只認最後一次,從最後一次觸發開始計時。
function debounce(fn, delay) {
let timer = null;
return function (...args) {
let context = this;
if(timer) clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
}
}
複製代碼
如今咱們能夠把防抖
和節流
放到一塊兒,爲何呢?由於防抖有時候觸發的太頻繁會致使一次響應都沒有,咱們但願到了固定的時間必須給用戶一個響應,事實上不少前端庫就是採起了這樣的思路。
function throttle(fn, delay) {
let last = 0, timer = null;
return function (...args) {
let context = this;
let now = new Date();
if(now - last > delay){
clearTimeout(timer);
setTimeout(function() {
last = now;
fn.apply(context, args);
}, delay);
} else {
// 這個時候表示時間到了,必須給響應
last = now;
fn.apply(context, args);
}
}
}
複製代碼
首先給圖片一個佔位資源:
<img src="default.jpg" data-src="http://www.xxx.com/target.jpg" />
複製代碼
接着,經過監聽 scroll 事件來判斷圖片是否到達視口:
let img = document.getElementsByTagName("img");
let num = img.length;
let count = 0;//計數器,從第一張圖片開始計
lazyload();//首次加載別忘了顯示圖片
window.addEventListener('scroll', lazyload);
function lazyload() {
let viewHeight = document.documentElement.clientHeight;//視口高度
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;//滾動條捲去的高度
for(let i = count; i <num; i++) {
// 元素如今已經出如今視口中
if(img[i].offsetTop < scrollHeight + viewHeight) {
if(img[i].getAttribute("src") !== "default.jpg") continue;
img[i].src = img[i].getAttribute("data-src");
count ++;
}
}
}
複製代碼
固然,最好對 scroll 事件作節流處理,以避免頻繁觸發:
// throttle函數咱們上節已經實現
window.addEventListener('scroll', throttle(lazyload, 200));
複製代碼
如今咱們用另一種方式來判斷圖片是否出如今了當前視口, 即 DOM 元素的 getBoundingClientRect
API。
上述的 lazyload 函數改爲下面這樣:
function lazyload() {
for(let i = count; i <num; i++) {
// 元素如今已經出如今視口中
if(img[i].getBoundingClientRect().top < document.documentElement.clientHeight) {
if(img[i].getAttribute("src") !== "default.jpg") continue;
img[i].src = img[i].getAttribute("data-src");
count ++;
}
}
}
複製代碼
這是瀏覽器內置的一個API
,實現了監聽window的scroll事件
、判斷是否在視口中
以及節流
三大功能。
咱們來具體試一把:
let img = document.document.getElementsByTagName("img");
const observer = new IntersectionObserver(changes => {
//changes 是被觀察的元素集合
for(let i = 0, len = changes.length; i < len; i++) {
let change = changes[i];
// 經過這個屬性判斷是否在視口中
if(change.isIntersecting) {
const imgElement = change.target;
imgElement.src = imgElement.getAttribute("data-src");
observer.unobserve(imgElement);
}
}
})
observer.observe(img);
複製代碼
這樣就很方便地實現了圖片懶加載,固然這個IntersectionObserver
也能夠用做其餘資源的預加載,功能很是強大。
以上文章在均在開源項目前端靈魂之問首發,旨在打造完整前端知識體系,若是對你些許的幫助,請幫項目點一個star,很是感謝!
另外,本人的掘金小冊《React Hooks與Immutable數據流實戰》最近已經上線,雖然是關於React的實戰教程,但也包含了諸多瀏覽器和性能優化相關的實踐手段,一共有36
個小節(價格一直是9.9,後面不會再變更,期待上新優惠的同窗實在抱歉💗), 後期打算將 hooks 源碼解析的系列文章直接放到小冊中,不斷給小冊增值,相信能對正在進階的各位有所幫助,望多多支持!小冊連接
若是要獲取更多學習資料、與我取得聯繫,或者加羣事宜,可關注公衆號:
參考文獻: