引言
半月刊第二期來啦,這段時間 Daily-Interview-Question 新增了 10 道高頻面試題,今天就把最近半月彙總的面試題和部分答案發給大家,幫助大家查漏補缺。
歡迎 PR 你認為不錯的面試題,歡迎在專案 Issue 區留下你的答案,如有問題歡迎討論。
專案地址是:Daily-Interview-Question
第 15 題:簡單講解一下 HTTP2 的多路複用
在 HTTP/1 中,每次請求都會建立一次TCP連線,也就是我們常說的3次握手4次揮手,這在一次請求過程中佔用了相當長的時間,即使開啟了 Keep-Alive ,解決了多次連線的問題,但是依然有兩個效率上的問題:
- 第一個:序列的檔案傳輸。當請求a檔案時,b檔案只能等待,等待a連線到伺服器、伺服器處理檔案、伺服器返回檔案,這三個步驟。我們假設這三步用時都是1秒,那麼a檔案用時為3秒,b檔案傳輸完成用時為6秒,依此類推。(注:此項計算有一個前提條件,就是瀏覽器和伺服器是單通道傳輸)
- 第二個:連線數過多。我們假設Apache設定了最大併發數為300,因為瀏覽器限制,瀏覽器發起的最大請求數為6(Chrome),也就是伺服器能承載的最高併發為50,當第51個人訪問時,就需要等待前面某個請求處理完成。
HTTP2採用二進位制格式傳輸,取代了HTTP1.x的文字格式,二進位制格式解析更高效。 多路複用代替了HTTP1.x的序列和阻塞機制,所有的相同域名請求都通過同一個TCP連線併發完成。在HTTP1.x中,併發多個請求需要多個TCP連線,瀏覽器為了控制資源會有6-8個TCP連線都限制。 HTTP2中
- 同域名下所有通訊都在單個連線上完成,消除了因多個 TCP 連線而帶來的延時和記憶體消耗。
- 單個連線上可以並行交錯的請求和響應,之間互不干擾
第 16 題:談談你對 TCP 三次握手和四次揮手的理解
第 17 題:A、B 機器正常連線後,B 機器突然重啟,問 A 此時處於 TCP 什麼狀態
如果A 與 B 建立了正常連線後,從未相互發過資料,這個時候 B 突然機器重啟,問 A 此時處於 TCP 什麼狀態?如何消除伺服器程式中的這個狀態?(超綱題,瞭解即可)
因為B會在重啟之後進入tcp狀態機的listen狀態,只要當a重新傳送一個資料包(無論是syn包或者是應用資料),b端應該會主動傳送一個帶rst位的重置包來進行連線重置,所以a應該在syn_sent狀態。
第 18 題:React 中 setState 什麼時候是同步的,什麼時候是非同步的?
在 React 中,如果是由 React 引發的事件處理(比如通過 onClick 引發的事件處理),呼叫 setState 不會同步更新 this.state,除此之外的 setState 呼叫會同步執行 this.state。所謂“除此之外”,指的是繞過 React 通過 addEventListener 直接新增的事件處理函式,還有通過 setTimeout/setInterval 產生的非同步呼叫。
**原因:**在 React 的 setState 函式實現中,會根據一個變數 isBatchingUpdates 判斷是直接更新 this.state 還是放到佇列中回頭再說,而 isBatchingUpdates 預設是 false,也就表示 setState 會同步更新 this.state,但是,有一個函式 batchedUpdates,這個函式會把 isBatchingUpdates 修改為t rue,而當 React 在呼叫事件處理函式之前就會呼叫這個 batchedUpdates,造成的後果就是由 React 控制的事件處理過程 setState 不會同步更新 this.state。
第 19 題:React setState 筆試題,下面的程式碼輸出什麼?
class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}
componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 1 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 2 次 log
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 3 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 4 次 log
}, 0);
}
render() {
return null;
}
};
複製程式碼
解析:
1、第一次和第二次都是在 react 自身生命週期內,觸發時 isBatchingUpdates 為 true,所以並不會直接執行更新 state,而是加入了 dirtyComponents,所以列印時獲取的都是更新前的狀態 0。
2、兩次 setState 時,獲取到 this.state.val 都是 0,所以執行時都是將 0 設定成 1,在 react 內部會被合併掉,只執行一次。設定完成後 state.val 值為 1。
3、setTimeout 中的程式碼,觸發時 isBatchingUpdates 為 false,所以能夠直接進行更新,所以連著輸出 2,3。
輸出: 0 0 2 3
第 20 題:介紹下 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 模組依賴於 lodash,B 模組同樣依賴於 lodash。在 npm3 以前會嚴格按照依賴樹的結構進行安裝,因此會造成模組冗餘。
從 npm3 開始預設加入了一個 dedupe 的過程。它會遍歷所有節點,逐個將模組放在根節點下面,也就是 node-modules 的第一層。當發現有重複模組時,則將其丟棄。
這裡需要對重複模組進行一個定義,它指的是模組名相同且 semver 相容。每個 semver 都對應一段版本允許範圍,如果兩個模組的版本允許範圍存在交集,那麼就可以得到一個相容版本,而不必版本號完全一致,這可以使更多冗餘模組在 dedupe 過程中被去掉。
- 安裝模組
這一步將會更新工程中的 node_modules,並執行模組中的生命週期函式(按照 preinstall、install、postinstall 的順序)。
- 執行工程自身生命週期
當前 npm 工程如果定義了鉤子此時會被執行(按照 install、postinstall、prepublish、prepare 的順序)。
最後一步是生成或更新版本描述檔案,npm install 過程完成。
第 21 題:有以下 3 個判斷陣列的方法,請分別介紹它們之間的區別和優劣
Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()
解析:
1. Object.prototype.toString.call()
每一個繼承 Object 的物件都有 toString
方法,如果 toString
方法沒有重寫的話,會返回 [Object type]
,其中 type 為物件的型別。但當除了 Object 型別的物件外,其他型別直接使用 toString
方法時,會直接返回都是內容的字串,所以我們需要使用call或者apply方法來改變toString方法的執行上下文。
const an = ['Hello','An'];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[object Array]"
複製程式碼
這種方法對於所有基本的資料型別都能進行判斷,即使是 null 和 undefined 。
Object.prototype.toString.call('An') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({name: 'An'}) // "[object Object]"
複製程式碼
Object.prototype.toString.call()
常用於判斷瀏覽器內建物件。
2. instanceof
instanceof
的內部機制是通過判斷物件的原型鏈中是不是能找到型別的 prototype
。
使用 instanceof
判斷一個物件是否為陣列,instanceof
會判斷這個物件的原型鏈上是否會找到對應的 Array
的原型,找到返回 true
,否則返回 false
。
[] instanceof Array; // true
複製程式碼
但 instanceof
只能用來判斷物件型別,原始型別不可以。並且所有物件型別 instanceof Object 都是 true。
[] instanceof Object; // true
複製程式碼
3. Array.isArray()
-
功能:用來判斷物件是否為陣列
-
instanceof 與 isArray
當檢測Array例項時,
Array.isArray
優於instanceof
,因為Array.isArray
可以檢測出iframes
var iframe = document.createElement('iframe'); document.body.appendChild(iframe); xArray = window.frames[window.frames.length-1].Array; var arr = new xArray(1,2,3); // [1,2,3] // Correctly checking for Array Array.isArray(arr); // true Object.prototype.toString.call(arr); // true // Considered harmful, because doesn't work though iframes arr instanceof Array; // false 複製程式碼
-
Array.isArray()
與Object.prototype.toString.call()
Array.isArray()
是ES5新增的方法,當不存在Array.isArray()
,可以用Object.prototype.toString.call()
實現。if (!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; } 複製程式碼
第 22 題:介紹下重繪和迴流(Repaint & Reflow),以及如何進行優化
解析:
1. 瀏覽器渲染機制
- 瀏覽器採用流式佈局模型(
Flow Based Layout
) - 瀏覽器會把
HTML
解析成DOM
,把CSS
解析成CSSOM
,DOM
和CSSOM
合併就產生了渲染樹(Render Tree
)。 - 有了
RenderTree
,我們就知道了所有節點的樣式,然後計算他們在頁面上的大小和位置,最後把節點繪製到頁面上。 - 由於瀏覽器使用流式佈局,對
Render Tree
的計算通常只需要遍歷一次就可以完成,但table及其內部元素除外,他們可能需要多次計算,通常要花3倍於同等元素的時間,這也是為什麼要避免使用table佈局的原因之一。
2. 重繪
由於節點的幾何屬性發生改變或者由於樣式發生改變而不會影響佈局的,稱為重繪,例如outline
, visibility
, color
、background-color
等,重繪的代價是高昂的,因為瀏覽器必須驗證DOM樹上其他節點元素的可見性。
3. 迴流
迴流是佈局或者幾何屬性需要改變就稱為迴流。迴流是影響瀏覽器效能的關鍵因素,因為其變化涉及到部分頁面(或是整個頁面)的佈局更新。一個元素的迴流可能會導致了其所有子元素以及DOM中緊隨其後的節點、祖先節點元素的隨後的迴流。
<body>
<div class="error">
<h4>我的元件</h4>
<p><strong>錯誤:</strong>錯誤的描述…</p>
<h5>錯誤糾正</h5>
<ol>
<li>第一步</li>
<li>第二步</li>
</ol>
</div>
</body>
複製程式碼
在上面的HTML片段中,對該段落(<p>
標籤)迴流將會引發強烈的迴流,因為它是一個子節點。這也導致了祖先的迴流(div.error
和body
– 視瀏覽器而定)。此外,<h5>
和<ol>
也會有簡單的迴流,因為這些節點在DOM中迴流元素之後。大部分的迴流將導致頁面的重新渲染。
迴流必定會發生重繪,重繪不一定會引發迴流。
4. 瀏覽器優化
現代瀏覽器大多都是通過佇列機制來批量更新佈局,瀏覽器會把修改操作放在佇列中,至少一個瀏覽器重新整理(即16.6ms)才會清空佇列,但當你獲取佈局資訊的時候,佇列中可能有會影響這些屬性或方法返回值的操作,即使沒有,瀏覽器也會強制清空佇列,觸發迴流與重繪來確保返回正確的值。
主要包括以下屬性或方法:
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
clientTop
、clientLeft
、clientWidth
、clientHeight
width
、height
getComputedStyle()
getBoundingClientRect()
所以,我們應該避免頻繁的使用上述的屬性,他們都會強制渲染重新整理佇列。
5. 減少重繪與迴流
- 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
這些,還是會引起迴流重繪的,不過它還是可以提升這些動畫的效能。
-
JavaScript
-
避免頻繁操作樣式,最好一次性重寫
style
屬性,或者將樣式列表定義為class
並一次性更改class
屬性。 -
避免頻繁操作DOM,建立一個
documentFragment
,在它上面應用所有DOM操作
,最後再把它新增到文件中。 -
避免頻繁讀取會引發迴流/重繪的屬性,如果確實需要多次使用,就用一個變數快取起來。
-
對具有複雜動畫的元素使用絕對定位,使它脫離文件流,否則會引起父元素及後續元素頻繁迴流。
-
第 23 題:介紹下觀察者模式和訂閱-釋出模式的區別,各自適用於什麼場景
解析:
聯絡
釋出-訂閱模式是觀察者模式的一種變體。釋出-訂閱只是把一部分功能抽象成一個獨立的ChangeManager。
意圖
都是某個物件(subject, publisher)改變,使依賴於它的多個物件(observers, subscribers)得到通知。
區別與適用場景
總的來說,釋出-訂閱模式適合更復雜的場景。
在「一對多」的場景下,釋出者的某次更新只想通知它的部分訂閱者?
在「多對一」或者「多對多」場景下。一個訂閱者依賴於多個釋出者,某個釋出者更新後是否需要通知訂閱者?還是等所有釋出者都更新完畢再通知訂閱者?
這些邏輯都可以放到ChangeManager裡。
第 24 題:聊聊 Redux 和 Vuex 的設計思想
歡迎在 Issue 區留下你的答案。
交流
進階系列文章彙總如下,內有優質前端資料,覺得不錯點個star。
我是木易楊,網易高階前端工程師,跟著我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!