事件机制
- 事件触发的三个阶段:
- window 往事件触发处传播,遇到注册的捕获事件会触发
- 传播到事件触发处时触发注册的事件
- 从事件触发处往 window 传播,遇到注册的冒泡事件会触发
- 注意:若给一个目标节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。
- 注册事件:
-
addEventListener
:参数解析:(事件名称,执行体,布尔值(决定是捕获事件还是冒泡事件,默认为冒泡即false)) - 阻止事件:
stopPropagation
来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件;stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。
node.addEventListener( 'click', event => { event.stopImmediatePropagation() console.log('冒泡') }, false ) // 点击 node 只会执行上面的函数,该函数不会执行 node.addEventListener( 'click', event => { console.log('捕获 ') }, true )
-
- 事件代理
- 如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上
- 优点: 节省内存、不需要给子节点注销事件
跨域
- 为什么存在跨域?因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。(想深究为什么,请自行百度)
- 跨域方式:
- jsonp:JSONP 的原理很简单,就是利用
<script>
标签没有跨域限制的漏洞。通过<script>
标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。操作简单,兼容性好,但只接受get请求。- 代码实现:
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:需要前后端同时支持,IE 8 和 9 需要通过 XDomainRequest 来实现。
- 浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
- 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
- document.domain:只能用于二级域名相同的情况下,eg:a.test.com 和 b.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('验证通过') } })
- jsonp:JSONP 的原理很简单,就是利用
Event loop
- 众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
- 不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。
- 微任务包括 process.nextTick ,promise ,Object.observe ,MutationObserver
- 宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
- 很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。
- test
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
- Event Loop的执行顺序:
- 执行同步代码,这属于宏任务
- 执行栈为空,查询是否有微任务需要执行
- 执行所有微任务
- 必要的话渲染 UI
- 然后开始下一轮 Event loop,执行宏任务中的异步代码
存储
特性 | cookie | localStorage | sessionStorage | indexDB |
---|---|---|---|---|
数据生命周期 | 一般由服务器生成,可以设置过期时间 | 除非被清理,否则一直存在 | 页面关闭就清理 | 除非被清理,否则一直存在 |
数据存储大小 | 4K | 5M | 5M | 无限 |
与服务端通信 | 每次都会携带在 header 中,对于请求性能影响 | 不参与 | 不参与 | 不参与 |
如果没有大量数据存储需求的话,可以使用 localStorage 和 sessionStorage 。对于不怎么改变的数据尽量使用 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 已经启动了。同时,在 Cache 中也可以发现我们所需的文件已被缓存。
- 当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的
浏览器的渲染机制
- 渲染步骤:
- 处理 HTML 并构建 DOM 树。
- 处理 CSS 构建 CSSOM 树。
- 将 DOM 与 CSSOM 合并成一个渲染树。
- 根据渲染树来布局,计算每个节点的位置。
- 调用 GPU 绘制,合成图层,显示在屏幕上
- 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢
- 当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM。
- Load 和 DOMContentLoaded 区别
- Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。
- DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载。
- 图层
- 一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用。
- 常见可以生成新图层的属性:
- 3D 变换:translate3d、translateZ
- will-change
- video、iframe 标签
- 通过动画实现的 opacity 动画转换
- position: fixed
- 重绘(Repaint)和回流(Reflow)
- 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘。
- 回流是布局或者几何属性需要改变就称为回流。
- 回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流。
- 可能会导致Reaint & Reflow
- 改变 window 大小
- 改变字体
- 添加或删除样式
- 文字改变
- 定位或者浮动
- 盒模型
- Reaint & Reflow与Event loop道不尽的关系:
- 当 Event loop 执行完 Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
- 然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
- 判断是否触发了 media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行 requestAnimationFrame 回调
- 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
- 更新界面
- 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。