前言
開宗明義,本瓜深知汝之痛點:前端面試知識點太雜,卿總為了面試而面試,忘了記,記了又忘,循環往複,為此叫苦不疊。
來,讓本瓜帶領各位都稍稍回顧一下,自己曾經在學生時代記憶元素周期表的光輝歲月。
氫、氦、锂、铍、硼、碳、氮、氧、氟、氖、鈉、鎂、鋁、矽、磷、硫、氯、氩、鉀、鈣、钪、钛、釩、鉻、猛、鐵、钴、鎳、銅、鋅......
咱當初記這前三十位元素,是死記硬背的嗎?答案是否定的,機智的我們用到了 串聯記憶法 。
一定是像這樣或類似這樣去記:
第一周期:氫 氦 ———— 輕嗨:輕輕的打個了招呼:嗨!
第二周期:锂 铍 硼 碳 氮 氧 氟 氖 ———— 你皮捧碳 蛋養福奶:你很皮,手裡捧了一把碳。雞蛋能夠滋養福氣的奶媽
第三周期:鈉 鎂 鋁 矽 磷 硫 氯 氩 ———— 那美女桂林留綠牙:那美女在桂林留綠色的牙齒
第四周期:鉀 鈣 钪 钛 釩 鉻 猛 鐵 钴 鎳 銅 鋅 ———— 賈蓋坑太凡哥 猛鐵骨裂痛心:“賈蓋”坑了“太凡哥”,導緻猛男鐵漢骨頭碎裂很痛心
串聯聯想與二進制鍊式聯想記憶的方法有相似之處,就是都要通過想象、創造和編故事來幫助我們達到雙腦學習和記憶的目的。—— 出處
有木有?本瓜記得尤為清楚,以上串聯起來的諧音故事簡直可以寫出一個狗血劇本了。尤其是“那美女(鈉鎂鋁)”簡單三字仿佛就能激起青春期的荷爾蒙。如此,學習能不有興趣嗎?興趣是最好的老師!想忘掉都難啊!
于是乎,本瓜類比歸化,将自己遇到過的高頻面試問題運用串聯聯想法進行了“串聯”整理,以期形成系統,與各位同好分享。
上圖!
撰文不易✍ 點贊鼓勵? 您的回報? 我的動力?
串聯一:從輸入URL到頁面加載發生了什麼?
此題是經典中的經典,可挖掘的點非常之多,亦非常之深。
一圖勝萬言
- 原創腦圖,轉載請說明出處
串聯知識點:URL解析、DNS查詢、TCP握手、HTTP請求、浏覽器處理傳回封包、頁面渲染
串聯記憶:共計六步,歸并為一句話來記憶:UDTH,處理傳回加渲染。
“UDTH” 即URL解析、DNS查詢、TCP握手、HTTP請求,
“處理傳回加渲染”,即浏覽器處理傳回封包,和頁面渲染。
同時,本瓜傾情在腦圖上标注了每個步驟可能考察的知識點“關鍵詞”,真的是個個重點,不容錯過!
一、URL 解析
URL(Uniform Resource Locator),統一資源定位符,用于定位網際網路上資源,俗稱網址。
// 示例引自 wikipedia
hierarchical part
┌───────────────────┴─────────────────────┐
authority path
┌───────────────┴───────────────┐┌───┴────┐
abc://username:[email protected]:123/path/data?key=value&key2=value2#fragid1
└┬┘ └───────┬───────┘ └────┬────┘ └┬┘ └─────────┬─────────┘ └──┬──┘
scheme user information host port query fragment
scheme - 定義網際網路服務的類型。常見的協定有 http、https、ftp、file,
其中最常見的類型是 http,而 https 則是進行加密的網絡傳輸。
host - 定義域主機(http 的預設主機是 www)
domain - 定義網際網路域名,比如 baidu.com
port - 定義主機上的端口号(http 的預設端口号是 80)
path - 定義伺服器上的路徑(如果省略,則文檔必須位于網站的根目錄中)。
filename - 定義文檔/資源的名稱
query - 即查詢參數
fragment - 即 # 後的hash值,一般用來定位到某個位置
更多可見:
- URL RFC
- Wikipedia-URI
URL 編碼
一般來說,URL 隻能使用英文字母、阿拉伯數字和某些标點符号,不能使用其他文字和符号。此在 URL RFC 已做硬性規定。
這意味着,如果URL中有漢字,就必須編碼後使用。但是麻煩的是,RFC 1738沒有規定具體的編碼方法,而是交給應用程式(浏覽器)自己決定。這導緻"URL編碼"成為了一個混亂的領域。
阮老師早在 2010 年已解釋了:關于URL編碼- 阮一峰
這裡可直接看結論:浏覽器對 URL 編碼會出現差異進而造成混亂,是以假設我們使用 Javascript 預先對 URL 編碼,然後再向伺服器送出。因為Javascript 的輸出總是一緻的,這樣就保證了伺服器得到的資料是格式統一的。
我們常使用到:encodeURI()、encodeURIComponent();前者對整個 URL 進行 utf-8 編碼,後者是對 URL 部分進行編碼。
本瓜請問:你能清楚的解釋 ASCII、Unicode、UTF-8、GBK 含義和關系嗎?
也許我們并不太了解我們常見、常用的東西。
類型 | 含義 |
ASCII | 8位一個位元組,1個位元組表示一個字元.即: 2 ** 8 = 256,是以ASCII碼最多隻能表示256個字元。 |
Unicode | 俗稱萬國碼,把所有的語言統一到一個編碼裡.解決了ASCII碼的限制以及亂碼的問題。unicode碼一般是用兩個位元組表示一個字元,特别生僻的用四個位元組表示一個字元。 |
UTF-8 | "可變長的編碼方式",如果是英文字元,則采用ASCII編碼,占用一個位元組。如果是常用漢字,就占用三個位元組,如果是生僻的字就占用4~6個位元組。 |
GBK | 國内版本,一個中文字元 == 兩個位元組 英文是一個位元組 |
強緩存、協商緩存
言外之音:本瓜起初是将強緩存、協商緩存放在第三步 “HTTP 請求”中,後來了解到:如果命中了強緩存則可不再走DNS解析這步。遂将其歸到此處。浏覽器強緩存是按照ip還是域名緩存的?
強緩存、協商緩存是必考題。具體流程如下:
- 浏覽器在加載資源時,根據請求頭的 expires和 cache-control判斷是否命中強緩存,是則直接從緩存讀取資源,不會發請求到伺服器。
- 如果沒有命中強緩存,浏覽器一定會發送一個請求到伺服器,通過Etag和Last-Modified-If驗證資源是否命中協商緩存,如果命中,伺服器會将這個請求傳回(304),告訴浏覽器從緩存中讀取資料。
- 如果前面兩者都沒有命中,直接從伺服器加載資源。
expires:HTTP/1.0 時期,根據對比本地時間和伺服器時間來判斷。
cache-control:HTTP/1.1 時期,根據相對時間來判斷,如設定max-age,機關為秒。
【ETag、If-None-Match】成對:Etag 是伺服器傳回給浏覽器的,If-None-Match 是浏覽器請求伺服器的。通過對比二者來判斷,它們記錄的是:檔案生成的唯一辨別。
【Last-Modified,If-Modified-Since】成對:Modified-Since 是伺服器傳回給浏覽器的,If-Modified-Since 是浏覽器請求伺服器的。通過對比二者來判斷,它們記錄的是:最後修改時間。
注:ETag 的優先級比 Last-Modified 更高。大部分 web 伺服器都預設開啟協商緩存,而且是同時啟用【ETag、If-None-Match】和 【Last-Modified,If-Modified-Since】。
綜上,強緩存和協商緩存如果命中,都是從用戶端緩存中加載資源,而不是從伺服器加載資源資料;不同的是:強緩存不會發請求到伺服器,協商緩存會發請求到伺服器進行對比判斷得出是否命中。
- 借一個流程圖,暫未找到真實出處,保留引用說明坑位。
以上還有另一個重點,就是cache-control的值的分類:如“no-cache”、“no-store”、“private”等,需要細扣。此處僅暫列二三、作初步釋義。
- no-cache: 跳過目前的強緩存,發送HTTP請求,即直接進入協商緩存階段。
- no-store:不進行任何形式的緩存。
- private: 這種情況就是隻有浏覽器能緩存了,中間的代理伺服器不能緩存。
更多可見:
- Cache-Control - MDN
- 緩存(二)——浏覽器緩存機制:強緩存、協商緩存 #41
二、DNS查詢
遞歸查詢
DNS 解析 URL(自右向左) 找到對應的 ip
// 例如:查找www.google.com的IP位址過程(真正的網址是www.google.com.):
// 根域名伺服器 -> com頂級域名伺服器 -> google.com域名伺服器 -> www.google.com對應的ip
. -> .com -> google.com. -> www.google.com.
這是一個遞歸查詢的過程。
關于根域名的更多知識,可見 根域名的知識-阮一峰。
DNS 緩存
請記住:有 DNS 的地方,就有 DNS 緩存。
DNS存在着多級緩存,從距離浏覽器的距離排序的話,有以下幾種:
1.浏覽器緩存 2.系統緩存 3.路由器緩存 4.IPS 伺服器緩存 5.根域名伺服器緩存 6.頂級域名伺服器緩存 7.主域名伺服器緩存。
檢視緩存:
- 浏覽器檢視 DNS 緩存:chrome://net-internals/#dns
- win10 系統檢視 DNS 緩存:win+R => cmd => ipconfig /displaydns
DNS 負載均衡
DNS負載均衡技術的實作原理是在DNS伺服器中為同一個主機名配置多個IP位址,在應答DNS查詢時,DNS伺服器對每個查詢将以DNS檔案中主機記錄的IP位址按順序傳回不同的解析結果,将用戶端的通路引導到不同的機器上去,使得不同的用戶端通路不同的伺服器,進而達到負載均衡的目的。—— 百科
三、TCP握手
DNS解析傳回域名的IP之後,接下來就是浏覽器要和該IP建立TCP連接配接了。
言外之音:TCP 的相關知識在大學基礎課程《計算機網絡》都有,本瓜内心苦:出來混的遲早是要還的......
TCP/IP 模型:鍊路層-網絡層-傳輸層-應用層。
與之對應,OSI(開放式系統互聯模型)也不能忘。通常認為 OSI 模型的最上面三層(應用層、表示層和會話層)對應 TCP/IP 模型中的應用層。wiki
在 TCP/IP 模型中,像常用的 HTTP/HTTPS/SSH 等協定都在應用層上。
三次握手、四次揮手
所謂三次握手(Three-way Handshake),是指建立一個 TCP 連接配接時,需要用戶端和伺服器總共發送3個包。
三次握手就跟早期打電話時的情況一樣:1、A:聽得到嗎?2、B:聽得到,你呢?3、A:我也聽到了。然後才開始真正對話。
1. 第一次握手(SYN=1, seq=x):
用戶端發送一個 TCP 的 SYN 标志位置1的包,指明用戶端打算連接配接的伺服器的端口,以及初始序号 X,儲存在標頭的序列号(Sequence Number)字段裡。
發送完畢後,用戶端進入 SYN_SEND 狀态。
2. 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):
伺服器發回确認包(ACK)應答。即 SYN 标志位和 ACK 标志位均為1。伺服器端選擇自己 ISN 序列号,放到 Seq 域裡,同時将确認序号(Acknowledgement Number)設定為客戶的 ISN 加1,即X+1。 發送完畢後,伺服器端進入 SYN_RCVD 狀态。
3. 第三次握手(ACK=1,ACKnum=y+1)
用戶端再次發送确認包(ACK),SYN 标志位為0,ACK 标志位為1,并且把伺服器發來 ACK 的序号字段+1,放在确定字段中發送給對方,并且在資料段放寫ISN的+1
發送完畢後,用戶端進入 ESTABLISHED 狀态,當伺服器端接收到這個包時,也進入 ESTABLISHED 狀态,TCP 握手結束。
所謂四次揮手(Four-way handshake),是指 TCP 的連接配接的拆除需要發送四個包。
四次揮手像老師拖堂的場景:1、學生說:老師,下課了。2、老師:好,我知道了,我說完這點。3、老師:好了,說完了,下課吧。4、學生:謝謝老師,老師再見。
1. 第一次揮手(FIN=1,seq=x)
假設用戶端想要關閉連接配接,用戶端發送一個 FIN 标志位置為1的包,表示自己已經沒有資料可以發送了,但是仍然可以接受資料。
發送完畢後,用戶端進入 FIN_WAIT_1 狀态。
2. 第二次揮手(ACK=1,ACKnum=x+1)
伺服器端确認用戶端的 FIN 包,發送一個确認包,表明自己接受到了用戶端關閉連接配接的請求,但還沒有準備好關閉連接配接。
發送完畢後,伺服器端進入 CLOSE_WAIT 狀态,用戶端接收到這個确認包之後,進入 FIN_WAIT_2 狀态,等待伺服器端關閉連接配接。
3. 第三次揮手(FIN=1,seq=y)
伺服器端準備好關閉連接配接時,向用戶端發送結束連接配接請求,FIN 置為1。
發送完畢後,伺服器端進入 LAST_ACK 狀态,等待來自用戶端的最後一個ACK。
4. 第四次揮手(ACK=1,ACKnum=y+1)
用戶端接收到來自伺服器端的關閉請求,發送一個确認包,并進入 TIME_WAIT狀态,等待可能出現的要求重傳的 ACK 包。
伺服器端接收到這個确認包之後,關閉連接配接,進入 CLOSED 狀态。
用戶端等待了某個固定時間(兩個最大段生命周期,2MSL,2 Maximum Segment Lifetime)之後,沒有收到伺服器端的 ACK ,認為伺服器端已經正常關閉連接配接,于是自己也關閉連接配接,進入 CLOSED 狀态。
你若問我:三次握手、四次揮手的詳細内容太難記了,還記不記?本瓜答:進大廠是必要的。
流量控制(滑動視窗)
為了增加網絡的吞吐量,想将資料包一起發送過去,實作“流量控制”,這時候便産生了“滑動視窗”這種協定。
滑動視窗允許發送方在收到接收方的确認之前發送多個資料段。視窗大小決定了在收到目的地确認之前,一次可以傳送的資料段的最大數目。視窗大小越大,主機一次可以傳輸的資料段就越多。當主機傳輸視窗大小數目的資料段後,就必須等收到确認,才可以再傳下面的資料段。
視窗的大小在通信雙方連接配接期間是可變的,通信雙方可以通過協商動态地修改視窗大小。改變視窗大小的唯一根據,就是接收端緩沖區的大小。
擁塞控制
需求>供給 就會産生擁塞
通過“擁塞視窗”、“慢啟動”、“快速重傳”、“快速恢複”動态解決。
TCP 使用多種擁塞控制政策來避免雪崩式擁塞。TCP會為每條連接配接維護一個“擁塞視窗”來限制可能在端對端間傳輸的未确認分組總數量。這類似 TCP 流量控制機制中使用的滑動視窗。TCP在一個連接配接初始化或逾時後使用一種“慢啟動”機制來增加擁塞視窗的大小。它的起始值一般為最大分段大小(Maximum segment size,MSS)的兩倍,雖然名為“慢啟動”,初始值也相當低,但其增長極快:當每個分段得到确認時,擁塞視窗會增加一個MSS,使得在每次往返時間(round-trip time,RTT)内擁塞視窗能高效地雙倍增長。—— TCP擁塞控制-wikipedia
滑動視窗(流量控制)和擁塞控制裡的原理性的東西太多,本瓜表示無力,暫時要求盡力去了解去記憶。
四、HTTP請求
HTTP是網際網路的資料通信的基礎 —— 維基百科
HTTP請求封包是由三部分組成: 請求行, 請求報頭和請求正文。
HTTP響應封包也是由三部分組成: 狀态碼, 響應報頭和響應封包。
HTTP、HTTPS
HTTP 和 HTTPS 的差別?
HTTP封包是包裹在TCP封包中發送的,伺服器端收到TCP封包時會解包提取出HTTP封包。但是這個過程中存在一定的風險,HTTP封包是明文,如果中間被截取的話會存在一些資訊洩露的風險。
如果在進入TCP封包之前對HTTP做一次加密就可以解決這個問題了。HTTPS協定的本質就是HTTP + SSL(or TLS)。在HTTP封包進入TCP封包之前,先使用SSL對HTTP封包進行加密。從網絡的層級結構看它位于HTTP協定與TCP協定之間。
HTTP2
http2 是完全相容 http/1.x 的,并在此基礎上添加了 4 個主要新特性:
- 二進制分幀:http/1.x 是一個文本協定,而 http2 是一個二進制協定。
- 頭部壓縮:http/1.x 中請求頭基本不變,http2 中提出了一個 HPACK 的壓縮方式,用于減少 http header 在每次請求中消耗的流量。
- 服務端推送:服務端主動向用戶端推送資料。
- 多路複用:http/1.x,每個 http 請求都會建立一個 TCP 連接配接;http2,所有的請求都會共用一個TCP連接配接。
更多了解,可見 HTTP2 詳解
GET、POST
直覺差別:
- GET 用來擷取資料,POST 用來送出資料。
- GET 參數有長度限制(受限于url長度,具體的數值取決于浏覽器和伺服器的限制,最長2M),而 POST 無限制。
- GET 請求的資料會附加在 URL 上,以"?"分割,多個參數用"&"連接配接,而 POST 請求會把請求的資料放在 HTTP 請求體中。都可被抓包。
- GET 請求會儲存在浏覽器曆史記錄中,還可能儲存在 WEB 伺服器的日志中。
隐藏差別(存在浏覽器差異):
- GET 産生一個 TCP 資料包;POST 産生兩個 TCP 資料包。
更多了解:
RESTful API 設計指南 - 阮一峰
Keep-Alive
我們都知道使用 Keep-Alive 是為了避免重建立立連接配接。
目前大部分浏覽器都是用 http1.1 協定,預設都會發起 Keep-Alive 的連接配接請求了,是以是否能完成一個完整的 Keep-Alive 連接配接就看伺服器設定情況。
HTTP 長連接配接不可能一直保持,它有兩個參數,例如 Keep-Alive: timeout=5, max=100,表示這個TCP通道可以保持5秒,max=100,表示這個長連接配接最多接收100次請求就斷開。
Keep-Alive 模式發送資料 HTTP 伺服器不會自動斷開連接配接,所有不能使用傳回EOF(-1)來判斷。
基于此,抛問題:當 HTTP 采用 keepalive 模式,當用戶端向伺服器發生請求之後,用戶端如何判斷伺服器的資料已經發生完成?
- 使用消息首部字段 Conent-Length:Conent-Length表示實體内容長度,用戶端可以根據這個值來判斷資料是否接收完成。
- 使用消息首部字段 Transfer-Encoding:如果是動态頁面,伺服器不可能預先知道内容大小,這時就可以使用Transfer-Encoding:chunk 模式來傳輸資料了。即如果要一邊産生資料,一邊發給用戶端,伺服器就需要使用"Transfer-Encoding: chunked"這樣的方式來代替Content-Length。chunked 編碼的資料在最後有一個空 chunked 塊,表明本次傳輸資料結束
本瓜之前面試騰訊 PCG 就被問到 Transfer-Encoding:chunk 這個,請大家格外注意。
參考:
- Keep-Alive - MDN
- HTTP長連接配接和短連接配接
五、浏覽器處理傳回封包
狀态碼
1xx:訓示資訊–表示請求已接收,繼續處理。
2xx:成功–表示請求已被成功接收、了解、接受。
3xx:重定向–要完成請求必須進行更進一步的操作。
4xx:用戶端錯誤–請求有文法錯誤或請求無法實作。
5xx:伺服器端錯誤–伺服器未能實作合法的請求。
平時遇到比較常見的狀态碼有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500。
切分傳回頭和傳回體(騰訊 PCG 考點)
這裡的考點其實和 http keep-alive 中的問題重合,但是還是想着重強調,因為本瓜掉過這個坑,再三點出,以示後人。
最終歸為這個問題:Detect end of HTTP request body - stackoverflow
解答:
1. If the client sends a message with Transfer-Encoding: Chunked, you will need to parse the somewhat complicated chunked transfer encoding syntax. You don not really have much choice in the matter -- if the client is sending in this format, you have to receive it. When the client is using this approach, you can detect the end of the body by a chunk with a length of 0.
2. If the client instead sends a Content-Length, you must use that.
即:如何擷取 HTTP 傳回體?
- 先把 header 直到 \r\n\r\n(兩個換行)整個讀取,即整個請求頭;
- 如果傳回 Transfer-Encoding: Chunked,則讀取,直到遇到空 chunked 塊,則結束。
- 如果傳回 Content-Length,則讀從請求頭的末尾開始計算 Content-Length 長度的位元組。
- 其他情況,等待傳回。
本地資料存儲
- cookie:4K,可以手動設定失效期。
- localStorage:5M,除非手動清除,否則一直存在。
- sessionStorage:5M,不可以跨标簽通路,頁面關閉就清理。
- indexedDB:浏覽器端資料庫,無限容量,除非手動清除,否則一直存在。
- Web SQL:關系資料庫,通過SQL語句通路(已經被抛棄)。
浏覽器緩存位置
按優先級從高到低:
- Service Worker:本質是一個web worker,是獨立于網頁運作的腳本。
- Memory Cache:Memory Cache指的是記憶體緩存,從效率上講它是最快的。
- Disk Cache:Disk Cache就是存儲在磁盤中的緩存,從存取效率上講是比記憶體緩存慢的,但是他的優勢在于存儲容量和存儲時長。
- Push Cache:即推送緩存,是 HTTP/2 的内容。
更多:
- service worker 靜态資源離線緩存實踐
- HTTP/2 push is tougher than I thought
離線緩存:
<html lang="en" manifest="offline.appcache">
HTML5-離線緩存(Application Cache)
注:“浏覽器緩存位置”和“離線緩存”了解相關,有個概念/印象即可。
六、頁面渲染
CssTree+DomTree
本瓜知道你知道過程是這樣的:
dom tree + css tree = render tree => layout =>painting?
但是你真的吃透了嗎?
HTML → DOM樹 轉化過程:
- 解碼:浏覽器從磁盤或網絡讀取HTML的原始位元組,然後根據指定的檔案編碼格式(例如 UTF-8)将其轉換為相應字元
- 令牌化:浏覽器把字元轉化成W3C HTML5 标準指定的各種确切的令牌,比如""、""以及其他在尖括号内的字元串。每個令牌都有特殊的含義以及它自己的一套規則
- 詞法分析:生成的令牌轉化為對象,這個對象定義了它們的屬性及規則
- DOM樹建構:最後,由于HTML标記定義了不同标簽之間的關系(某些标簽嵌套在其他标簽中),建立的對象在樹狀的資料結構中互相連結,樹狀資料結構也捕獲了原始标簽定義的父子關系:HTML對象是body對象的父對象,body是p對象的父對象等等
CSS → CSSOM樹 轉化過程類同以上
CSSOM隻輸出包含有樣式的節點,最終輸出為:
Render Tree (生成渲染樹,計算可見節點和樣式)
- 不包括Header 、 script 、meta 等不可見的節點
- 某些通過 CSS 隐藏的節點在渲染樹中也會被忽略,比如應用了 display:none 規則的節點,而visibility: hidden隻是視覺不可見,仍占據空間,不會被忽略。
layout:依照盒子模型,計算出每個節點在螢幕中的位置及尺寸
painting:按照算出來的規則,通過顯示卡,把内容畫到螢幕上。
回流、重繪
回流:
當可見節點位置及尺寸發生變化時會發生回流
重繪:
改變某個元素的背景色、文字顔色、邊框顔色等等不影響它周圍或内部布局的屬性時,螢幕的一部分要重畫,但是元素的幾何尺寸沒有變。
這裡本瓜再抛兩個問題。
Q1:浏覽器在什麼時候向伺服器發送擷取css、js外部檔案的請求?
A1:解析DOM時碰到外部連結,如果還有connection,則立刻觸發下載下傳請求。
Q2:CSSOM DOM JavaScript 三者阻塞關系?
A2:CSSOM DOM互不影響,JavaScript會阻塞DOM樹的建構但JS前的HTML可以正常解析成DOM樹,CSSOM的建構會阻塞JavaScript的執行。對此句存疑?
- css 加載的阻塞情況:
- css加載不會阻塞DOM樹的解析
// DOM解析和CSS解析是兩個并行的程序,是以這也解釋了為什麼CSS加載不會阻塞DOM的解析。
- css加載會阻塞DOM樹的渲染
// 由于Render Tree是依賴于DOM Tree和CSSOM Tree的,是以他必須等待到CSSOM Tree建構完成,也就是CSS資源加載完成(或者CSS資源加載失敗)後,才能開始渲染。是以,CSS加載是會阻塞Dom的渲染的。
- css加載會阻塞後面js語句的執行
// 由于js可能會操作之前的Dom節點和css樣式,是以浏覽器會維持html中css和js的順序。是以,樣式表會在後面的js執行前先加載執行完畢。是以css會阻塞後面js的執行。
參考閱讀:
- css加載會造成阻塞嗎?
- 浏覽器頁面渲染流程梳理
綜合補充
web 性能優化
- 雅虎35條軍規
串聯二:老生常談,請你談一下閉包?
假若你認為此題簡單,一兩句話就能說完?那當真浮于表面。此題實則一兩天都說不完!它可以牽扯出 js 原理的大部分知識。是真正意義上的“母題”。
一圖勝萬言
- 原創腦圖,轉載請說明出處
串聯知識點:閉包、作用域、原型鍊、js繼承。
串聯記憶:此題并非像上文題“從輸入URL到頁面加載發生了什麼?”,後者“串聯點”是按解答步驟來遞進的。而這裡的“串聯點”,更多是你中有我,我中有你,前後互相補充,互相完善。當你領略完的時候,一定會有一種“萬物歸宗”的感覺。
歸并為一五言詩來記憶:
閉包作用域
原型多考慮
繼承八大法
基礎好好叙
一、閉包
閉包含義
一言以蔽之。
在一個函數内有另外一個函數可以通路它的内部變量,并且另外一個函數在外部被調用,這樣的詞法環境叫閉包。
- 作用:
- 讀取函數内部的變量;(私有變量、不污染全局)
- 讓變量始終儲存在記憶體中。
閉包應用
- 最經典試題
for(var i = 0; i < 5; i++){
(function(j){
setTimeout(function(){
console.log(j);
},1000);
})(i);
}
console.log(i);
垃圾回收機制
- js 垃圾回收機制:标記清除和引用計數。
标記清除簡單講就是變量存儲在記憶體中,當變量進入執行環境的時候,垃圾回收器會給它加上标記,這個變量離開執行環境,将其标記為“清除”,不可追蹤,不被其他對象引用,或者是兩個對象互相引用,不被第三個對象引用,然後由垃圾回收器收回,釋放記憶體空間。
防抖、節流函數
- 防抖
function debounce(fn, delay) {
var timer; // 維護一個 timer
return function () {
var _this = this; // 取debounce執行作用域的this
var args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
fn.apply(_this, args); // 用apply指向調用debounce的對象,相當于_this.fn(args);
}, delay);
};
}
- 節流
function throttle(fn, delay) {
var timer;
return function () {
var _this = this;
var args = arguments;
if (timer) {
return;
}
timer = setTimeout(function () {
fn.apply(_this, args);
timer = null; // 在delay後執行完fn之後清空timer,此時timer為假,throttle觸發可以進入計時器
}, delay)
}
}
二、作用域
全局作用域
- 直接編寫在script标簽中的JS代碼,都在全局作用域;
- 全局作用域在頁面打開時建立,在頁面關閉時銷毀;
- 在全局作用域中有一個全局對象window,它代表的是一個浏覽器的視窗,它由浏覽器建立我們可以直接使用;
- 全局作用域中,建立變量都會作為window對象的屬性儲存;
- 建立的函數都會作為window對象的方法儲存;
- 全局作用域中的變量都是全局變量,在頁面的任何部分都可以通路的到;
我們可以在控制台直接列印試試看,正如以上所說:
函數作用域(局部作用域)
- 變量在函數内聲明,變量屬于局部作用域。
- 局部變量:隻能在函數内部通路。
- 局部變量隻作用于函數内,是以不同的函數可以使用相同名稱的變量。
- 局部變量在函數開始執行時建立,函數執行完後局部變量會自動銷毀。
塊級作用域
塊級作用域 : 塊級作用域指的就是使用 if () { }; while ( ) { } ......這些語句所形成的語句塊 , 并且其中變量必須使用 let 或 const 聲明,保證了外部不可以通路語句塊中的變量。
注:函數作用域和塊級作用域沒有直接關系。
- const、let、var 差別
- const 聲明則不能改變,塊級作用域,不允許變量提升。
- let 塊級作用域,不允許變量提升。
- var 非塊級作用域,允許變量提升。
作用域鍊
出現函數嵌套函數,則就會出現作用域鍊 scope chain。
- 周遊嵌套作用域鍊的規則很簡單:引擎從目前的執行作用域開始查找變量,如果找不到, 就向上一級繼續查找。當抵達最外層的全局作用域時,無論找到還是沒找到,查找過程都會停止。
- 局部作用域(如函數作用域)可以通路到全局作用域中的變量和方法,而全局作用域不能通路局部作用域的變量和方法。
用作用域鍊來解釋閉包:
function outter() {
var private= "I am private";
function show() {
console.log(private);
}
// [[scope]]已經确定:[outter上下文的變量對象,全局上下文變量對象]
return show;
}
var ref = outter();
console.log(private); // outter執行完以後,private不會被銷毀,并且隻能被show方法所通路,
//直接通路它會出現報錯:private is not defined
ref(); // 列印I am private
其實,我們要明白的是函數的聲明和調用是分開的,如果不搞清楚這一點,很多基礎面試題就容易出錯。
- 深究:
JavaScript深入之詞法作用域和動态作用域 #3
變量生命周期
一個變量的聲明意味着就是我們在記憶體當中申請了一個空間用來存儲。這個記憶體也就是我們電腦的運作記憶體,如果我們一直的聲明變量,不釋放的話。會占用很大的記憶體。
在 c/c++ 當中是需要程式員在合适的地方手動的去釋放變量記憶體,而 javascript 和 java 擁有垃圾回收機制(咱們在上文已說明)。
js 變量分為兩種類型:全局變量和局部變量
- 全局變量的生命周期:從程式開始執行建立,到整個頁面關閉時,變量收回。
- 局部變量的生命周期:從函數開始調用開始,一直到函數調用結束。
但有的時候我們需要讓局部變量的生命周期長一點,此時就用到了閉包。
三、原型鍊
執行個體與原型
一個原型對象的隐形屬性指向構造它的構造函數的顯示屬性。
當一個對象去查找它的屬性,找不到就去找他的構造函數的屬性,一直向上找,直到找到 Object()。
判斷資料類型
- typeof
- instanceof
- constructor
- Object.prototype.toString.call()
new 一個對象
步驟:
- 建立一個新對象
- 将構造函數的作用域賦給新對象(是以this指向了這個新對象)
- 執行構造函數中的代碼(為這個新對象添加屬性)
- 傳回新對象
this 指向
this 指向 5 大規則:
- 如果 new 關鍵詞出現在被調用函數的前面,那麼JavaScript引擎會建立一個新的對象,被調用函數中的this指向的就是這個新建立的函數。
- 如果通過apply、call或者bind的方式觸發函數,那麼函數中的this指向傳入函數的第一個參數。
- 如果一個函數是某個對象的方法,并且對象使用句點符号觸發函數,那麼this指向的就是該函數作為那個對象的屬性的對象,也就是,this指向句點左邊的對象
- 如果一個函數作為FFI被調用,意味着這個函數不符合以上任意一種調用方式,this指向全局對象,在浏覽器中,即是window。
- 如果出現上面對條規則的累加情況,則優先級自1至4遞減,this的指向按照優先級最高的規則判斷。
參考:this指向記憶5大原則
- 箭頭函數中的 this 指向:箭頭函數中的this是在定義函數的時候綁定,而不是在執行函數的時候綁定。
更多:JS中的箭頭函數與this
bind、call、apply
- call
call()方法接收的第一個參數和apply()方法接收的一樣,變化的是其餘的參數直接傳遞給函數。換句話說,在使用call()方法時,傳遞給函數的參數必須逐個列舉出來。
function sum(num1 , num2){
return num1 + num2;
}
function callSum(num1 , num2){
return sum.call(this , sum1 , sum2);
}
console.log(callSum(10 , 10)); // 20
- apply
apply()方法接收兩個參數:一個是在其中運作函數的作用域,另一個是參數數組,這裡的參數數組可以是Array的執行個體,也可以是arguments對象(類數組對象)。
function sum(num1 , num2){
return num1 + num2;
}
function callSum1(num1,num2){
return sum.apply(this,arguments); // 傳入arguments類數組對象
}
function callSum2(num1,num2){
return sum.apply(this,[num1 , num2]); // 傳入數組
}
console.log(callSum1(10 , 10)); // 20
console.log(callSum2(10 , 10)); // 20
call和apply的差別在于二者傳參的時候,前者是一個一個的傳,後者是傳數組或類數組arguments
- bind
bind()方法建立一個新的函數, 當被調用時,将其this關鍵字設定為提供的值,在調用新函數時,在任何提供之前提供一個給定的參數序列。
手寫深淺拷貝
淺:
function clone(target) {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = target[key];
}
return cloneTarget;
};
深(遞歸):
function clone(target) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
了解更多,推薦閱讀:如何寫出一個驚豔面試官的深拷貝?
四、js 繼承
八種繼承方式,詳細請看此篇:JavaScript常用八種繼承方案。
本瓜不做贅述,可列二三關鍵必記。
串聯三:請你談談 Vue 原理?
本瓜不裝了,攤牌了。其實本文的目錄結構編寫時間線在 《 Vue(v2.6.11)萬行源碼生啃,就硬剛!》 這篇文章之前。當時就是因為似懂非懂,才定下心來“生啃源碼”。現在源碼看完了,體會的确又不一樣了。但由于細節太多,篇幅受限。此處也僅列架構、點出要點、注釋連結,以便記憶。
一圖勝萬言
- 原創腦圖,轉載請說明出處
串聯知識點:Vue初始化和生命周期、虛拟DOM、響應式原理、元件編譯、Vue常用補充、Vue全家桶。
串聯記憶:編一順口溜,見笑。
V U E 真容易
初始化 有生命
虛拟 dom 好給力
響應式 看仔細
元件化 大家利
全家桶 笑嘻嘻
會打包 掙一億
- 邀大家來改編
一、init&render
挂載和初始化
new Vue()發生了什麼?
Vue 實際上是一個類,類在 Javascript 中是用 Function 來實作的。Vue 隻能通過 new 關鍵字初始化,然後會調用 this._init 方法。
初始化主要實作:合并配置(mergeOptions),初始化生命周期(initLifecycle),初始化事件中心(initEvents),初始化渲染(initRender),初始化 data、props、computed、watcher 等等。
流程圖參考如下:
- 此圖在元件編譯環節少了 optimize ,可能由于版本差異。 Vue2.4.4 源碼
- 借圖,未找到真實出處,保留引用說明坑位。
執行個體生命周期
生命周期圖示,還是得看官網文檔。還記得這句話嗎?
下圖展示了執行個體的生命周期。你不需要立馬弄明白所有的東西,不過随着你的不斷學習和使用,它的參考價值會越來越高。
注釋版:
推薦:源碼解讀
要點注釋:
- beforeCreate 和 created 函數都是在執行個體化 Vue 的階段,在 _init 方法中執行的。從源碼中可以看到 beforeCreate 和 created 的鈎子調用是在 initState 的前後,initState 的作用是初始化 props、data、methods、watch、computed 等屬性。那麼顯然 beforeCreate 的鈎子函數中就不能擷取到 props、data 中定義的值,也不能調用 methods 中定義的函數。而 created 鈎子函數可以。
Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate') // beforeCreate 鈎子
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // created 鈎子
// ...
}
- 在執行 vm._render() 函數渲染 VNode 之前,執行了 beforeMount 鈎子函數,在執行完 vm._update() 把 VNode patch 到真實 DOM 後,執行 mounted 鈎子。(此點重要)
- beforeUpdate 的執行時機是在渲染 Watcher 的 before 函數中調用。update 的執行時機是在 flushSchedulerQueue 函數調用的時候。
- beforeDestroy 和 destroyed 鈎子函數的執行時機在元件銷毀的階段。
- activated 和 deactivated 鈎子函數是專門為 keep-alive 元件定制的鈎子。
重點說明:
- 在 Vue2 中,所有 Vue 的元件的渲染最終都需要 render 方法,無論我們是用單檔案 .vue 方式開發元件,還是寫了 el 或者 template 屬性,最終都會轉換成 render 方法,用來把執行個體渲染成一個虛拟 Node(Virtual DOM)。
二、虛拟DOM
Vue 2.0 相比 Vue 1.0 最大的更新就是利用了 Virtual DOM。
vdom
vdom 其實就是一顆 js 對象樹,最少包含标簽名( tag)、屬性(attrs)和子元素對象( children)三個屬性。原本對 DOM 節點的操作(浏覽器将 DOM 設計的非常複雜)轉成了對 js 對象的操作,加快處理速度、提升性能。
VNode 的建立是由 createElement 方法實作的。
欲知原理,推薦閱讀:snabbdom
diff & patch
在實際代碼中,會對新舊兩棵樹進行一個深度的周遊,每個節點都會有一個标記。每周遊到一個節點就把該節點和新的樹進行對比,如果有差異就記錄到一個對象中。即用 diff 算法比較差異,然後調用 patch 應用到真實 DOM 上去。patch 的過程即一個打更新檔的過程。
圖檔來源
diff 算法是一個交叉對比的過程,大緻可以簡要概括為:頭頭比較、尾尾比較、頭尾比較、尾頭比較。
入門級别 diff 詳情推薦看此篇:LINK
圖檔來源
注意:render函數傳回的是 vdom,patch生成的才是真實DOM。
三、響應式原理
官方生圖,高屋建瓴。
本瓜曾在《簡析 vue 的雙向綁定原理》這篇文章寫過,如今看又是一番心情。
當你把一個普通的 JavaScript 對象傳入 Vue 執行個體作為 data 選項,Vue 将周遊此對象所有的 property,并使用 Object.defineProperty 把這些 property 全部轉為 getter/setter。Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是 Vue 不支援 IE8 以及更低版本浏覽器的原因。
釋出訂閱者模式(位元組考題)
class emit {
}
cosnt eeee = new emit()
eeee.on('aa' , function() { console.log(1)})
eeee.on('aa' , function() {console.log(2)})
eeee.emit('aa')
//class emit{}
// 要求手寫釋出者-訂閱模式
class Subject{
constructor () {
this.observers =[]
}
add (observer) {
this.observers.push(observer)
}
notify () {
this.observers.map((item, index) => {
item.update()
})
}
}
class Observer {
constructor (name) {
this.name = name
}
update () {
console.log("I`m " + this.name)
}
}
var sub = new Subject()
var obs1 = new Observer("obs1")
var obs2 = new Observer("obs2")
sub.add(obs1)
sub.add(obs2)
sub.notify() // I`m obs1 I`m obs2
除了“釋出者訂閱模式”,你還知道哪些 js 設計模式?這裡留個坑,以後再補,東西太多了......
Observe
Observe 的功能就是用來監測資料的變化。它的作用是給對象的屬性添加 getter 和 setter,用于依賴收集和派發更新:
這裡貼一下源碼片段,咱可以感受下:
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
...
}
walk (obj: Object) {
...
}
observeArray (items: Array<any>) {
...
}
}
有沒有覺得和上面提到的“釋出訂閱者模式”中的相似。Observer 首先執行個體化 Dep 對象,接着通過執行 def 函數把自身執行個體添加到資料對象 value 的 ob 屬性上。(def 函數是一個簡單的對 Object.defineProperty 的封裝)。
Dep
Dep 是整個 getter 依賴收集的核心。
由于 Watcher 是有多個的,是以需要用 Dep 收集變化之後集中管理,再通知到對應的 Watcher。由此也好了解 Dep 是依賴于 Watcher 的。
貼源碼片段,感受一下:
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
...
}
Watcher
貼源碼片段,感受一二:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
...
// parse expression for getter
...
}
get () {
pushTarget(this)
...
}
addDep (dep: Dep) {
const id = dep.id
...
}
cleanupDeps () {
...
}
// ...
}
Watcher 會通知視圖的更新 re-render。
常見視圖更新場景:
- 資料變 → 使用資料的視圖變(對應:負責敦促視圖更新的render-watcher)
- 資料變 → 使用資料的計算屬性變 → 使用計算屬性的視圖變(對應:執行敦促計算屬性更新的computed-watcher)
- 資料變 → 開發者主動注冊的watch回調函數執行(對應:使用者注冊的普通watcher(watch-api或watch屬性))
四、元件編譯
元件的思想也是 Vue 核心,将元件編譯為 vdom ,則也是一重難點!
你可以發現在 Vue 這一節有很多引用的圖,其實它們有的相似,更多的是側重點不同,建議都可按照流程圖了解了解,做到融會貫通。
元件
官方示例:
// 定義一個名為 button-counter 的新元件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
<div id="components-demo">
<button-counter></button-counter>
</div>
new Vue({ el: '#components-demo' })
元件還涉及:元件之間的通信、插槽、動态元件等内容。後續再表(OS: 這是自己給自己挖了個多大的坑)。
parse
編譯過程首先就是對模闆做解析,生成 AST,它是一種抽象文法樹,是對源代碼的抽象文法結構的樹狀表現形式。在很多編譯技術中,如 babel 編譯 ES6 的代碼都會先生成 AST。這個過程會用到大量的正規表達式對字元串解析,源碼很難讀。
但是我們需要知道的是 start、end、comment、chars 四大函數。
對于普通标簽的處理流程大緻:
- 識别開始标簽,生成比對結構match。
- 處理attrs,将數組處理成 {name:'xxx',value:'xxx'}
- 生成astElement,處理for,if和once的标簽。
- 識别結束标簽,将沒有閉合标簽的元素一起處理。
- 建立父子關系,最後再對astElement做所有跟Vue 屬性相關對處理。slot、component等等。
參考閱讀:Vue parse之 從template到astElement 源碼詳解
optimize
當我們的模闆 template 經過 parse 過程後,會輸出生成 AST 樹,那麼接下來我們需要對這顆樹做優化 —— optimize。
optimize 中最重要的是标記靜态根(markStaticRoots )、靜态節點(markStatic )。如果是靜态節點則它們生成的DOM永遠不需要改變,這讓模闆的更新更搞笑(不變的節點不用更新)。
問題:為什麼子節點的元素類型是靜态文本類型,就會給 optimize 過程加大成本呢?
首先來分析一下,之是以在 optimize 過程中做這個靜态根節點的優化,目的是什麼,成本是什麼?
目的:在 patch 過程中,減少不必要的比對過程,加速更新。
成本:a. 需要維護靜态模闆的存儲對象。b. 多層render函數調用。
推薦閱讀
codegen
編譯的最後一步就是把優化後的 AST 樹轉換成可執行的代碼,即在 codegen 環節。
主要步驟(調用函數):
- generate
- genIf
- genFor
- genData & genChildren
此節考的不多,僅做了解。了解更多,得看源碼。
五、常用補充
keep-alive(常考)
keepalive 是 Vue 内置的一個元件,可以使被包含的元件保留狀态,或避免重新渲染 。也就是所謂的元件緩存。
v-if、v-show、v-for
三個高頻子問題:
- v-if、v-show 差別?
答:v-if 相當于 display; v-show 相當于 visibility; 前者會控制是否建立,後者僅控制是否隐藏顯示。
- v-if、v-for 為什麼不能放一起用?
答:因為 v-for 優先級比 v-if 高,是以在每次重新渲染的時候會先周遊整個清單,再進行 if 判斷是否展示,消耗性能。
- v-for 中能用 index 作 key 嗎?
答:key 是 diff 算法中用來對比的,用 index 作為 key 并未唯一識别,當插入元素時,key 也會變化。index 作為 key,隻适用于不依賴子元件狀态或臨時 DOM 狀态 (例如:表單輸入值) 的清單渲染輸出(官網說明)。
本瓜這裡不做細答,想了解更多請自行解決。
自定義指令
Vue 中的混入(Minxin)、自定義指令(directive)、過濾器(filter)有共通之處,在注冊的時候,需要平衡局部注冊和全局注冊的優劣。
transition
本瓜讀源碼的過程中,發現源碼中有較大的篇幅在描述關于transition。在官方文檔中,transition 也是作為獨立的重要一節來說明。進入/離開 & 清單過渡,Vue 動畫&過渡,是容易忽視的點。
六、全家桶
自從用上了 Vue 全家桶,腿也不疼了,腰也不酸了。咦,一口氣寫五個頁面,媽媽再也不用擔心我的學習了。(好像有點串廣告了......)
Vue-Router
官方的路由管理器
分為兩種模式:hash 和 history
- 預設 hash 模式,通過加錨點的方式
- history 利用 history.pushState API實作
原生: HTML5引入了 history.pushState() 和 history.replaceState() 方法,它們分别可以添加和修改曆史記錄條目。這些方法通常與window.onpopstate 配合使用。詳情連結
Vuex
官方狀态管理
由以下幾部分核心組成:
- state:資料狀态;
- mutations:更改狀态(計算狀态);
- getters:将state中的某個狀态進行過濾然後擷取新的狀态;
- actions:執行多個mutation,它可以進行異步操作(async );
- modules:把狀态和管理規則分類來裝,讓目錄結構更清晰;
VueCLI
官方腳手架
- VueCLI4中很重要的是 vue.config.js 這個檔案的配置。
VuePress
靜态網站生成器
- 采用 Vue + webpack,可以在 Markdown 中使用 Vue 元件,頁面簡潔大方,與 Vue 官網風格統一。
NuxtJS
服務端渲染
七、webpack
隻要你做前端有兩年經驗左右,那一樣就得要求自己掌握一款打包工具了。
webpack 原理
官方解釋:
webpack 是一個現代 JavaScript 應用程式的靜态子產品打包工具。當 webpack 處理應用程式時,它會在内部建構一個 依賴圖(dependency graph),此依賴圖會映射項目所需的每個子產品,并生成一個或多個_bundle_。
核心概念:
- Entry:入口,Webpack 執行建構的第一步将從 Entry 開始,可抽象成輸入。
- Module:子產品,在 Webpack 裡一切皆子產品,一個子產品對應着一個檔案。Webpack 會從配置的 Entry 開始遞歸找出所有依賴的子產品。
- Chunk:代碼塊,一個 Chunk 由多個子產品組合而成,用于代碼合并與分割。
- Loader:子產品轉換器,用于把子產品原内容按照需求轉換成新内容。
- Plugin:擴充插件,在 Webpack 建構流程中的特定時機會廣播出對應的事件,插件可以監聽這些事件的發生,在特定時機做對應的事情。
功能:
代碼轉換、檔案優化、代碼分割、子產品合并、自動重新整理、代碼校驗、自動釋出。
優化打包速度
- 搭載 webpack-parallel-uglify-plugin 插件,加速“壓縮JS=>編譯成 AST=>還原JS”的過程。
- 使用 HappyPack 提升 loader 解析速度。
- 使用 DLLPlugin 和 DLLReferencePlugin 插件,提前打包。
- tree-shaking 用來消除無用子產品。
AMD、CMD
前端子產品化有四種規範:CommonJS、AMD、CMD、ES6。
- AMD(異步子產品定義)
- CMD(通用子產品定義)
AMD(異步子產品定義) | CMD(通用子產品定義) |
速度快 | 性能較差 |
會浪費資源 | 隻有真正需要才加載依賴 |
預先加載所有的依賴,直到使用的時候才執行 | 直到使用的時候才定義依賴 |
- Node.js是commonJS規範的主要實踐者:module、module.exports(exports)、require、global。
- ES6 在語言标準的層面上,實作了子產品功能,主要由兩個指令構成:export和import。export指令用于規定子產品的對外接口,import指令用于輸入其他子產品提供的功能。
問:比較 import 和 require 的差別?
import | require |
ES6标準中的子產品化解決方案 | 是node中遵循CommonJS規範的子產品化解決方案 |
不支援動态引入 | 支援動态引入 |
是關鍵詞 | 不是關鍵詞 |
編譯時加載,必須放在子產品頂部 | 運作時加載,理論上來說放在哪裡都可以 |
性能較好 | 性能較差 |
實時綁定方式,即導入和導出的值都指向同一個記憶體地 | 導出時是值拷貝,就算導出的值變化了,導入的值也不會變化 |
會編譯成require/exports來執行 | - |
更多:前端子產品化:CommonJS,AMD,CMD,ES6
實作plugin插件(騰訊WXG考點)
- 建立 plugins/demo-plugin.js 檔案;
- 傳遞參數 Options;
- 通過 Compilation 寫入檔案;
- 管理 Warnings 和 Errors
從零實作一個 Webpack Plugin
串聯四:你最喜歡什麼算法?
其實本瓜想回答:我最喜歡減法!因為幸福生活需要用減法。?
算法這一個 part 也已久遠,既然逃不掉,那就正面挑戰它!其實也沒那麼難。
一圖勝萬言
- 原創腦圖,轉載請說明出處
串聯知識點:資料結構、基礎算法、排序算法、進階算法。
串聯記憶:
算法算法我不怕
資料結構打趴下
周遊排序我最溜
指針動态貪心刷
一、資料結構
隊列
先入先出。
棧
先入後出。
堆
堆通常是一個可以被看做一棵樹的數組對象。
堆總是滿足下列性質:
- 堆中某個節點的值總是不大于或不小于其父節點的值;
- 堆總是一棵完全二叉樹。
- 完全二叉樹:在一顆二叉樹中,若除最後一層外的其餘層都是滿的,并且最後一層要麼是滿的,要麼在右邊缺少連續若幹節點,則此二叉樹為完全二叉樹(Complete Binary Tree)—— wiki。
(本瓜曾被拷問過這個點,大廠就是會考《資料結構》,别逃避,出來混遲早是要還的?)。
連結清單
- 循環清單(考點)
// 循環連結清單
function Node(element){
this.element = element;
this.prev = null;
this.next = null;
}
function display(){
var current = this.head;
//檢查頭節點當循環到頭節點時退出循環
while(!(current.next == null) && !(current.next.element=='head')){
print(current.next.element);
current = current.next;
}
}
function Llist(){
this.head = new Node('head');
this.head.next = this.head;
this.find = find;
this.insert = insert;
this.display = display;
this.findPrevious = findPrevious;
this.remove = remove;
}
哈希
散列函數(英語:Hash function)又稱雜湊演算法、哈希函數。以 Key:Value 的方式存儲資料。
哈希表最大的特點是可以快速定位到要查找的資料,查詢的時間複雜度接近O(1)。本瓜建議大家可以把這裡所有的資料結構的“增删改查”操作的時間複雜度都理一下,也是會被考的。
時間複雜度、空間複雜度
簡單了解:
- 循環的次數寫成 n 的表達式,就是時間複雜度。
- 申請的變量數量寫成 n 的表達式,就是空間複雜度。
時間複雜度更多重要一點,常見的時間複雜度:O(1)、O(n)、O(logn)、O(n2)。本瓜小TIP:面試/筆試如果不知道怎麼算,就在這裡面猜吧。實在不行就答:O(n) ~ O(n2) 之間,大機率不會錯?。
樹的周遊(廣度、深度)
廣度優先周遊(BFS):
需要用到隊列(Queue)來存儲節點對象,隊列的特點就是先進先出。示例
深度優先周遊(DFS):
- 前序(根結點 -> 左子樹 -> 右子樹)
- 中序(左子樹 -> 根結點 -> 右子樹)
- 後序(左子樹 -> 右子樹 -> 根結點)
二、基礎算法
遞歸思想
- 著名的斐波那契數列,你要知道!
function result(){
if(n==1||n==2){
return 1
}
return reslt(n-2)+result(n-1)
}
- 函數柯裡化,你也要知道!
函數柯裡化:是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且傳回接受餘下的參數而且傳回結果的新函數的技術。—— wiki
// 普通方式
var add1 = function(a, b, c){
return a + b + c;
}
// 柯裡化
var add2 = function(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
// demo
var foo = function(x) {
return function(y) {
return x + y
}
}
foo(3)(4) // 7
這樣處理參數,可以直接追加,而不需要初始傳參就寫完全。這裡隻是淺談,更多請自行探索。
二分法
要求手寫二分法,實在不行,能默寫也可以啊!
// 二分法:先排序,再找目标
function binary_search(arr,target) {
let min=0
let max=arr.length-1
while(min<=max){
let mid=Math.ceil((min+max)/2)
if(arr[mid]==target){
return mid
}else if(arr[mid]>target){
max=mid-1
}else if(arr[mid]<target){
min=mid+1
}
}
return "null"
}
console.log(binary_search([1,5,7,19,88],19))//3
三、排序算法
排序是比較常用也比較重要的一塊,此處并未全列出。僅強調快排和冒泡,會用雙循環也行啊。
快速排序
// 快排:選取基準,比基準大的放右邊,比基準小的放左邊,然後兩邊用遞歸
function quickSort(arr, i, j) {
if(i < j) {
let left = i;
let right = j;
let pivot = arr[left];
while(i < j) {
while(arr[j] >= pivot && i < j) { // 從後往前找比基準小的數
j--;
}
if(i < j) {
arr[i++] = arr[j];
}
while(arr[i] <= pivot && i < j) { // 從前往後找比基準大的數
i++;
}
if(i < j) {
arr[j--] = arr[i];
}
}
arr[i] = pivot;
quickSort(arr, left, i-1);
quickSort(arr, i+1, right);
return arr;
}
}
冒泡排序
// 冒泡:雙層循環
var arr=[10,20,50,100,40,200];
for(var i=0;i<arr.length-1;i++){
for(var j=0;j<arr.length-1-i;j++){
if(arr[j]>arr[j+1]){
var temp=arr[j]
arr[j]=arr[j+1]
arr[j+1]=temp
}
}
}
console.log(arr)
四、進階算法
雙指針
看到“有序”和“數組”。立刻把雙指針法排程進你的大腦記憶體。普通雙指針走不通,立刻想對撞指針!
示例:合并兩個有序數組(雙指針解法)
示例: 輸入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
輸出: [1,2,2,3,5,6]
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {void} Do not return anything, modify nums1 in-place instead.
*/
const merge = function(nums1, m, nums2, n) {
// 初始化兩個指針的指向,初始化 nums1 尾部索引k
let i = m - 1, j = n - 1, k = m + n - 1
// 當兩個數組都沒周遊完時,指針同步移動
while(i >= 0 && j >= 0) {
// 取較大的值,從末尾往前填補
if(nums1[i] >= nums2[j]) {
nums1[k] = nums1[i]
i--
k--
} else {
nums1[k] = nums2[j]
j--
k--
}
}
// nums2 留下的情況,特殊處理一下
while(j>=0) {
nums1[k] = nums2[j]
k--
j--
}
};
進階!
動态規劃
動态規劃的思想就是把一個大的問題進行拆分,細分成一個個小的子問題,且能夠從這些小的子問題的解當中推導出原問題的解。
同時需要滿足以下兩個重要性質才能進行動态規劃:
- 最優子結構性
- 子問題重疊性質
動态規劃執行個體:斐波拉契數列(上文有提到)
斐波拉契數列:采用遞歸,雖然代碼很簡潔,但是明顯随着次數的增加,導緻遞歸樹增長的非常龐大,耗時較久。
用動态規劃實作斐波拉契數列,代碼如下
function feiBoLaQie(n) {
//建立一個數組,用于存放斐波拉契數組的數值
let val = [];
//将數組初始化,将數組的每一項都置為0
for(let i =0 ;i<=n;i++){
val[i] = 0;
}
if (n==1 || n==2){
return 1;
} else{
val[1] = 1;
val[2] = 2;
for (let j =3; j<=n;j++){
val[j] = val[j-1] + val[j-2];
}
}
return val[n-1];
}
console.log(feiBoLaQie(40));//102334155
通過數組 val 中儲存了中間結果, 如果要計算的斐波那契數是 1 或者 2, 那麼 if 語句會傳回 1。 否則,數值 1 和 2 将被儲存在 val 數組中 1 和 2 的位置。
循環将會從 3 到輸入的參數之間進行周遊, 将數組的每個元素指派為前兩個元素之和, 循環結束, 數組的最後一個元素值即為最終計算得到的斐波那契數值, 這個數值也将作為函數的傳回值。
動态規劃解決速度更快。
- 參考閱讀
- 更多動态規劃示例
貪心算法
貪心算法遵循一種近似解決問題的技術,期盼通過每個階段的局部最優選擇(目前最好的解),進而達到全局的最優(全局最優解)。
注:貪心得到結果是一個可以接受的解,不一定總是得到最優的解。
示例:最少硬币找零問題
是給出要找零的錢數,以及可以用硬币的額度數量,找出有多少種找零方法。
如:美國面額硬币有:1,5,10,25
我們給36美分的零錢,看能得怎樣的結果?
function MinCoinChange(coins){
var coins = coins;
var cache = {};
this.makeChange = function(amount){
var change = [], total = 0;
for(var i = coins.length; i >= 0; i--){
var coin = coins[i];
while(total + coin <= amount){
change.push(coin);
total += coin;
}
}
return change;
}
}
var minCoinChange = new MinCoinChange([1, 5, 10, 25]);
minCoinChange.makeChange(36);
//[25, 10, 1] 即一個25美分、一個10美分、一個1美分
串聯五:web 安全你知道那些?
一圖勝萬言
- 原創腦圖,轉載請說明出處
串聯知識點:跨域、XSS(跨站腳本_gong擊)、CRFS(跨站請求僞造)、SQL 注入、DNS 劫持、HTTP 劫持。
串聯記憶:三跨兩劫持一注入
一、跨域
跨域定義
當一個請求url的協定、域名、端口三者之間任意一個與目前頁面url不同即為跨域。
跨域限制 是浏覽器的一種保護機制,若跨域,則:
- 無法讀取非同源網頁的 Cookie、LocalStorage 和 IndexedDB。
- 無法接觸非同源網頁的 DOM。
- 無法向非同源位址發送 AJAX 請求
跨域解決
跨域解決:
- JSONP;
- CORS(跨域資源分享);
// 普通跨域請求:隻需伺服器端設定 Access-Control-Allow-Origin。
// 帶cookie跨域請求:前後端都需要進行設定。
// 如前端在 axios 中設定
axios.defaults.withCredentials = true
- vue項目 設定 proxy 代理;
- nginx 代理;
- 設定document.domain解決無法讀取非同源網頁的 Cookie問題;
- 跨文檔通信 API:window.postMessage();
二、XSS
XSS 原理
XSS的原理是WEB應用程式混淆了使用者送出的資料和JS腳本的代碼邊界,導緻浏覽器把使用者的輸入當成了JS代碼來執行。XSS的gong擊對象是浏覽器一端的普通使用者。
示例:
<input type="text" value="<%= getParameter("keyword") %>">
<button>搜尋</button>
<div>
您搜尋的關鍵詞是:<%= getParameter("keyword") %>
</div>
當浏覽器請求
http://xxx/search?keyword="><script>alert('XSS');</script>
惡意代碼,就會其執行
反射型 XSS
存儲型 XSS 的攻擊步驟:
- 攻擊者将惡意代碼送出到目标網站的資料庫中。
- 使用者打開目标網站時,網站服務端将惡意代碼從資料庫取出,拼接在 HTML 中傳回給浏覽器。
- 使用者浏覽器接收到響應後解析執行,混在其中的惡意代碼也被執行。
- 惡意代碼竊取使用者資料并發送到攻擊者的網站,或者冒充使用者的行為,調用目标網站接口執行攻擊者指定的操作。
這種攻擊常見于帶有使用者儲存資料的網站功能,如論壇發帖、商品評論、使用者私信等。
存儲型 XSS
反射型 XSS 的攻擊步驟:
- 攻擊者構造出特殊的 URL,其中包含惡意代碼。
- 使用者打開帶有惡意代碼的 URL 時,網站服務端将惡意代碼從 URL 中取出,拼接在 HTML 中傳回給浏覽器。
- 使用者浏覽器接收到響應後解析執行,混在其中的惡意代碼也被執行。
- 惡意代碼竊取使用者資料并發送到攻擊者的網站,或者冒充使用者的行為,調用目标網站接口執行攻擊者指定的操作。
反射型 XSS 跟存儲型 XSS 的差別是:存儲型 XSS 的惡意代碼存在資料庫裡,反射型 XSS 的惡意代碼存在 URL 裡。
DOM型 XSS
DOM 型 XSS 的攻擊步驟:
- 攻擊者構造出特殊的 URL,其中包含惡意代碼。
- 使用者打開帶有惡意代碼的 URL。
- 使用者浏覽器接收到響應後解析執行,前端 JavaScript 取出 URL 中的惡意代碼并執行。
- 惡意代碼竊取使用者資料并發送到攻擊者的網站,或者冒充使用者的行為,調用目标網站接口執行攻擊者指定的操作。
DOM 型 XSS 跟前兩種 XSS 的差別:DOM 型 XSS 攻擊中,取出和執行惡意代碼由浏覽器端完成,屬于前端 JavaScript 自身的安全漏洞,而其他兩種 XSS 都屬于服務端的安全漏洞。
XSS 防禦
- 輸入過濾,不要相信任何用戶端的輸入;
- 對 HTML 做充分轉義;
- 設定 HTTP-only:禁止 JavaScript 讀取某些敏感 Cookie,攻擊者完成 XSS 注入後也無法竊取此 Cookie。
- 驗證碼:防止腳本冒充使用者送出危險操作。
- 謹慎使用:.innerHTML、.outerHTML、document.write();
三、CSRF
CSRF 原理
跨站請求僞造:攻擊者誘導受害者進入第三方網站,在第三方網站中,向被攻擊網站發送跨站請求。利用受害者在被攻擊網站已經擷取的注冊憑證,繞過背景的使用者驗證,達到冒充使用者對被攻擊的網站執行某項操作的目的。
曾在 09 年發生了著名的“谷歌郵箱竊取”事件,利用的就是 “CSRF”。
主要流程:
- 受害者登入a.com,并保留了登入憑證(Cookie)。
- 攻擊者引誘受害者通路了b.com。
- b.com 向 a.com 發送了一個請求:a.com/act=xx。浏覽器會預設攜帶a.com的Cookie。
- a.com接收到請求後,對請求進行驗證,并确認是受害者的憑證,誤以為是受害者自己發送的請求。
- a.com以受害者的名義執行了act=xx。
- 攻擊完成,攻擊者在受害者不知情的情況下,冒充受害者,讓a.com執行了自己定義的操作。
注:攻擊者無法直接竊取到使用者的資訊(Cookie,Header,網站内容等),僅僅是冒用 Cookie 中的資訊。
這告訴我們沖浪的時候不能随便點連結,是有風險哒。
CSRF 防禦
防禦政策:
- 自動防禦政策:同源檢測(Origin 和 Referer 驗證)。
- 主動防禦措施:Token驗證 或者 雙重Cookie驗證 以及配合Samesite Cookie。
- 保證頁面的幂等性,後端接口不要在GET頁面中做使用者操作。
四、SQL 注入
SQL 注入原理
Sql 注入攻擊是通過将惡意的 Sql 查詢或添加語句插入到應用的輸入參數中,再在背景 Sql 伺服器上解析執行進行的攻擊,它目前黑客對資料庫進行攻擊的最常用手段之一。
SQL 注入防禦
防禦:用sql語句預編譯和綁定變量,是防禦sql注入的最佳方法。還可以通過嚴格檢查參數的資料類型的方式來防禦。
五、DNS 劫持
DNS 劫持原理
DNS劫持又稱域名劫持,是指在劫持的網絡範圍内攔截域名解析的請求,分析請求的域名,把審查範圍以外的請求放行,否則傳回假的IP位址或者什麼都不做使請求失去響應,其效果就是對特定的網絡不能通路或通路的是假網址。其實本質就是對DNS解析伺服器做手腳
DNS 劫持防禦
解決辦法:
DNS的劫持過程是通過攻擊營運商的解析伺服器來達到目的。我們可以不用營運商的DNS解析而使用自己的解析伺服器或者是提前在自己的App中将解析好的域名以IP的形式發出去就可以繞過營運商DNS解析,這樣一來也避免了DNS劫持的問題。
六、HTTP 劫持
HTTP 劫持原理
在營運商的路由器節點上,設定協定檢測,一旦發現是HTTP請求,而且是html類型請求,則攔截進行惡意處理。
常見有兩種:
- 類似DNS劫持傳回302讓使用者浏覽器跳轉到另外的位址。(釣魚網站就是這麼幹)
- 在伺服器傳回的HTML資料中插入js或dom節點(廣告)。(常見)
HTTP 劫持防禦
- 使用HTTPS;
- 使用禁止轉碼申明;
- 在開發的網頁中加入代碼過濾:用 js 檢查所有的外鍊是否屬于白名單;
- 聯系營運商;