IT培訓-高端面授IT培訓機構
          云和教育:云和數據集團高端IT職業教育品牌
          • 國家級
            全民數字素養與技能培訓基地
          • 河南省
            第一批產教融合型企業建設培育單位
          • 鄭州市
            數字技能人才(碼農)培養評價聯盟

          收藏 | 10道瀏覽器面試題解析

          • 發布時間:
            2020-12-16
          • 版權所有:
            云和教育
          • 分享:
          前言
          Preface

          想要成為一名合格的前端工程師,掌握相關瀏覽器的工作原理是必備的,這樣子才會有一個完整知識體系,要是「能參透瀏覽器的工作原理,你就能解決80%的前端難題」。
          今天總結了10道瀏覽器面試題及解析,作為前端開發工程師的你趕緊來看看吧!

          1. 常見的瀏覽器內核有哪些?
          2. 瀏覽器的主要組成部分是什么?

          1. 「用戶界面」?– 包括地址欄、前進/后退按鈕、書簽菜單等。
          2. 「瀏覽器引擎」?– 在用戶界面和呈現引擎之間傳送指令。
          3. 「呈現引擎」?– 負責顯示請求的內容。如果請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,并將解析后的內容顯示在屏幕上。
          4. 「網絡」?– 用于網絡調用,比如 HTTP 請求。
          5. 「用戶界面后端」?-用于繪制基本的窗口小部件,比如組合框和窗口。
          6. 「JavaScript 解釋器」– 用于解析和執行 JavaScript 代碼。
          7. 「數據存儲」?– 這是持久層。瀏覽器需要在硬盤上保存各種數據,例如 Cookie。新的 HTML 規范 (HTML5) 定義了“網絡數據庫”,這是一個完整(但是輕便)的瀏覽器內數據庫。
          值得注意的是,和大多數瀏覽器不同,Chrome 瀏覽器的每個標簽頁都分別對應一個呈現引擎實例。每個標簽頁都是一個獨立的進程。
          3. 為什么JavaScript是單線程的,與異步沖突嗎

          補充:JS中其實是沒有線程概念的,所謂的單線程也只是相對于多線程而言。JS的設計初衷就沒有考慮這些,針對JS這種不具備并行任務處理的特性,我們稱之為“單線程”。

          JS單線程是指一個瀏覽器進程中只有一個JS的執行線程,同一時刻內只會有一段代碼在執行。

          舉個通俗例子,假設JS支持多線程操作的話,JS可以操作DOM,那么一個線程在刪除DOM,另外一個線程就在獲取DOM數據,這樣子明顯不合理,這算是證明之一。

          來看段代碼??

          function foo() { ? ?console.log("first");
          setTimeout(( function(){ ? ? ? ?console.log( 'second' );
          }),5);
          }
          for (var i = 0; i < 1000000; i++) {
          foo();
          }復制代碼

          打印結果就是首先是很多個first,然后再是second。

          異步機制是瀏覽器的兩個或以上常駐線程共同完成的,舉個例子,比如異步請求由兩個常駐線程,JS執行線程和事件觸發線程共同完成的。

          • JS執行線程發起異步請求(瀏覽器會開啟一個HTTP請求線程來執行請求,這時JS的任務完成,繼續執行線程隊列中剩下任務)
          • 然后在未來的某一時刻事件觸發線程監視到之前的發起的HTTP請求已完成,它就會把完成事件插入到JS執行隊列的尾部等待JS處理
          再比如定時器觸發(settimeout和setinterval) 是由「瀏覽器的定時器線程」執行的定時計數,然后在定時時間把定時處理函數的執行請求插入到JS執行隊列的尾端(所以用這兩個函數的時候,實際的執行時間是大于或等于指定時間的,不保證能準確定時的)。
          所以這么說,JS單線程與異步更多是瀏覽器行為,之間不沖突。
          4. CSS加載會造成阻塞嗎

          先給出結論

          • CSS不會阻塞DOM解析,但會阻塞DOM渲染。
          • CSS會阻塞JS執行,并不會阻塞JS文件下載
          先講一講CSSOM作用
          • 第一個是提供給 JavaScript 操作樣式表的能力
          • 第二個是為布局樹的合成提供基礎的樣式信息
          • 這個 CSSOM 體現在 DOM 中就是document.styleSheets。
          由之前講過的瀏覽器渲染流程我們可以看出:
          DOM 和 CSSOM通常是并行構建的,所以「CSS 加載不會阻塞 DOM 的解析」。
          然而由于Render Tree 是依賴DOM Tree和 CSSOM Tree的,所以它必須等到兩者都加載完畢后,完成相應的構建,才開始渲染,因此,「CSS加載會阻塞DOM渲染」。
          由于 JavaScript 是可操縱 DOM 和 css 樣式 的,如果在修改這些元素屬性同時渲染界面(即 JavaScript 線程和 UI 線程同時運行),那么渲染線程前后獲得的元素數據就可能不一致了。
          因此為了防止渲染出現不可預期的結果,瀏覽器設置?「GUI 渲染線程與 JavaScript 引擎為互斥」的關系。
          有個需要注意的點就是:
          「有時候JS需要等到CSS的下載,這是為什么呢?」
          仔細思考一下,其實這樣做是有道理的,如果腳本的內容是獲取元素的樣式,寬高等CSS控制的屬性,瀏覽器是需要計算的,也就是依賴于CSS。瀏覽器也無法感知腳本內容到底是什么,為避免樣式獲取,因而只好等前面所有的樣式下載完后,再執行JS。
          JS文件下載和CSS文件下載是并行的,有時候CSS文件很大,所以JS需要等待。
          因此,樣式表會在后面的 js 執行前先加載執行完畢,所以「css 會阻塞后面 js 的執行」。
          5. 為什么JS會阻塞頁面加載

          先給出結論??

          • 「JS阻塞DOM解析」,也就會阻塞頁面
          這也是為什么說JS文件放在最下面的原因,那為什么會阻塞DOM解析呢
          你可以這樣子理解:
          由于 JavaScript 是可操縱 DOM 的,如果在修改這些元素屬性同時渲染界面(即 JavaScript 線程和 UI 線程同時運行),那么渲染線程前后獲得的元素數據就可能不一致了。
          因此為了防止渲染出現不可預期的結果,瀏覽器設置?「GUI 渲染線程與 JavaScript 引擎為互斥」的關系。
          當 JavaScript 引擎執行時 GUI 線程會被掛起,GUI 更新會被保存在一個隊列中等到引擎線程空閑時立即被執行。
          當瀏覽器在執行 JavaScript 程序的時候,GUI 渲染線程會被保存在一個隊列中,直到 JS 程序執行完成,才會接著執行。
          因此如果 JS 執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞的感覺。
          另外,如果 JavaScript 文件中沒有操作 DOM 相關代碼,就可以將該 JavaScript 腳本設置為異步加載,通過 async 或 defer 來標記代碼。
          6. defer 和 async 的區別 ?
          • 兩者都是異步去加載外部JS文件,不會阻塞DOM解析
          • Async是在外部JS加載完成后,瀏覽器空閑時,Load事件觸發前執行,標記為async的腳本并不保證按照指定他們的先后順序執行,該屬性對于內聯腳本無作用 (即沒有「src」屬性的腳本)。
          • defer是在JS加載完成后,整個文檔解析完成后,觸發?DOMContentLoaded?事件前執行,如果缺少?src?屬性(即內嵌腳本),該屬性不應被使用,因為這種情況下它不起作用
          7. DOMContentLoaded 與 load 的區別 ?

          • DOMContentLoaded事件觸發時:僅當DOM解析完成后,不包括樣式表,圖片等資源。
          • onload 事件觸發時,頁面上所有的 DOM,樣式表,腳本,圖片等資源已經加載完畢。
          那么也就是先DOMContentLoaded -> load,那么在Jquery中,使用(document).load(callback)監聽的就是load事件。
          那我們可以聊一聊它們與async和defer區別
          帶async的腳本一定會在load事件之前執行,可能會在DOMContentLoaded之前或之后執行。
          • 情況1:HTML 還沒有被解析完的時候,async腳本已經加載完了,那么 HTML 停止解析,去執行腳本,腳本執行完畢后觸發DOMContentLoaded事件
          • 情況2:HTML 解析完了之后,async腳本才加載完,然后再執行腳本,那么在HTML解析完畢、async腳本還沒加載完的時候就觸發DOMContentLoaded事件
          如果 script 標簽中包含 defer,那么這一塊腳本將不會影響 HTML 文檔的解析,而是等到HTML 解析完成后才會執行。而 DOMContentLoaded 只有在 defer 腳本執行結束后才會被觸發。
          • 情況1:HTML還沒解析完成時,defer腳本已經加載完畢,那么defer腳本將等待HTML解析完成后再執行。defer腳本執行完畢后觸發DOMContentLoaded事件
          • 情況2:HTML解析完成時,defer腳本還沒加載完畢,那么defer腳本繼續加載,加載完成后直接執行,執行完畢后觸發DOMContentLoaded事件
          8. 為什么CSS動畫比JavaScript高效

          我覺得這個題目說法上可能就是行不通,不能這么說,如果了解的話,都知道will-change只是一個優化的手段,使用JS改變transform也可以享受這個屬性帶來的變化,所以這個說法上有點不妥。

          所以圍繞這個問題展開話,更應該說建議推薦使用CSS動畫,至于為什么呢,涉及的知識點大概就是重排重繪,合成,這方面的點,我在瀏覽器渲染流程中也提及了。

          盡可能的避免重排和重繪,具體是哪些操作呢,如果非要去操作JS實現動畫的話,有哪些優化的手段呢?

          比如??

          • 使用createDocumentFragment進行批量的 DOM 操作
          • 對于 resize、scroll 等進行防抖/節流處理。
          • rAF優化等等
          剩下的東西就留給你們思考吧,希望我這是拋磚引玉吧(●’?’●)
          9. 能不能實現事件防抖和節流

          函數節流(throttle)

          節流的意思是讓函數有節制地執行,而不是毫無節制的觸發一次就執行一次。什么叫有節制呢?就是在一段時間內,只執行一次。

          規定在一個單位時間內,只能觸發一次函數。如果這個單位時間內觸發多次函數,只有一次生效。

          抓取一個關鍵的點:就是執行的時機。要做到控制執行的時機,我們可以通過「一個開關」,與定時器setTimeout結合完成。

          ?function throttle(fn, delay) { ? ? ? ? ? ?let flag = true,
          timer = null; ? ? ? ? ? ?return function (...args) { ? ? ? ? ? ? ? ?let context = this; ? ? ? ? ? ? ? ?if (!flag) return;
          flag = false;
          clearTimeout(timer)
          timer = setTimeout(() => {
          fn.apply(context, args);
          flag = true;
          }, delay);
          };
          };復制代碼

          函數防抖(debounce)

          在事件被觸發n秒后再執行回調,如果在這n秒內又被觸發,則重新計時。

          核心思想:每次事件觸發都會刪除原有定時器,建立新的定時器。通俗意思就是反復觸發函數,只認最后一次,從最后一次開始計時。

          代碼:

          ?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)
          }
          }復制代碼
          如何使用 debounce 和 throttle 以及常見的坑

          自己造一個 debounce / throttle 的輪子看起來多么誘人,或者隨便找個博文復制過來。「我是建議直接使用 underscore 或 Lodash」?。如果僅需要?_.debounce?和?_.throttle?方法,可以使用 Lodash 的自定義構建工具,生成一個 2KB 的壓縮庫。使用以下的簡單命令即可:
          npm i -g lodash-cli
          npm i -g lodash-clilodash-cli include=debounce,throttle復制代碼
          常見的坑是,不止一次地調用?_.debounce?方法:
          // 錯誤$(window).on('scroll', function() {
          _.debounce(doSomething, 300);
          });// 正確$(window).on('scroll', _.debounce(doSomething, 200));復制代碼
          debounce 方法保存到一個變量以后,就可以用它的私有方法?debounced_version.cancel(),lodash 和 underscore.js 都有效。
          let debounced_version = _.debounce(doSomething, 200);$(window).on(‘scroll’, debounced_version);// 如果需要的話debounced_version.cancel();復制代碼

          適合應用場景

          防抖

          • search搜索,用戶不斷輸入值時,用防抖來節約Ajax請求,也就是輸入框事件。
          • window觸發resize時,不斷的調整瀏覽器窗口大小會不斷的觸發這個事件,用防抖來讓其只觸發一次

          節流

          • 鼠標的點擊事件,比如mousedown只觸發一次
          • 監聽滾動事件,比如是否滑到底部自動加載更多,用throttle判斷
          • 比如游戲中發射子彈的頻率(1秒發射一顆)
          10. 談一談你對requestAnimationFrame(rAF)理解

          正好跟節流有點關系,有點相似處,就準備梳理一下這個知識點。

          「高性能動畫是什么,那它衡量的標準是什么呢?」

          動畫幀率可以作為衡量標準,一般來說畫面在 60fps 的幀率下效果比較好。

          換算一下就是,每一幀要在 16.7ms (16.7 = 1000/60) 內完成渲染。

          我們來看看MDN對它的解釋吧??

          window.requestAnimationFrame() 方法告訴瀏覽器您希望執行動畫并請求瀏覽器在下一次重繪之前調用指定的函數來更新動畫。該方法使用一個回調函數作為參數,這個回調函數會在瀏覽器重繪之前調用。— MDN

          當我們調用這個函數的時候,我們告訴它需要做兩件事:

          1. 我們需要新的一幀;
          2. 當你渲染新的一幀時需要執行我傳給你的回調函數

          rAF與 setTimeout 相比

          rAF(requestAnimationFrame) 最大的優勢是「由系統來決定回調函數的執行時機」。

          具體一點講就是,系統每次繪制之前會主動調用 rAF 中的回調函數,如果系統繪制率是 60Hz,那么回調函數就每16.7ms 被執行一次,如果繪制頻率是75Hz,那么這個間隔時間就變成了 1000/75=13.3ms。

          換句話說就是,rAF 的執行步伐跟著系統的繪制頻率走。它能保證回調函數在屏幕每一次的繪制間隔中只被執行一次(上一個知識點剛剛梳理完「函數節流」),這樣就不會引起丟幀現象,也不會導致動畫出現卡頓的問題。

          另外它可以自動調節頻率。如果callback工作太多無法在一幀內完成會自動降低為30fps。雖然降低了,但總比掉幀好。

          與setTimeout動畫對比的話,有以下幾點優勢

          • 當頁面隱藏或者最小化時,setTimeout仍然在后臺執行動畫,此時頁面不可見或者是不可用狀態,動畫刷新沒有意義,而且浪費CPU。
          • rAF不一樣,當頁面處理未激活的狀態時,該頁面的屏幕繪制任務也會被系統暫停,因此跟著系統步伐走的rAF也會停止渲染,當頁面被激活時,動畫就從上次停留的地方繼續執行,有效節省了 CPU 開銷。

          什么時候調用呢

          規范中似乎是這么去定義的:

          • 在重新渲染前調用。
          • 很可能在宏任務之后不去調用

          這樣子分析的話,似乎很合理嘛,為什么要在重新渲染前去調用呢?因為rAF作為官方推薦的一種做流暢動畫所應該使用的API,做動畫不可避免的去操作DOM,而如果是在渲染后去修改DOM的話,那就只能等到下一輪渲染機會的時候才能去繪制出來了,這樣子似乎不合理。

          rAF在瀏覽器決定渲染之前給你最后一個機會去改變 DOM 屬性,然后很快在接下來的繪制中幫你呈現出來,所以這是做流暢動畫的不二選擇。

          至于宏任務,微任務,這可以說起來就要展開篇幅了,暫時不在這里梳理了。

          rAF與節流相比

          跟?_.throttle(dosomething, 16)?等價。它是高保真的,如果追求更好的精確度的話,可以用瀏覽器原生的 API 。

          可以使用 rAF API 替換 throttle 方法,考慮一下優缺點:

          優點

          • 動畫保持 60fps(每一幀 16 ms),瀏覽器內部決定渲染的最佳時機
          • 簡潔標準的 API,后期維護成本低

          缺點

          • 動畫的開始/取消需要開發者自己控制,不像 ‘.debounce’ 或 ‘.throttle’由函數內部處理。
          • 瀏覽器標簽未激活時,一切都不會執行。
          • 盡管所有的現代瀏覽器都支持 rAF ,IE9,Opera Mini 和 老的 Android 還是需要打補丁。
          • Node.js 不支持,無法在服務器端用于文件系統事件。

          根據經驗,如果 JavaScript 方法需要繪制或者直接改變屬性,我會選擇?requestAnimationFrame,只要涉及到重新計算元素位置,就可以使用它。

          涉及到 AJAX 請求,添加/移除 class (可以觸發 CSS 動畫),我會選擇?_.debounce?或者?_.throttle?,可以設置更低的執行頻率(例子中的200ms 換成16ms)。

          云和數據HTML5全棧精英班,經過多年的技術迭代和項目革新,逐步發展成為集網站、手機應用、小程序、快應用、桌面應用、后臺開發等多領域開發課程,新增Egg、TypeScript、Vue、React、HybridAPP等時下最流行的新技術,結合企業實際用人需求,只為培養更多高端IT技術人才。