前端面试题(四)浏览器篇

事件机制

事件触发三阶段

事件触发有三个阶段javascript

  • window 往事件触发处传播,遇到注册的捕获事件会触发
  • 传播到事件触发处时触发注册的事件
  • 从事件触发处往 window 传播,遇到注册的冒泡事件会触发

事件触发通常来讲会按照上面的顺序进行,可是也有特例,若是给一个目标节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。css

// 如下会先打印冒泡而后是捕获
node.addEventListener('click',(event) =>{
	console.log('冒泡')
},false);
node.addEventListener('click',(event) =>{
	console.log('捕获 ')
},true)
复制代码

注册事件

一般咱们使用 addEventListener 注册事件,该函数的第三个参数能够是布尔值,也能够是对象。对于布尔值 useCapture 参数来讲,该参数默认值为 falseuseCapture 决定了注册的事件是捕获事件仍是冒泡事件。对于对象参数来讲,可使用如下几个属性html

  • capture,布尔值,和 useCapture 做用同样
  • once,布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
  • passive,布尔值,表示永远不会调用 preventDefault

通常来讲,咱们只但愿事件只触发在目标上,这时候可使用 stopPropagation 来阻止事件的进一步传播。一般咱们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也能够阻止捕获事件。stopImmediatePropagation 一样也能实现阻止事件,可是还能阻止该事件目标执行别的注册事件。java

node.addEventListener('click',(event) =>{
	event.stopImmediatePropagation()
	console.log('冒泡')
},false);
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener('click',(event) => {
	console.log('捕获 ')
},true)
复制代码

事件代理

若是一个节点中的子节点是动态生成的,那么子节点须要注册事件的话应该注册在父节点上node

<ul id="ul">
	<li>1</li>
    <li>2</li>
	<li>3</li>
	<li>4</li>
	<li>5</li>
</ul>
<script> let ul = document.querySelector('#ul') ul.addEventListener('click', (event) => { console.log(event.target); }) </script>
复制代码

事件代理的方式相对于直接给目标注册事件来讲,有如下优势web

  • 节省内存
  • 不须要给子节点注销事件

跨域

由于浏览器出于安全考虑,有同源策略。也就是说,若是协议、域名或者端口有一个不一样就是跨域,Ajax 请求会失败。json

咱们能够经过如下几种经常使用方法解决跨域的问题后端

JSONP

JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。经过 <script> 标签指向一个须要访问的地址并提供一个回调函数来接收数据当须要通信时。api

<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
    function jsonp(data) {
    	console.log(data)
	}
</script>    
复制代码

JSONP 使用简单且兼容性不错,可是只限于 get 请求。跨域

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就须要本身封装一个 JSONP,如下是简单实现

function jsonp(url, jsonpCallback, success) {
  let script = document.createElement("script");
  script.src = url;
  script.async = true;
  script.type = "text/javascript";
  window[jsonpCallback] = function(data) {
    success && success(data);
  };
  document.body.appendChild(script);
}
jsonp(
  "http://xxx",
  "callback",
  function(value) {
    console.log(value);
  }
);
复制代码

CORS

CORS须要浏览器和后端同时支持。IE 8 和 9 须要经过 XDomainRequest 来实现。

浏览器会自动进行 CORS 通讯,实现CORS通讯的关键是后端。只要后端实现了 CORS,就实现了跨域。

服务端设置 Access-Control-Allow-Origin 就能够开启 CORS。 该属性表示哪些域名能够访问资源,若是设置通配符则表示全部网站均可以访问资源。

document.domain

该方式只能用于二级域名相同的状况下,好比 a.test.comb.test.com 适用于该方式。

只须要给页面添加 document.domain = 'test.com' 表示二级域名都相同就能够实现跨域

postMessage

这种方式一般用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另外一个页面判断来源并接收消息

// 发送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
    var origin = event.origin || event.originalEvent.origin; 
    if (origin === 'http://test.com') {
        console.log('验证经过')
    }
});
复制代码

Event loop

众所周知 JS 是门非阻塞单线程语言,由于在最初 JS 就是为了和浏览器交互而诞生的。若是 JS 是门多线程的语言话,咱们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另外一个线程中删除节点),固然能够引入读写锁解决这个问题。

JS 在执行的过程当中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。若是遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出须要执行的代码并放入执行栈中执行,因此本质上来讲 JS 中的异步仍是同步行为。

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

console.log('script end');
复制代码

以上代码虽然 setTimeout 延时为 0,其实仍是异步。这是由于 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增长。因此 setTimeout 仍是会在 script end 以后打印。

不一样的任务源会被分配到不一样的 Task 队列中,任务源能够分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

new Promise((resolve) => {
    console.log('Promise')
    resolve()
}).then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout
复制代码

以上代码虽然 setTimeout 写在 Promise 以前,可是由于 Promise 属于微任务而 setTimeout 属于宏任务,因此会有以上的打印。

微任务包括 process.nextTickpromiseObject.observeMutationObserver

宏任务包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

不少人有个误区,认为微任务快于宏任务,实际上是错误的。由于宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。

因此正确的一次 Event loop 顺序是这样的

  1. 执行同步代码,这属于宏任务
  2. 执行栈为空,查询是否有微任务须要执行
  3. 执行全部微任务
  4. 必要的话渲染 UI
  5. 而后开始下一轮 Event loop,执行宏任务中的异步代码

经过上述的 Event loop 顺序可知,若是宏任务中的异步代码有大量的计算而且须要操做 DOM 的话,为了更快的 界面响应,咱们能够把操做 DOM 放入微任务中。

Node 中的 Event loop

Node 中的 Event loop 和浏览器中的不相同。

Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
复制代码

timer

timers 阶段会执行 setTimeoutsetInterval

一个 timer 指定的时间并非准确时间,而是在达到这个时间后尽快执行回调,可能会由于系统正在执行别的事务而延迟。

下限的时间有一个范围:[1, 2147483647] ,若是设定的时间不在这个范围,将被设置为1。

I/O

I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调

idle, prepare

idle, prepare 阶段内部实现

poll

poll 阶段很重要,这一阶段中,系统会作两件事情

  1. 执行到点的定时器
  2. 执行 poll 队列中的事件

而且当 poll 中没有定时器的状况下,会发现如下两件事情

  • 若是 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制
  • 若是 poll 队列为空,会有两件事发生
    • 若是有 setImmediate 须要执行,poll 阶段会中止而且进入到 check 阶段执行 setImmediate
    • 若是没有 setImmediate 须要执行,会等待回调被加入到队列中并当即执行回调

若是有别的定时器须要被执行,会回到 timer 阶段执行回调。

check

check 阶段执行 setImmediate

close callbacks

close callbacks 阶段执行 close 事件

而且在 Node 中,有些状况下的定时器执行顺序是随机的

setTimeout(() => {
    console.log('setTimeout');
}, 0);
setImmediate(() => {
    console.log('setImmediate');
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 由于可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 不然会执行 setTimeout
复制代码

固然在这种状况下,执行顺序是相同的

var fs = require('fs')

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});
// 由于 readFile 的回调在 poll 中执行
// 发现有 setImmediate ,因此会当即跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 因此以上输出必定是 setImmediate,setTimeout
复制代码

上面介绍的都是 macrotask 的执行状况,microtask 会在以上每一个阶段完成后当即执行。

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

// 以上代码在浏览器和 node 中打印状况是不一样的
// 浏览器中必定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2
复制代码

Node 中的 process.nextTick 会先于其余 microtask 执行。

setTimeout(() => {
 console.log("timer1");

 Promise.resolve().then(function() {
   console.log("promise1");
 });
}, 0);

process.nextTick(() => {
 console.log("nextTick");
});
// nextTick, timer1, promise1
复制代码

存储

cookie,localStorage,sessionStorage,indexDB

特性 cookie localStorage sessionStorage indexDB
数据生命周期 通常由服务器生成,能够设置过时时间 除非被清理,不然一直存在 页面关闭就清理 除非被清理,不然一直存在
数据存储大小 4K 5M 5M 无限
与服务端通讯 每次都会携带在 header 中,对于请求性能影响 不参与 不参与 不参与

从上表能够看到,cookie 已经不建议用于存储。若是没有大量数据存储需求的话,可使用 localStoragesessionStorage 。对于不怎么改变的数据尽可能使用 localStorage 存储,不然能够用 sessionStorage 存储。

对于 cookie,咱们还须要注意安全性。

属性 做用
value 若是用于保存用户登陆态,应该将该值加密,不能使用明文的用户标识
http-only 不能经过 JS 访问 Cookie,减小 XSS 攻击
secure 只能在协议为 HTTPS 的请求中携带
same-site 规定浏览器不能在跨域请求中携带 Cookie,减小 CSRF 攻击

Service Worker

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也能够在网络可用时做为浏览器和网络间的代理。它们旨在(除其余以外)使得可以建立有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采起适当的动做。他们还容许访问推送通知和后台同步API。

目前该技术一般用来作缓存文件,提升首屏速度,能够试着来实现这个功能。

// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register("sw.js")
    .then(function(registration) {
      console.log("service worker 注册成功");
    })
    .catch(function(err) {
      console.log("servcie worker 注册失败");
    });
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener("install", e => {
  e.waitUntil(
    caches.open("my-cache").then(function(cache) {
      return cache.addAll(["./index.html", "./index.js"]);
    })
  );
});

// 拦截全部请求事件
// 若是缓存中已经有请求的数据就直接用缓存,不然去请求数据
self.addEventListener("fetch", e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response;
      }
      console.log("fetch source");
    })
  );
});
复制代码

打开页面,能够在开发者工具中的 Application 看到 Service Worker 已经启动了

image

在 Cache 中也能够发现咱们所需的文件已被缓存

image

当咱们从新刷新页面能够发现咱们缓存的数据是从 Service Worker 中读取的

image

渲染机制

浏览器的渲染机制通常分为如下几个步骤

  1. 处理 HTML 并构建 DOM 树。
  2. 处理 CSS 构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,计算每一个节点的位置。
  5. 调用 GPU 绘制,合成图层,显示在屏幕上。

image

在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。而且构建 CSSOM 树是一个十分消耗性能的过程,因此应该尽可能保证层级扁平,减小过分层叠,越是具体的 CSS 选择器,执行速度越慢。

当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方从新开始。也就是说,若是你想首屏渲染的越快,就越不该该在首屏就加载 JS 文件。而且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,因此也能够认为这种状况下,CSS 也会暂停构建 DOM。

image

image

Load 和 DOMContentLoaded 区别

Load 事件触发表明页面中的 DOM,CSS,JS,图片已经所有加载完毕。

DOMContentLoaded 事件触发表明初始的 HTML 被彻底加载和解析,不须要等待 CSS,JS,图片加载。

图层

通常来讲,能够把普通文档流当作一个图层。特定的属性能够生成一个新的图层。不一样的图层渲染互不影响,因此对于某些频繁须要渲染的建议单独生成一个新图层,提升性能。但也不能生成过多的图层,会引发副作用。

经过如下几个经常使用属性能够生成新图层

  • 3D 变换:translate3dtranslateZ
  • will-change
  • videoiframe 标签
  • 经过动画实现的 opacity 动画转换
  • position: fixed

重绘(Repaint)和回流(Reflow)

重绘和回流是渲染步骤中的一小节,可是这两个步骤对于性能影响很大。

  • 重绘是当节点须要更改外观而不会影响布局的,好比改变 color 就叫称为重绘
  • 回流是布局或者几何属性须要改变就称为回流。

回流一定会发生重绘,重绘不必定会引起回流。回流所需的成本比重绘高的多,改变深层次的节点极可能致使父节点的一系列回流。

因此如下几个动做可能会致使性能问题:

  • 改变 window 大小
  • 改变字体
  • 添加或删除样式
  • 文字改变
  • 定位或者浮动
  • 盒模型

不少人不知道的是,重绘和回流其实和 Event loop 有关。

  1. 当 Event loop 执行完 Microtasks 后,会判断 document 是否须要更新。由于浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
  2. 而后判断是否有 resize 或者 scroll ,有的话会去触发事件,因此 resizescroll 事件也是至少 16ms 才会触发一次,而且自带节流功能。
  3. 判断是否触发了 media query
  4. 更新动画而且发送事件
  5. 判断是否有全屏操做事件
  6. 执行 requestAnimationFrame 回调
  7. 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,能够用于懒加载上,可是兼容性很差
  8. 更新界面
  9. 以上就是一帧中可能会作的事情。若是在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。

以上内容来自于 HTML 文档

减小重绘和回流

  • 使用 translate 替代 top

    <div class="test"></div>
    <style> .test { position: absolute; top: 10px; width: 100px; height: 100px; background: red; } </style>
    <script> setTimeout(() => { // 引发回流 document.querySelector('.test').style.top = '100px' }, 1000) </script>
    复制代码
  • 使用 visibility 替换 display: none ,由于前者只会引发重绘,后者会引起回流(改变了布局)

  • 把 DOM 离线后修改,好比:先把 DOM 给 display:none (有一次 Reflow),而后你修改100次,而后再把它显示出来

  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量

    for(let i = 0; i < 1000; i++) {
        // 获取 offsetTop 会致使回流,由于须要去获取正确的值
        console.log(document.querySelector('.test').style.offsetTop)
    }
    复制代码
  • 不要使用 table 布局,可能很小的一个小改动会形成整个 table 的从新布局

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也能够选择使用 requestAnimationFrame

  • CSS 选择符从右往左匹配查找,避免 DOM 深度过深

  • 将频繁运行的动画变为图层,图层可以阻止该节点回流影响别的元素。好比对于 video 标签,浏览器会自动将该节点变为图层。

    image
相关文章
相关标签/搜索