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技术人才。