目录
DOM的事件模型是什么?
DOM的事件模型(注册事件的方式)分为:
-
脚本模型
-
内联模型(同类一个,后者覆盖前者)
-
动态绑定(同类可多个)
脚本模型
<!-- 脚本模型:⾏内绑定 --> <button onclick="javascrpt:alert('Hello')">Hello1</button>
内联模型
<!-- 内联模型:同⼀个元素的同类事件只能添加⼀个, 如果添加多个则后添加的会覆盖之前添加的 --> <button onclick="sayHello()">Hello2</button> <script> function sayHello() { alert("Hello"); } </script>
动态绑定
<!-- 动态绑定 --> <button id="btn">Hello3</button> <script> var btn = document.getElementById("btn"); /* 1. 这种方式只能给同一个元素添加一个事件 */ btn.onclick = function () { alert("Hello"); } /* 2. 这种方式可以给同⼀个元素添加多个同类事件 */ btn.addEventListener("click", function () { alert("hello1"); }); btn.addEventListener("click", function () { alert("hello2"); }) </script>
DOM的事件流是什么?
事件
事件是HTML⽂档或浏览器窗⼝中发⽣的⼀些特定的交互瞬间。
事件流
⼜称为事件传播,是⻚⾯中接收事件的顺序。DOM2级事件规定的事件流包括了3个阶段:
-
事件捕获阶段(capture phase)
-
处于⽬标阶段(target phase)
-
事件冒泡阶段(bubbling phase)
如上图所示,事件流的触发顺序是:
-
事件捕获阶段,为截获事件提供了机会 1
-
实际的⽬标元素接收到事件
-
事件冒泡阶段,可在这个阶段对事件做出响应
事件冒泡(Event Bubbling)
事件开始由最具体的元素(⽂档中嵌套层次最深的那个节点)接收到后,开始逐级向上传播到较为不具体的节点。
<html>
<head>
<title>Document</title>
</head>
<body>
<button>按钮</button>
</body>
</html>
如果点击了上面页面代码中的 <button>
按钮,那么该 click
点击事件会沿着 DOM 树向上逐级传播,在途经的每个节点上都会发生,具体顺序如下:
-
button 元素
-
body 元素
-
html 元素
-
document 对象
事件捕获(Event Capturing)
事件开始由较为不具体的节点接收后,然后开始逐级向下传播到最具体的元素上。
事件捕获的最大作用在于:事件在到达预定⽬标之前就可以捕获到它。
如果仍以上面那段 HTML 代码为例,当点击按钮后,在事件捕获的过程中,document 对象会首先接收到这个 click
事件,然后再沿着 DOM 树依次向下,直到 <button>
。具体顺序如下:
-
document 对象
-
html 元素
-
body 元素
-
button 元素
说说什么是事件委托?
事件委托,就是利用了事件冒泡的机制,在较上层位置的元素上添加一个事件监听函数,来管理该元素及其所有子孙元素上的某一类的所有事件。
例:
<ul id="list">
<li>111</li>
<li>222</li>
<li>333</li>
</ul>
<script type="text/javascript">
// ⽗元素
var list = document.getElementById('list');// 为⽗元素绑定事件,委托管理它的所有⼦元素的点击事件
list.onclick = function (event) {
var currentTarget = event.target;
if (currentTarget.tagName.toLowerCase() === 'li') {
alert(currentTarget.innerText)
}
}
</script>
适用场景:在绑定大量事件的时候,可以选择事件委托
优点
-
事件委托可以减少事件注册数量,节省内存占⽤!
-
当新增⼦元素时,⽆需再次做事件绑定,因此非常适合动态添加元素
浏览器与新技术
常见的浏览器内核有哪些?
浏览器的内核,通常指的是渲染引擎,但现在JS引擎也成为浏览器的重要部分,所以下表展示了当前一些比较流行的常规浏览器、无头浏览器、以及JS运行时所含的渲染引擎和JS引擎:
浏览器/运行时 | 内核(即渲染引擎) | JS引擎 |
---|---|---|
Chrome | Webkit (Chrome 27) / Blink (Chrome 28+) | V8 |
FireFox | Gecko | SpiderMonkey |
Safari | Webkit | JavaScriptCore |
Edge | EdgeHTML | Chakra (for JavaScript) |
IE | Trident | Chakra (for JScript) |
PhantomJS (无头浏览器) | Webkit | JavaScriptCore |
Puppeteer (无头浏览器) | Webkit (Chrome 27) / Blink (Chrome 28+) | V8 |
Node.js | 无 | V8 |
(1)Chrome
内核 webkit–>blink
(2)opera
Presto(Opera前内核) (已废弃), Opera现已改用Google Chrome的Blink内核。
(3)firefox
内核 :Gecko
(4)IE
内核:Trident
(5)Safari
内核:webki
浏览器是如何进行界面渲染的?
不同的渲染引擎的具体做法稍有差异,但是大体流程都是差不多的,下面以 webkit 的渲染流程来说明:
上图展示的流程是:
-
获取 HTML ⽂件并进⾏解析,生成一棵 DOM 树(DOM Tree)
-
解析 HTML 的同时也会解析 CSS,⽣成样式规则(Style Rules)
-
根据 DOM 树和样式规则,生成一棵渲染树(Render Tree)
-
进行布局(Layout),即为每个节点分配⼀个在屏幕上应显示的确切坐标位置
-
进⾏绘制(Paint),遍历渲染树节点,调⽤ GPU 将元素呈现出来
浏览器是如何解析CSS选择器的?
在生成渲染树的过程中,渲染引擎会根据选择器提供的信息来遍历 DOM 树,找到对应的 DOM 节点后将样式规则附加到上面。
来看一段样式选择器代码、以及一段要应用样式的 HTML:
.mod-nav h3 span { font-size: 16px; }
<div class="mod-nav"> <header> <h3> <span>标题</span> </h3> </header> <div> <ul> <li><a href="#">项目一</a></li> <li><a href="#">项目一</a></li> <li><a href="#">项目一</a></li> </ul> </div> </div>
渲染引擎是怎么根据以上样式选择器去遍历这个 DOM 树的呢?是按照从左往右的选择器顺序去匹配,还是从右往左呢?
为了更直观的观查,我们先将这棵 DOM 树先绘制成图:
然后我们来对比一下两种顺序的匹配:
从左往右:.mod-nav > h3 > span
-
从
.mod-nav
开始遍历⼦节点header
、div
-
然后向各⾃的⼦节点遍历
-
在右侧
div
的分⽀中,当遍历到叶节点a
后,发现不符合规则。则重新回溯到ul
节点,再遍历下⼀个li
-a
从右往左:span > h3 > .mod-nav
-
先找到所有的
span
节点 ,然后基于每⼀个span
再向上查找h3
-
由
h3
再向上查找.mod-nav
的节点 -
最后触及根元素
html
结束该分⽀遍历
可以看到,从右向左的匹配规则可以在第⼀步时就筛选掉⼤量不符合条件的叶节点;⽽从左向右的匹配规则需要消耗大量时间在失败的查找上,这在真实页面中⼀棵 DOM 树的节点成百上千的情况下,这种遍历方式的效率会非常的低,根本不适合采用。
因此,浏览器遵循 “从右往左” 的规则来解析 CSS 选择器!
DOM树是如何构建的?
构建的过程如下:
-
浏览器将接收到的⼆进制数据,按指定编码格式转换为 HTML 字符串
-
开始解析,将 HTML 字符串解析成 Tokens
-
构建节点,对节点添加特定的属性,并通过指针确定节点的⽗、⼦、兄弟关系、以及所属 treeScope
-
通过已确定的节点⽗、⼦、兄弟关系,构建出 DOM 树
下图为以上描述过程的图示:
浏览器重绘与重排的区别是什么?
重排
重排是指部分或整个渲染树需要重新分析,并且节点的尺⼨需要重新计算。
表现为重新⽣成布局,重新排列元素。
重绘
重绘是由于节点的⼏何属性发⽣改变,或由于样式发⽣改变(例如:改变元素背景⾊)。
表现为某些元素的外观被改变。
两者的关系
重绘不⼀定会出现重排,重排必定会出现重绘。
只改变元素的外观不会引发⽹⻚重排;但若浏览器进行重排后,将会重绘受此次重排影响的部分。
重排和重绘的代价都很⾼昂,破坏⽤户体验、让界面显示变迟缓。
但相⽐之下,重排的性能影响会更⼤,在⽆法避免的情况下,⼀般宁可选择代价较⼩的重绘。
如何触发重排和重绘?
改变任意的⽤于构建渲染树的信息,都会引发⼀次重排或重绘。比如:
-
添加、删除、更新 DOM 节点
-
通过
display: none
隐藏 DOM 节点(同时会触发重排和重绘) -
通过
visibility: hidden
隐藏 DOM节点(只会触发重绘,因为没有⼏何变化 ) -
移动 DOM 节点,或是给⻚⾯中的 DOM 节点添加动画
-
添加⼀个样式表,调整样式属性
-
⽤户⾏为,例如:调整浏览器窗⼝的⼤⼩,改变字号,或者滚动页面
如何避免重排或重绘?
主要有三大方式来避免:
-
集中修改样式
-
使用文档碎片(DocumentFragment)
-
将元素提升为合成层
集中修改样式
通常以改 class 的⽅式,实现样式的集中修改。
el.setAttribute('className', isDark ? 'dark' : 'light')
使用文档碎片
通过 document.createDocumentFragment
可创建⼀个游离于 DOM 树外的节点,在该节点上做批量操作后再将它插⼊ DOM 树中,只会引发⼀次重排。
// 创建碎片节点
const fragment = document.createDocumentFragment()
// 多次操作碎片节点
for (let i = 0; i< 10; i++) {
const node = document.createElement("p")
node.innerHTML = i
fragment.appendChild(node)
}
// 一次性添加到 DOM 树中
document.body.appendChild(fragment)
将元素提升为合成层
将元素提升为合成层的最好⽅式是使⽤ CSS 的 will-change
属性:
#target {
will-change: transform;
}
提升为合成层有下列几个优点:
-
合成层的位图会由 GPU 合成,⽐由 CPU 处理更快
-
当需要重绘时只重绘本身,不影响其他层
-
transform
和opacity
不会触发重排和重绘
前端如何实现即时通讯?
基于Web的前端,存在以下几种可实现即时通讯的方式:
-
短轮询
-
Comet
-
SSE
-
WebSocket
短轮询
短轮询就是客户端定时发送请求,获取服务器上的最新数据。不是真正的即时通讯,但一定程度上可以模拟即时通讯的效果。
优缺点:
-
优点:浏览器兼容性好,实现简单
-
缺点:实时性不高,资源消耗高,存在较多无用请求,影响性能
-
适用于短时间用一下
Comet
有两种实现 Comet 的方式:
-
使用 Ajax 长轮询(long-polling)
-
使用 HTTP 长连接(基于 iframe 和 htmlfile 的流)
优缺点:
-
优点:浏览器兼容性好,即时性好,不存在⽆⽤请求
-
缺点:服务器压力较大(维护⻓连接会消耗较多服务器资源)
SSE
服务端推送事件(Server-Sent Event),它是⼀种允许服务端向客户端推送新数据的 HTML5 技术。
优缺点:
-
优点:基于 HTTP,无需太多改造就能使⽤;相比 WebSocket 要简单方便很多
-
缺点:基于⽂本传输,效率没有 WebSocket ⾼;不是严格的双向通信,客户端⽆法复⽤连接来向服务端发送请求,
而是每次都需重新创建新请求
WebSocket socket.io.js
这是基于 TCP 协议的全新、独⽴的协议,作⽤是在服务器和客户端之间建⽴实时的双向通信。
WebSocket 协议与 HTTP 协议保持兼容,但它不会融⼊ HTTP 协议,仅作为 HTML 5 的⼀部分。
优缺点:
-
优点:真正意义上的双向实时通信,性能好、延迟低
-
缺点:由于是独⽴于 HTTP 的协议,因此要使用的话需要对项⽬作改造;使⽤复杂度较⾼,通常需要引⼊成熟的库;并且⽆法兼容低版本的浏览器
HTTP 和 WebSocket 的连接通信比较图:
什么是浏览器的同源策略?
首先,同源是指资源地址的 "协议 + 域名 + 端⼝" 三者都相同,即使两个不同域名指向了同⼀ IP 地址,也被判断为⾮同源。
下面是一些地址的同源判断示例:
了解了什么是同源,再来说同源策略。
同源策略是一种⽤于隔离潜在恶意⽂件的重要安全保护机制,它用于限制从⼀个源加载的⽂档或脚本与来⾃另⼀个源的资源进⾏交互。
在浏览器中,⼤部分内容都受同源策略限制,除了以下三个标签:src
-
<img>
-
<link>
-
script
如何实现跨域?
历史上出现过的跨域⼿段有很多,本章主要介绍目前主流的3种跨域⽅案:
-
JSONP
-
在页面上定义一个方法 fn是可以直接执行
-
script src ='....?callback=fn' fn(参数)
-
-
CORS
-
服务器代理(webpack代理, Nginx反向代理)
-
devServer:{ 开发环境的服务器配制 proxy只能用于开发环境,到了生产后端解决 }
-
-
跨域浏览器
-
html5 postMessage也能跨域发送信息
JSONP
这是一种非常经典的跨域方案,它利用了<script>
标签不受同源策略的限制的特性,实现跨域效果。
优点:
-
实现简单
-
兼容性好
缺点:
-
只支持 GET 请求 (因为
<script>
标签只能发送 GET 请求) -
存在被 XSS 攻击的可能,缺乏安全性保证
-
需要服务端配合改造
实现示例:
// 1. JSONP 发送请求的函数封装
function JSONP({ url, params, callbackKey, callback }) {
// 在参数中指定 callback 名字
params = params || {} params[callbackKey] = 'jsonpCallback'
// 预留 callback
window.jsonpCallback = callback
// 拼接参数字符串
const queryString = Object.keys(params).map(key => `${key}=${params[key]}`).join('&')
// 创建 script 标签
const script = document.createElement('script')
script.setAttribute('src', `${url}?${queryString}`)
// 插⼊ DOM 树
document.body.appendChild(script)
}
// 2. 调用示例
JSONP({
url: 'http://s.weibo.com/ajax/jsonp/suggestion',
params: {
key: 'test'
},
callbackKey: '_cb',
callback(res) {
console.log(res.data)
}
})
CORS
跨域资源共享(CORS),这是⽬前比较主流的跨域解决⽅案,它利用一些额外的 HTTP 响应头来通知浏览器允许访问来自指定 origin 的非同源服务器上的资源。
当在⼀个资源中去请求与本资源所在的服务器有不同协议、域、或端⼝的另一个资源时,就会发起⼀个跨域 HTTP 请求。
如果你⽤的是 Node.js 的 Express 框架,则可以这样来进行设置:
// 创建一个 CORS 中间件
function allowCrossDomain(req, res, next) {
res.header('Access-Control-Allow-Origin', 'http://example.com');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
}
//...
app.configure(function() {
// ...
// 为 Express 配置 CORS 中间件
app.use(allowCrossDomain);
// ...
});
在实际项目中,建议使用一些已经比较成熟的开源中间件。
Nginx反向代理
这是目前最方便,最推荐使用的跨域解决方案。Nginx 本身是⼀款极其强⼤的 Web 服务器,轻量级、启动快、⾼并发。
现在的后端服务程序,通常会使用 Nginx 进行反向代理:
反向代理的原理其实很简单:
Nginx 作为代理服务器,所有客户端的请求都必须先经过 Nginx 的处理,然后再将请求转发给其他后端程序(比如 Node.js 或Java 程序),这样就规避同源策略的影响
下面是一个配置 Nginx 反向代理的示例配置文件:
# 进程, 可更具 cpu 数量调整
worker_processes 1;
events {
# 连接数
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#连接超时时间,服务器会在这个时间过后关闭连接。
keepalive_timeout 10;
# 开启 Gzip 压缩
gzip on;
# 直接请求nginx也是会报跨域错误的这⾥设置允许跨域
# 如果代理地址已经允许跨域则不需要这些, 否则报错(虽然这样nginx跨域就没意义了)
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
# srever模块配置是http模块中的⼀个⼦模块,⽤来定义⼀个虚拟访问主机
server {
listen 80;
server_name localhost;
# 根路径指到index.html
location / {
root html;
index index.html index.htm;
}
# 请求转发:
# 例如 http://localhost/api 的请求会被转发到 http://192.168.0.103:8080
location /api {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.0.103:8080;
}
# 重定向错误⻚⾯到/50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}