作者 | 木易楊
責編 | 伍杏玲
第 1 題:簡單講解一下 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 連接而帶來的延時和內存消耗。
- 單個連接上可以並行交錯的請求和響應,之間互不干擾
詳解:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/14
第 2 題:談談你對 TCP 三次握手和四次揮手的理解
詳解:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/15
第 3 題:A、B 機器正常連接後,B 機器突然重啟,問 A 此時處於 TCP 什麼狀態
如果A 與 B 建立了正常連接後,從未相互發過數據,這個時候 B 突然機器重啟,問 A 此時處於 TCP 什麼狀態?如何消除服務器程序中的這個狀態?(超綱題,瞭解即可)
因為B會在重啟之後進入tcp狀態機的listen狀態,只要當a重新發送一個數據包(無論是syn包或者是應用數據),b端應該會主動發送一個帶rst位的重置包來進行連接重置,所以a應該在syn_sent狀態。
詳解:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/21
第 4 題: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。
詳解:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/17
第 5 題: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
詳解:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/18
第 6 題:介紹下 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 過程完成。
詳解:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/22
第 7 題:有以下 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]';
};
}
詳解:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/23
第 8 題:介紹下重繪和迴流(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操作,最後再把它添加到文檔中。
- 避免頻繁讀取會引發迴流/重繪的屬性,如果確實需要多次使用,就用一個變量緩存起來。
- 對具有複雜動畫的元素使用絕對定位,使它脫離文檔流,否則會引起父元素及後續元素頻繁迴流。
詳解:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/24
第 9 題:介紹下觀察者模式和訂閱-發佈模式的區別,各自適用於什麼場景
解析:
聯繫
發佈-訂閱模式是觀察者模式的一種變體。發佈-訂閱只是把一部分功能抽象成一個獨立的ChangeManager。
意圖
都是某個對象(subject, publisher)改變,使依賴於它的多個對象(observers, subscribers)得到通知。
區別與適用場景
總的來說,發佈-訂閱模式適合更復雜的場景。
在「一對多」的場景下,發佈者的某次更新只想通知它的部分訂閱者?
在「多對一」或者「多對多」場景下。一個訂閱者依賴於多個發佈者,某個發佈者更新後是否需要通知訂閱者?還是等所有發佈者都更新完畢再通知訂閱者?
這些邏輯都可以放到ChangeManager裡。
詳解:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/25
第 10 題:聊聊 Redux 和 Vuex 的設計思想
歡迎在 Issue 區留下你的答案。
作者簡介:木易楊,網易高級前端工程師,跟著我每週重點攻克一個前端面試重難點。讓我帶你走進高級前端的世界,在進階的路上,共勉!
進階系列文章彙總如下,內有優質前端資料:
https://github.com/yygmind/blog
聲明:本文系作者投稿,版權歸作者所有。