前端面試總結與思考
- 2019 年 10 月 6 日
- 筆記
1、談談你對tcp三次握手和四次揮手的理解?
序列號seq:佔4個位元組,用來標記數據段的順序,TCP把連接中發送的所有數據位元組都編上一個序號,第一個位元組的編號由本地隨機產生;給位元組編上序號後,就給每一個報文段指派一個序號;序列號seq就是這個報文段中的第一個位元組的數據編號。
確認號ack:佔4個位元組,期待收到對方下一個報文段的第一個數據位元組的序號;序列號表示報文段攜帶數據的第一個位元組的編號;而確認號指的是期望接收到下一個位元組的編號;因此當前報文段最後一個位元組的編號+1即為確認號。
確認ACK:佔1位,僅當ACK=1時,確認號欄位才有效。ACK=0時,確認號無效
同步SYN:連接建立時用於同步序號。當SYN=1,ACK=0時表示:這是一個連接請求報文段。若同意連接,則在響應報文段中使得SYN=1,ACK=1。因此,SYN=1表示這是一個連接請求,或連接接受報文。SYN這個標誌位只有在TCP建產連接時才會被置1,握手完成後SYN標誌位被置0。
終止FIN:用來釋放一個連接。FIN=1表示:此報文段的發送方的數據已經發送完畢,並要求釋放運輸連接
PS:ACK、SYN和FIN這些大寫的單詞表示標誌位,其值要麼是1,要麼是0;ack、seq小寫的單詞表示序號。
欄位 含義
URG 緊急指針是否有效。為1,表示某一位需要被優先處理
ACK 確認號是否有效,一般置為1。
PSH 提示接收端應用程式立即從TCP緩衝區把數據讀走。
RST 對方要求重新建立連接,複位。
SYN 請求建立連接,並在其序列號的欄位進行序列號的初始值設定。建立連接,設置為1
FIN 希望斷開連接。
三次握手過程理解
第一次握手:建立連接時,客戶端發送syn包(syn=j)到伺服器,並進入SYN_SENT狀態,等待伺服器確認;SYN:同步序列編號(Synchronize Sequence Numbers)。
第二次握手:伺服器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態;
第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和伺服器進入ESTABLISHED(TCP連接成功)狀態,完成三次握手。
四次揮手過程理解
1)客戶端進程發出連接釋放報文,並且停止發送數據。釋放數據報文首部,FIN=1,其序列號為seq=u(等於前面已經傳送過來的數據的最後一個位元組的序號加1),此時,客戶端進入FIN-WAIT-1(終止等待1)狀態。TCP規定,FIN報文段即使不攜帶數據,也要消耗一個序號。
2)伺服器收到連接釋放報文,發出確認報文,ACK=1,ack=u+1,並且帶上自己的序列號seq=v,此時,服務端就進入了CLOSE-WAIT(關閉等待)狀態。TCP伺服器通知高層的應用進程,客戶端向伺服器的方向就釋放了,這時候處於半關閉狀態,即客戶端已經沒有數據要發送了,但是伺服器若發送數據,客戶端依然要接受。這個狀態還要持續一段時間,也就是整個CLOSE-WAIT狀態持續的時間。
3)客戶端收到伺服器的確認請求後,此時,客戶端就進入FIN-WAIT-2(終止等待2)狀態,等待伺服器發送連接釋放報文(在這之前還需要接受伺服器發送的最後的數據)。
4)伺服器將最後的數據發送完畢後,就向客戶端發送連接釋放報文,FIN=1,ack=u+1,由於在半關閉狀態,伺服器很可能又發送了一些數據,假定此時的序列號為seq=w,此時,伺服器就進入了LAST-ACK(最後確認)狀態,等待客戶端的確認。
5)客戶端收到伺服器的連接釋放報文後,必須發出確認,ACK=1,ack=w+1,而自己的序列號是seq=u+1,此時,客戶端就進入了TIME-WAIT(時間等待)狀態。注意此時TCP連接還沒有釋放,必須經過2∗∗MSL(最長報文段壽命)的時間後,當客戶端撤銷相應的TCB後,才進入CLOSED狀態。
6)伺服器只要收到了客戶端發出的確認,立即進入CLOSED狀態。同樣,撤銷TCB後,就結束了這次的TCP連接。可以看到,伺服器結束TCP連接的時間要比客戶端早一些。
常見面試題
【問題1】為什麼連接的時候是三次握手,關閉的時候卻是四次握手?
答:因為當Server端收到Client端的SYN連接請求報文後,可以直接發送SYN+ACK報文。其中ACK報文是用來應答的,SYN報文是用來同步的。但是關閉連接時,當Server端收到FIN報文時,很可能並不會立即關閉SOCKET,所以只能先回復一個ACK報文,告訴Client端,"你發的FIN報文我收到了"。只有等到我Server端所有的報文都發送完了,我才能發送FIN報文,因此不能一起發送。故需要四步握手。
【問題2】為什麼TIME_WAIT狀態需要經過2MSL(最大報文段生存時間)才能返回到CLOSE狀態?
答:雖然按道理,四個報文都發送完畢,我們可以直接進入CLOSE狀態了,但是我們必須假象網路是不可靠的,有可以最後一個ACK丟失。所以TIME_WAIT狀態就是用來重發可能丟失的ACK報文。在Client發送出最後的ACK回復,但該ACK可能丟失。Server如果沒有收到ACK,將不斷重複發送FIN片段。所以Client不能立即關閉,它必須確認Server接收到了該ACK。Client會在發送出ACK之後進入到TIME_WAIT狀態。Client會設置一個計時器,等待2MSL的時間。如果在該時間內再次收到FIN,那麼Client會重發ACK並再次等待2MSL。所謂的2MSL是兩倍的MSL(Maximum Segment Lifetime)。MSL指一個片段在網路中最大的存活時間,2MSL就是一個發送和一個回復所需的最大時間。如果直到2MSL,Client都沒有再次收到FIN,那麼Client推斷ACK已經被成功接收,則結束TCP連接。
【問題3】為什麼不能用兩次握手進行連接?
答:3次握手完成兩個重要的功能,既要雙方做好發送數據的準備工作(雙方都知道彼此已準備好),也要允許雙方就初始序列號進行協商,這個序列號在握手過程中被發送和確認。
現在把三次握手改成僅需要兩次握手,死鎖是可能發生的。作為例子,考慮電腦S和C之間的通訊,假定C給S發送一個連接請求分組,S收到了這個分組,並發 送了確認應答分組。按照兩次握手的協定,S認為連接已經成功地建立了,可以開始發送數據分組。可是,C在S的應答分組在傳輸中被丟失的情況下,將不知道S 是否已準備好,不知道S建立什麼樣的序列號,C甚至懷疑S是否收到自己的連接請求分組。在這種情況下,C認為連接還未建立成功,將忽略S發來的任何數據分 組,只等待連接確認應答分組。而S在發出的分組超時後,重複發送同樣的分組。這樣就形成了死鎖。
【問題4】如果已經建立了連接,但是客戶端突然出現故障了怎麼辦?
TCP還設有一個保活計時器,顯然,客戶端如果出現故障,伺服器不能一直等下去,白白浪費資源。伺服器每收到一次客戶端的請求後都會重新複位這個計時器,時間通常是設置為2小時,若兩小時還沒有收到客戶端的任何數據,伺服器就會發送一個探測報文段,以後每隔75秒鐘發送一次。若一連發送10個探測報文仍然沒反應,伺服器就認為客戶端出了故障,接著就關閉連接。
2、react中setState什麼時候是同步的,什麼時候是非同步的?
setState 只在合成事件和鉤子函數中是「非同步」的,在原生事件和 setTimeout 中都是同步的。
合成事件:就是react 在組件中的onClick等都是屬於它自定義的合成事件
原生事件:比如通過addeventListener添加的,dom中的原生事件
setState的「非同步」並不是說內部由非同步程式碼實現,其實本身執行的過程和程式碼都是同步的,只是合成事件和鉤子函數的調用順序在更新之前,導致在合成事件和鉤子函數中沒法立馬拿到更新後的值,形式了所謂的「非同步」,當然可以通過第二個參數 setState(partialState, callback) 中的callback拿到更新後的結果。
setState 的批量更新優化也是建立在「非同步」(合成事件、鉤子函數)之上的,在原生事件和setTimeout 中不會批量更新,在「非同步」中如果對同一個值進行多次 setState , setState 的批量更新策略會對其進行覆蓋,取最後一次的執行,如果是同時 setState 多個不同的值,在更新時會對其進行合併批量更新。
state = { val: 0 } batchUpdates = () => { this.setState({ val: this.state.val + 1 }) this.setState({ val: this.state.val + 1 }) this.setState({ val: this.state.val + 1 }) }
其實val只是1
具體大家可以看看react的源碼~
————————————————
3、介紹下重繪和迴流,以及如何進行優化?
迴流(Reflow) 和 重繪(Repaint) 可以說是每一個web開發者都經常聽到的兩個詞語,我也不例外,可是我之前一直不是很清楚這兩步具體做了什麼事情。而且很尷尬的是每每提到性能優化的時候,我們可以說出 減少迴流及其重繪 可以提高頁面性能,當然但是一深入問到有什麼方式呢?可能就說不出具體體現了,所以整理一下有關這方面的知識 ↓
從瀏覽器的渲染的過程出發
從上面這個圖上,我們可以看到,瀏覽器渲染過程如下:
解析HTML,生成DOM樹,解析CSS,生成CSSOM樹
將DOM樹和CSSOM樹結合,生成渲染樹(Render Tree)
Layout(迴流): 根據生成的渲染樹,進行迴流(Layout),得到節點的幾何資訊(位置,大小)
Painting(重繪): 根據渲染樹以及迴流得到的幾何資訊,得到節點的絕對像素
Display(展示): 將像素髮送給GPU,展示在頁面上。(這一步其實還有很多內容,比如會在GPU將多個合成層合併為同一個層,並展示在頁面中。而css3硬體加速的原理則是新建合成層,這裡我們不展開詳細說明)
渲染過程看起來很簡單,讓我們來具體了解下每一步具體做了什麼。
生成渲染樹
為了構建渲染樹,瀏覽器主要完成了以下工作:
從DOM樹的根節點開始遍歷每個可見節點。
對於每個可見的節點,找到CSSOM樹中對應的規則,並應用它們。
根據每個可見節點以及其對應的樣式,組合生成渲染樹。
第一步中,既然說到了要遍歷可見的節點,那麼我們得先知道,什麼節點是不可見的。不可見的節點包括:
一些不會渲染輸出的節點,比如<script>、<meta>、<link>等。
一些通過css進行隱藏的節點。比如display:none。注意,利用visibility和opacity隱藏的節點,還是會顯示在渲染樹上的。只有display:none的節點才不會顯示在渲染樹上。
從上面的例子來講,我們可以看到span標籤的樣式有一個display:none,因此,它最終並沒有在渲染樹上。
注意:渲染樹只包含可見的節點
迴流(Reflow)
前面我們通過構造渲染樹,我們將可見DOM節點以及它對應的樣式結合起來,可是我們還需要計算它們在設備視口(viewport)內的確切位置和大小,這個計算的階段就是迴流。
為了弄清每個對象在網站上的確切大小和位置,瀏覽器從渲染樹的根節點開始遍歷,我們可以以下面這個實例來表示:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Critial Path: Hello world!</title> </head> <body> <div style="width: 50%"> <div style="width: 50%">Hello world!</div> </div> </body> </html>
我們可以看到,第一個div將節點的顯示尺寸設置為視口寬度的50%,第二個div將其尺寸設置為父節點的50%。而在迴流這個階段,我們就需要根據視口具體的寬度,將其轉為實際的像素值。(如下圖)
重繪(Repaint )
最終,我們通過構造渲染樹和迴流階段,我們知道了哪些節點是可見的,以及可見節點的樣式和具體的幾何資訊(位置、大小),那麼我們就可以將渲染樹的每個節點都轉換為螢幕上的實際像素,這個階段就叫做重繪節點。
既然知道了瀏覽器的渲染過程後,我們就來探討下,何時會發生迴流重繪。
何時發生迴流重繪
我們前面知道了,迴流這一階段主要是計算節點的位置和幾何資訊,那麼當頁面布局和幾何資訊發生變化的時候,就需要迴流。比如以下情況:
添加或刪除可見的DOM元素
元素的位置發生變化
元素的尺寸發生變化(包括外邊距、內邊框、邊框大小、高度和寬度等)
內容發生變化,比如文本變化或圖片被另一個不同尺寸的圖片所替代
頁面一開始渲染的時候(這肯定避免不了)
瀏覽器的窗口尺寸變化(因為迴流是根據視口的大小來計算元素的位置和大小的)
根據改變的範圍和程度,渲染樹中或大或小的部分需要重新計算,有些改變會觸發整個頁面的重排,比如,滾動條出現的時候或者修改了根節點
注意:迴流一定會觸發重繪,而重繪不一定會迴流
瀏覽器的渲染機制、優化機制及其處理動畫流程
1. 瀏覽器渲染機制
瀏覽器採用流式布局模型(Flow Based Layout)
瀏覽器會把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合併就產生了渲染樹(Render Tree)
有了RenderTree,我們就知道了所有節點的樣式,然後計算他們在頁面上的大小和位置,最後把節點繪製到頁面上
由於瀏覽器使用流式布局,對Render Tree的計算通常只需要遍歷一次就可以完成,但table及其內部元素除外,他們可能需要多次計算,通常要花3倍於同等元素的時間,這也是為什麼要避免使用table布局的原因之一
2. 瀏覽器優化機制
現代瀏覽器大多都是通過隊列機制來批量更新布局,瀏覽器會把修改操作放在隊列中,至少一個瀏覽器刷新(即16.6ms)才會清空隊列,但當你獲取布局資訊的時候,隊列中可能有會影響這些屬性或方法返回值的操作,即使沒有,瀏覽器也會強制清空隊列,觸發迴流與重繪來確保返回正確的值
主要包括以下屬性或方法:
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
width、height
getComputedStyle()、getBoundingClientRect()
以上屬性和方法都需要返回最新的布局資訊,因此瀏覽器不得不清空隊列,觸發迴流重繪來返回正確的值。因此,我們在修改樣式的時候,最好避免使用上面列出的屬性,他們都會刷新渲染隊列。如果要使用它們,最好將值快取起來
3. 瀏覽器處理動畫流程
瀏覽器會把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合併就產生了渲染樹(Render Tree)
分割圖層:瀏覽器根據z-index,和脫離了原來dom層的dom解構進行分層
計算樣式:分解圖層完畢後,將所有的圖層批量進行樣式計算。這裡有些屬性是CPU去計算的,有些屬性是GPU去計算的
reflow -> relayout -> paint set up -> repaint:這一系列過程其實是頁面從迴流到重繪發生的步驟,這也是為什麼迴流必然引起重繪,而重繪卻不一定要迴流的原因
GPU:重繪後的「畫布」交給GPU去處理
組合布局:瀏覽器組合布局,然後頁面就出現了
如何減少迴流和重繪
CSS
使用 transform 替代 top
使用 visibility 替換 display: none ,因為前者只會引起重繪,後者會引發迴流(改變了布局)
避免使用table布局,可能很小的一個小改動會造成整個 table 的重新布局
儘可能在DOM樹的最末端改變class,迴流是不可避免的,但可以減少其影響。儘可能在DOM樹的最末端改變class,可以限制了迴流的範圍,使其影響儘可能少的節點
避免設置多層內聯樣式,CSS 選擇符從右往左匹配查找,避免節點層級過多
<div> <a> <span></span> </a> </div> <style> span { color: red; } div > a > span { color: red; } </style>
對於第一種設置樣式的方式來說,瀏覽器只需要找到頁面中所有的 span 標籤然後設置顏色,但是對於第二種設置樣式的方式來說,瀏覽器首先需要找到所有的 span 標籤,然後找到 span 標籤上的 a 標籤,最後再去找到 div 標籤,然後給符合這種條件的 span 標籤設置顏色,這樣的遞歸過程就很複雜。所以我們應該儘可能的避免寫過於具體的 CSS 選擇器,然後對於 HTML 來說也盡量少的添加無意義標籤,保證層級扁平
將動畫效果應用到position屬性為absolute或fixed的元素上,避免影響其他元素的布局,這樣只是一個重繪,而不是迴流,同時,控制動畫速度可以選擇 requestAnimationFrame,詳見探討 requestAnimationFrame
避免使用CSS表達式,可能會引發迴流
將頻繁重繪或者迴流的節點設置為圖層,圖層能夠阻止該節點的渲染行為影響別的節點,例如will-change、video、iframe等標籤,瀏覽器會自動將該節點變為圖層
CSS3 硬體加速(GPU加速),使用css3硬體加速,可以讓transform、opacity、filters這些動畫不會引起迴流重繪 。但是對於動畫的其它屬性,比如background-color這些,還是會引起迴流重繪的,不過它還是可以提升這些動畫的性能
我們可以先看個 demo例子 。我通過使用chrome的Performance捕獲了動畫一段時間裡的迴流重繪情況,實際結果如下圖
從圖中我們可以看出,在動畫進行的時候,有Layout(迴流),既然有迴流當然會有painting(重繪)。但是按理論來說使用是沒有迴流及重繪的,至於這點我查閱了一下其他資料也並沒有相關於這方面的說明,如果有我會及時更新上來,亦或許可能是我測試有問題的原因吧,暫不糾結我們需要知道css3硬體加速是可以提升頁面性能的 ~
重點
使用css3硬體加速,可以讓transform、opacity、filters這些動畫不會引起迴流重繪
對於動畫的其它屬性,比如background-color這些,還是會引起重繪的,但是不會引起迴流,但在性能面前它還是可以提升的
css3硬體加速的缺點
當然,css3硬體加速還是有坑的:
如果你為太多元素使用css3硬體加速,會導致記憶體佔用較大,會有性能問題。
在GPU渲染字體會導致抗鋸齒無效。這是因為GPU和CPU的演算法不同。因此如果你不在動畫結束的時候關閉硬體加速,會產生字體模糊。
JavaScript
避免頻繁操作樣式,最好一次性重寫style屬性,或者將樣式列表定義為class並一次性更改class屬性
程式碼展示:
const el = document.getElementById('test'); el.style.padding = '5px'; el.style.borderLeft = '1px'; el.style.borderRight = '2px';
有三個樣式屬性被修改了,每一個都會影響元素的幾何結構,引起迴流。當然,大部分現代瀏覽器都對其做了優化,因此,只會觸發一次重排。但是如果在舊版的瀏覽器或者在上面程式碼執行的時候,有其他程式碼訪問了布局資訊(上文中的會觸發迴流的布局資訊),那麼就會導致三次重排,因此我們可以合併所有的改變然後依次處理,比如我們應該改成
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
避免頻繁操作DOM,創建一個documentFragment,在它上面應用所有DOM操作,最後再把它添加到文檔中
程式碼展示:
function appendDataToElement(appendToElement, data) { let li; for (let i = 0; i < data.length; i++) { li = document.createElement('li'); li.textContent = 'text'; appendToElement.appendChild(li); } } const ul = document.getElementById('list'); appendDataToElement(ul, data);
使用文檔片段(document fragment)在當前DOM之外構建一個子樹,再把它拷貝迴文檔,所以應該改成 ↓
const ul = document.getElementById('list'); const fragment = document.createDocumentFragment(); appendDataToElement(fragment, data); ul.appendChild(fragment);
避免頻繁讀取會引發迴流/重繪的屬性,如果確實需要多次使用,就用一個變數快取起來
程式碼展示:
function initBox() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
這段程式碼看上去是沒有什麼問題,可是其實會造成很大的性能問題。在每次循環的時候,都讀取了box的一個offsetWidth屬性值,然後利用它來更新p標籤的width屬性。這就導致了每一次循環的時候,瀏覽器都必須先使上一次循環中的樣式更新操作生效,才能響應本次循環的樣式讀取操作。每一次循環都會強制瀏覽器刷新隊列。我們可以優化為 ↓
const width = box.offsetWidth; function initBox() { for (let i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = width + 'px'; } }
對具有複雜動畫的元素使用絕對定位,使它脫離文檔流,否則會引起父元素及後續元素頻繁迴流
在這對於複雜動畫效果,由於會經常的引起迴流重繪,因此,我們可以使用絕對定位,讓它脫離文檔流。否則會引起父元素以及後續元素頻繁的迴流,程式碼可以看頁面 → demo例子
打開這個例子後,我們可以打開控制台,控制台上會輸出當前的幀數(雖然不準)。但是我們發現幀數一直都沒到60。
我們還可以參考 Performance性能火焰圖,可以看出當我們動畫不是決定定位的時候,從圖中可以看到Rendering(渲染計算,包括迴流)和Painting(重繪)在錄製的性能階段一直處於高峰,從環狀圖也可以看出,這個迴流(Layout,可以放大每一個Rendering就可以看到)一直在進行計算著
當我們點擊按鈕後,就是將動畫脫離文檔流後,控制台的fs幀數就可以穩定60啦,我們再去抓取動畫的Performance ,此時我們可以看到 Rendering和Painting 所佔的比例很少了,可見動畫設置為絕對定位脫離文檔流,可大大優化我們頁面的性能!
整理
最後整理一下css中哪些樣式屬性會導致迴流和重繪,及其什麼時候開啟GPU加速
觸發迴流
1)盒子模型相關屬性:
width * height
offset * client * scroll
padding * margin
border * border-width
min-height(width) * max-height(width)
display
2)定位和浮動
top * bottom * left * right
position
float
clear
3)改變節點內部文字結構
text-align * line-height * vertical-align
overflow * overflow-y * overflow-x
font-family * font-size * font-weight
white-space
觸發重繪
border-style * border-radius
visibility * text-decoration
color * background * background-image * background-position * background-repeat * background-size
outline-color * outline * outline-style * outline-width
box-shadow
觸發GPU加速
概念:
創建了新的layer,屬性改變直接由GPU處理,加快處理速度。使得有一些屬性的改變可以略過relayout(迴流計算),減少瀏覽器在動畫運行時所需要做的工作
缺點:GPU使用會增加記憶體消耗,同時也會影響字體的抗鋸齒效果,導致文本在動畫期間會顯得有些模糊
以 chrome瀏覽器為例,符合以下情況,則會創建一個獨立的layer:
1)transform(3d轉換)
2) video標籤
3)混合插件(如 Flash)
4) isolation == isolate
5)opacity < 1
6)filter != normal
7)z-index != auto || 0 + 父元素display: flex|inline-flex
8)mix-blend-mode != normal
9)position == fixed || absolute
10)-webkit-overflow-scrolling == touch
11)will-change:指定可以形成新layer的元素
————————————————
4、聊聊redux和vuex的共同點與區別?

相同點:
1.都是通過store來作為全局狀態存儲對象
- 改變store的直接方法(vuex中的mutation和redux中的reducer)只允許同步操作。
不同點
- vuex只有展示組件(通過全局根部植入直接訪問store),而redux中展示組件通過容器組件連接store再進行訪問。 另外vuex自帶module化功能,而redux是沒有的。
- vuex中消除了action的概念
- vuex只能配合vue而redux可以配合任何框架
- vuex中的非同步操作只能在action中進行,而redux中沒有特別的為非同步操作創建一個方法。
【其他一些補充】
vuex中改變store的唯一方法就是通過mutation,非同步方法通過action最後也是通過mutation來改變store。這裡說下為什麼vuex要用action,個人理解是因為所有非同步函數是不能追蹤的,由於vuex需要通過mutation記錄每次store的變化,因此mutation中不允許有非同步操作就像redux中的reducer中的操作必須也是同步的一樣。
5、為什麼前端要提倡模組化開發
前端的發展總會讓我們眼前一亮,這又有什麼規範出來了,上個規範我還沒理解透徹呢。但不管未來怎麼發展,了解歷史還是非常重要的,以史為鏡,可以知得失。知道了規範的發展歷史,才能更好的了解目前的規範。
沒有模組化,前端程式碼會怎麼樣?
- 變數和方法不容易維護,容易污染全局作用域
- 載入資源的方式通過script標籤從上到下。
- 依賴的環境主觀邏輯偏重,程式碼較多就會比較複雜。
- 大型項目資源難以維護,特別是多人合作的情況下,資源的引入會讓人奔潰。
當年我們是怎麼引入資源的。

script.png
看著上面的script標籤,是不是有一種很熟悉的感覺。通過script引入你想要的資源,從上到下的順序,這其中順序是非常重要的,資源的載入先後決定你的程式碼是否能夠跑的下去。當然我們還沒有加入defer和async屬性,不然載入的邏輯會更加複雜。這裡引入的資源還是算少的,過多的script標籤會造成過多的請求。同時項目越大,到最後依賴會越來越複雜,並且難以維護,依賴模糊,請求過多。全局污染的可能性就會更大。那麼問題來了,如何形成獨立的作用域?
defer和async的區別
defer要等到整個頁面在記憶體中正常渲染結束(DOM 結構完全生成,以及其他腳本執行完成),才會執行;async一旦下載完,渲染引擎就會中斷渲染,執行這個腳本以後,再繼續渲染。一句話,defer是「渲染完再執行」,async是「下載完就執行」。另外,如果有多個defer腳本,會按照它們在頁面出現的順序載入,而多個async腳本是不能保證載入順序的。
模組化的基石
立即執行函數(immediately-invoked function expression),簡稱IIFE,其實是一個javaScript函數。可以在函數內部定義方法以及私有屬性,相當於一個封閉的作用域。例如下面的程式碼:
let module = (function(){ let _private = 'myself'; let fun = () =>{ console.log(_private) } return { fun:fun } })() module.fun()//myself module._private//undefined
以上程式碼便可以形成一個獨立的作用域,一定程度上可以減少全局污染的可能性。這種寫法可是現代模組化的基石。雖然能夠定義方法,但是不能定義屬性,這時候各種前端規範就陸續登場了。
首先登場的是common.js
最先遵守CommonJS規範是node.js。這次變革讓服務端也能用js爽歪歪的寫了,我們的javaScript並不止於瀏覽器,服務端也能分一杯羹,被人稱為模組化的第一座里程碑。想想長征二萬五,第一座里程碑在哪裡?
CommomJS模組的特點
- 模組內的程式碼只會運行在模組作用域內,不會污染到全局作用域
- 模組的可以多次引入,但只會在第一次載入的時候運行一次,後面的運行都是取快取的值。想要讓模組再次運行,必須清楚快取。
// 刪除指定模組的快取 delete require.cache[moduleName]; // 刪除所有模組的快取 Object.keys(require.cache).forEach(function(key) { delete require.cache[key]; })
- 模組的載入順序,遵循在程式碼中出現的順序。
為什麼要少用exports
exports只是一個變數,指向module.exports,也就是exports只是一個引用而已。所以對外輸出模組的時候,我們就可以通過exports添加方法和和屬性。通過module.exports對外輸出其實也是讀取module.exports的變數。但是使用exports時要非常的小心,因為稍不注意就會切斷和module.exports的聯繫。例如:
exports = function(x) {console.log(x)};
上面的程式碼運行之後,exports不再指向module.exports。如果你難以區分清楚,一般最好就別用exports,只使用module.exports就行。
怎麼區分模組是直接執行,還是被調用執行。
require.mainAPI就有這樣的作用,如果模組是直接執行,那麼這時require.main屬性就指向模組本身。例如下面:
require.main === module
為什麼客戶端不使用commonjs規範?
我們知道客戶端(瀏覽器)載入資源主要是通過網路獲取,一般本地讀取的比較少,而node.js主要是用於伺服器編程,模組文件一般都存在於本地硬碟上,然後I/O讀取是非常快速的,所以即使是同步載入也是能夠適用的,而瀏覽器載入資源必須通過非同步獲取,比如常見的ajax請求,這時候AMD規範就非常合適了,可以非同步載入模組,允許回調函數。
客戶端的規範不僅僅只有AMD,還有CMD.
每個規範的興起背後總有一些原因,requirejs的流行是因為commonjs未能滿足我們需要的效果,sea.js被創造的原因也是因為requirejs不能滿足一些場景。
AMD和CMD的區別
– |
AMD |
CMD |
---|---|---|
原理 |
define(id ?,dependencies ?,factory)定義了一個單獨的函數「define」。id為要定義的模組。依賴通過dependencies傳入factory是一個工廠參數的對象,指定模組的導出值。 |
CMD規範與AMD類似,並盡量保持簡單,但是更多的與common.js保持兼容性。 |
優點 |
特別適用於瀏覽器環境的非同步載入 ,且可以並行載入。依賴前置,提前執行。定義模組時就能清楚的聲明所要依賴的模組 |
依賴就近,延遲執行。按需載入,需要用到時再require |
缺點 |
開發成本較高,模組定義方式的語義交為難理解,不是很符合通過的模組化思維方式。 |
依賴SPM打包,模組的載入主觀邏輯交重。 |
體現 |
require.js |
sea.js |
ES6讓前端模組化觸手可及
概念
ES6的模組不是對象,import語法會被JavaScript引擎靜態分析,請注意,這是一個很重要的功能,我們通常使用commonjs時,程式碼都是在運行時載入的,而es6是在編譯時就引入模組程式碼,當然我們現在的瀏覽器還沒有這麼強大的功能,需要藉助各類的編譯工具(webpack)才能正確的姿勢來使用es6的模組化的功能。也正因為能夠編譯時就引入模組程式碼,所以使得靜態分析就能夠實現了。
ES6模組化有哪些優點
- 靜態化編譯 如果能夠靜態化,編譯的時候就能確定模組的依賴關係,以及輸出和輸入的變數,然後CommonJS和AMD以及CMD都只能在運行程式碼時才能確定這些關係。
- 不需要特殊的UMD模組化格式 不再需要UMD模組的格式,將來伺服器和瀏覽器都會支援ES6模組格式。目前各種工具庫(webpack)其實已經做到這一點了。
- 目前的各類全局變數都可以模組化 比如navigator現在是全局變數,以後就可以模組化載入。這樣就不再需要對象作為命名空間。
需要注意的地方
- export語句輸出的介面,通過import引入之後,與其對應的值是動態的綁定關係,也就是模組的內的值即使改變了,也是可以取到實時的值的。而commonJS模組輸出的是值的快取,不存在動態更新。
- 由於es6設計初衷就是要靜態優化,所以export命令不能處於塊級作用域內,如果出現就會報錯,所以一般export統一寫在底部或則頂層。
function fun(){ export default 'hello' //SyntaxError }
- import命令具有提升效果,會提升到整個模組的頭部首先執行。例如:
fun() import {fun} from 'myModule';
上面的程式碼import的執行早於fun調用,原因是import命令是編譯階段執行的,也就是在程式碼運行之前。
export default使用
export default就是輸出一個叫default的變數或方法,然後系統允許你為它取任意名字。所以,你可以看到下面的寫法。
//modules.js function add(x,y){ return x*y } export {add as default}; //等同於 export default add; //app.js import {default add foo} from 'modules'; //等同於 import foo from 'modules'
這是因為export default命令其實只是輸出一個叫做default的變數,所以它後面不能跟變數聲明語句。
特別技巧偵查程式碼是否處於ES6模組中
利用頂層的this等於undefined這個語法點,可以偵測當前程式碼是否在 ES6 模組之中。
const isNotModuleScript = this !== undefined;
6、在輸入框中如何判斷輸入的是一個正確的身份證
var test=/^
d{15}(dd[0-9Xx])?
$/
7、實現一個sleep函數?
sleep函數作用是讓執行緒休眠,等到指定時間在重新喚起。
方法一:這種實現方式是利用一個偽死循環阻塞主執行緒。因為JS是單執行緒的。所以通過這種方式可以實現真正意義上的sleep()。
function sleep(delay) { var start = (new Date()).getTime(); while ((new Date()).getTime() - start < delay) { continue; } } function test() { console.log('111'); sleep(2000); console.log('222'); } test()
方法二:定時器
function sleep1(ms, callback) { setTimeout(callback, ms) } //sleep 1s sleep1(1000, () => { console.log(1000) })
方法三:es6非同步處理
const sleep = time => { return new Promise(resolve => setTimeout(resolve,time) ) } sleep(1000).then(()=>{ console.log(1) })
方法四:yield後面是一個生成器 generator
function sleepGenerator(time) { yield new Promise(function(resolve,reject){ setTimeout(resolve,time); }) } sleepGenerator(1000).next().value.then(()=>{console.log(1)})
方法五:es7—- async/await是基於Promise的,是進一步的一種優化
function sleep(time) { return new Promise(resolve => setTimeout(resolve,time) ) } async function output() { let out = await sleep(1000); console.log(1); return out; } output();
注意點:
async用來申明裡面包裹的內容可以進行同步的方式執行,await則是進行執行順序控制,每次執行一個await,程式都會暫停等待await返回值,然後再執行之後的await。
await後面調用的函數需要返回一個promise,另外這個函數是一個普通的函數即可,而不是generator。
await只能用在async函數之中,用在普通函數中會報錯。
await命令後面的 Promise 對象,運行結果可能是 rejected,所以最好把 await 命令放在 try…catch 程式碼塊中。
其實,async / await的用法和co差不多,await和yield都是表示暫停,外面包裹一層async 或者 co來表示裡面的程式碼可以採用同步的方式進行處理。不過async / await裡面的await後面跟著的函數不需要額外處理,co是需要將它寫成一個generator的。
————————————————
8、為什麼通常在發送數據埋點請求的時候使用的是1*1像素的透明gif圖片?
- 避免跨域(img 天然支援跨域)
- 利用空白gif或1×1 px的img是互聯網廣告或網站監測方面常用的手段,簡單、安全、相比PNG/JPG體積小,1px 透明圖,對網頁內容的影響幾乎沒有影響,這種請求用在很多地方,比如瀏覽、點擊、熱點、心跳、ID頒發等等,
- 圖片請求不佔用 Ajax 請求限額
- GIF的最低合法體積最小(最小的BMP文件需要74個位元組,PNG需要67個位元組,而合法的GIF,只需要43個位元組)
- 不會阻塞頁面載入,影響用戶的體驗,只要new Image對象就好了,一般情況下也不需要append到DOM中,通過它的onerror和onload事件來檢測發送狀態。
- 示例: <script type="text/javascript"> var thisPage = location.href; var referringPage = (document.referrer) ? document.referrer : "none"; var beacon = new Image(); beacon.src = "http://www.example.com/logger/beacon.gif?page=" + encodeURI(thisPage) + "&ref=" + encodeURI(referringPage); </script>
9、a.b.c.d和a['b'] ['c']['d'],那個性能更高?
10、介紹下webpack熱跟新原理,
是如何做到在不刷新瀏覽器的前提下更新頁面的
模組熱替換(Hot Module Replacement)
模組熱替換功能會在應用程式運行過程中替換、添加或刪除模組,無需重新載入整個頁面。主要是通過以下幾種方式,來顯著加快開發速度:
保留在完全重新載入頁面時丟失的應用程式狀態。
只更新變更內容,以節省寶貴的開發時間。
調整樣式更加快速 – 幾乎相當於在瀏覽器調試器中更改樣式。
webpack-dev-server實現熱更新(HMR)
webpack-dev-server就是一個基於node.js和webpack的小型伺服器。
熱更新可以做到在不刷新瀏覽器的前提下更新頁面。
安裝webpack-dev-server npm install webpack-dev-server --g npm install webpack-dev-serve --save-dev 配置webpack.config.js文件 const webpack=require('webpack');//引入webpack entry:__dirname+'/src/main.js', output:{ publicPath:'/dist',//必須加publicPath path:__dirname+'/dist', filename:'bundle.js' }, devServer:{ host:'localhost', port:'8080', open:true//自動拉起瀏覽器 hot:true,//熱載入 //hotOnly:true }, plugins:[ //熱更新插件 new webpack.HotModuleReplacementPlugin() ]
但是通過日誌發現頁面先熱更新然後又自動刷新,這和自動刷新是一樣的。
如果只需要觸發HMR,可以再加個參數hotOnly:true,這時候只有熱更新,禁用了自動刷新功能。
如果需要自動刷新就不需要設置熱更新。
熱跟新必須有以下5點:
1.引入webpack
2.output里加publicPath
3.devServer中增加hot:true
4.devServer中增加hotOnly:true
5.在plugins中配置 new webpack.HotModuleReplacementPlugin()
————————————————
11、為什麼普通for循環的性能遠遠高於forEach的性能,請解釋其中的原因。
總結如下: 1.如果只是遍歷集合或者數組,用foreach好些,快些,因為for每遍歷一次都要判斷一下條件。 2.如果對集合中的值進行修改,就要用for循環了。其實foreach的內部原理其實也是Iterator,但它不能像Iterator
一樣可以人為的控制,而且也不能調用iterator.remove();更不能使用下標來訪問每個元素,所以不能用於增加,
刪除等複雜的操作。
———————————————————————————————————–
for循環和foreach的區別
關於for循環和foreach的區別,你真的知道,用了那麼多年使用起來已經很熟悉了,可突然問我講講這兩的區別,一下還真把我給卡住了一下,
下面從源碼的角度簡單分析一下吧;
for循環的使用
for循環通過下標的方式,對集合中指定位置進行操作,每次遍歷會執行判斷條件 i<list.size(),滿足則繼續執行,執行完一次i++;
[java] view plain copy for(int i=0;i<list.size();i++) { System.out.println(i + ":" + list.get(i)); }
也就是說,即使在for循環中對list的元素remove和add也是可以的,因為添加或刪除後list中元素個數變化,繼續循環會再次判斷i<list.size(); 也就是說list.size()值也發生了變化,所以
是可行的,具體操作如下程式碼
[java] view plain copy for (int i = 0; i < list.size(); i++) { if (i == 3) { list.add("中間插入的一個字元串"); } if (i == 5) { { list.remove(6); } } System.out.println(i + ":" + list.get(i)); }
增強for循環:foreach循環的原理
同樣地,使用foreach遍歷上述集合,注意foreach是C#中的寫法,在Java中寫法依然是for (int i : list)
寫法for(String str : list)
查看文檔可知,foreach除了可以遍曆數組,還可以用於遍歷所有實現了Iterable<T>介面的對象。
用普通for循環的方式模擬實現一個foreach,由於List實現了Iterable<T>,
過程如下:首先通過iterator()方法獲得一個集合的迭代器,然後每次通過游標的形式依次判斷是否有下一個元素,如果有通過 next()方法則可以取出。 注意:
執行完next()方法,游標向後移一位,只能後移,不能前進。
用傳統for循環的方式模擬 增強for循環

和for循環的區別在於,它對索引的邊界值只會計算一次。所以在foreach中對集合進行添加或刪掉會導致錯誤,拋出異常java.util.ConcurrentModificationException
[java] view plain copy private static void testForeachMethod(ArrayList<String> list) { int count = 0; // 記錄index for (String str : list) { System.out.println(str); count++; if (count == 3) { // foreach中修改集合長度會拋出異常 // list.add("foreach中插入的ABC"); } } }

具體可以從源碼的角度進行理解
1.首先是調用iterator()方法獲得一個集合迭代器


初始化時
expectedModCount記錄修改後的個數,當迭代器能檢測到expectedModCount是否有過修改


在創建迭代器之後,除非通過迭代器自身的 remove
或 add
方法從結構上對列表進行修改,否則在任何時間以任何方式對列表進行修改,迭代器都會拋出
ConcurrentModificationException
。因此,面對並發的修改,迭代器很快就會完全失敗,而不是冒著在將來某個不確定時間發生任意不確定行為的風險。
注意,迭代器的快速失敗行為無法得到保證,因為一般來說,不可能對是否出現不同步並發修改做出任何硬性保證。快速失敗迭代器會盡最大努力拋出 ConcurrentModificationException
。
因此,為提高這類迭代器的正確性而編寫一個依賴於此異常的程式是錯誤的做法:迭代器的快速失敗行為應該僅用於檢測 bug。
12、介紹下npm模組安裝機制,
為什麼輸入npm install就可以自動安裝對應的模組?
1. npm 模組安裝機制:
- 發出
npm install
命令 - 查詢node_modules目錄之中是否已經存在指定模組
- npm 向 registry 查詢模組壓縮包的網址
- 下載壓縮包,存放在根目錄下的
.npm
目錄里 - 解壓壓縮包到當前項目的
node_modules
目錄 - 若存在,不再重新安裝
- 若不存在
2. npm 實現原理
輸入 npm install 命令並敲下回車後,會經歷如下幾個階段(以 npm 5.5.1 為例):
- 執行工程自身 preinstall
當前 npm 工程如果定義了 preinstall 鉤子此時會被執行。
- 確定首層依賴模組
首先需要做的是確定工程中的首層依賴,也就是 dependencies 和 devDependencies 屬性中直接指定的模組(假設此時沒有添加 npm install 參數)。
工程本身是整棵依賴樹的根節點,每個首層依賴模組都是根節點下面的一棵子樹,npm 會開啟多進程從每個首層依賴模組開始逐步尋找更深層級的節點。
- 獲取模組
獲取模組是一個遞歸的過程,分為以下幾步:
- 獲取模組資訊。在下載一個模組之前,首先要確定其版本,這是因為 package.json 中往往是 semantic version(semver,語義化版本)。此時如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有該模組資訊直接拿即可,如果沒有則從倉庫獲取。如 packaeg.json 中某個包的版本是 ^1.1.0,npm 就會去倉庫中獲取符合 1.x.x 形式的最新版本。
- 獲取模組實體。上一步會獲取到模組的壓縮包地址(resolved 欄位),npm 會用此地址檢查本地快取,快取中有就直接拿,如果沒有則從倉庫下載。
- 查找該模組依賴,如果有依賴則回到第1步,如果沒有則停止。
- 模組扁平化(dedupe)
上一步獲取到的是一棵完整的依賴樹,其中可能包含大量重複模組。比如 A 模組依賴於 loadsh,B 模組同樣依賴於 lodash。在 npm3 以前會嚴格按照依賴樹的結構進行安裝,因此會造成模組冗餘。
從 npm3 開始默認加入了一個 dedupe 的過程。它會遍歷所有節點,逐個將模組放在根節點下面,也就是 node-modules 的第一層。當發現有重複模組時,則將其丟棄。
這裡需要對重複模組進行一個定義,它指的是模組名相同且 semver 兼容。每個 semver 都對應一段版本允許範圍,如果兩個模組的版本允許範圍存在交集,那麼就可以得到一個兼容版本,而不必版本號完全一致,這可以使更多冗餘模組在 dedupe 過程中被去掉。
比如 node-modules 下 foo 模組依賴 lodash@^1.0.0,bar 模組依賴 lodash@^1.1.0,則 ^1.1.0 為兼容版本。
而當 foo 依賴 lodash@^2.0.0,bar 依賴 lodash@^1.1.0,則依據 semver 的規則,二者不存在兼容版本。會將一個版本放在 node_modules 中,另一個仍保留在依賴樹里。
舉個例子,假設一個依賴樹原本是這樣:
node_modules — foo —- lodash@version1
— bar —- lodash@version2
假設 version1 和 version2 是兼容版本,則經過 dedupe 會成為下面的形式:
node_modules — foo
— bar
— lodash(保留的版本為兼容版本)
假設 version1 和 version2 為非兼容版本,則後面的版本保留在依賴樹中:
node_modules — foo — lodash@version1
— bar —- lodash@version2
- 安裝模組
這一步將會更新工程中的 node_modules,並執行模組中的生命周期函數(按照 preinstall、install、postinstall 的順序)。
- 執行工程自身生命周期
當前 npm 工程如果定義了鉤子此時會被執行(按照 install、postinstall、prepublish、prepare 的順序)。
最後一步是生成或更新版本描述文件,npm install 過程完成。