gzl的博客

  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

浏览器的事件处理

发表于 2019-09-07 更新于 2020-02-13 分类于 浏览器

事件流

可以想象画在一张纸上的一组同心圆,如果把手指放在圆心上,那么手指指向的不是一个圆,而是纸上所有的圆。

事件流描述的是从页面中接受事件的顺序。

触发顺序,先捕获,后冒泡

事件冒泡

即事件开始时由最具体的元素接受,然后逐级向上传播到较为不具体的节点。

结构上(非视觉上)嵌套关系的元素,会存在事件冒泡的功能,即同一事件,自子元素冒泡向父元素。(自底向上)。

focus,blur,change,submit,reset,select 等事件不冒泡。

比如下面这个例子,点击.box,会先后打印出 box,content,wrapper。

1
2
3
4
5
<div class="wrapper">
<div class="content">
<div class="box"></div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
let wrapper = document.getElementsByClassName('wrapper')[0];
let content = document.getElementsByClassName('content')[0];
let box = document.getElementsByClassName('box')[0];

wrapper.addEventListener('click', function () {
console.log('wrapper')
}, false)
content.addEventListener('click', function () {
console.log('content')
}, false)
box.addEventListener('click', function () {
console.log('box')
}, false)

事件捕获

结构上(非视觉上)嵌套关系的元素,会存在事件捕获的功能,即同一事件,自父元素捕获至子元素。(自顶向下)。

下面的代码与事件冒泡的代码的不同就是 false 改成了 true,会先后打印出 wrapper,content,box。

1
2
3
4
5
6
7
8
9
wrapper.addEventListener('click', function () {
console.log('wrapper')
}, true)
content.addEventListener('click', function () {
console.log('content')
}, true)
box.addEventListener('click', function () {
console.log('box')
}, true)

结合

先捕获,后冒泡,这里注意一下 boxBubble 和 box 的打印顺序,因为 boxBubble 是先绑定的,所以先执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 冒泡
wrapper.addEventListener('click', function () {
console.log('wrapperBubble')
}, false)
content.addEventListener('click', function () {
console.log('contentBubble')
}, false)
box.addEventListener('click', function () {
console.log('boxBubble')
}, false)

// 捕获
wrapper.addEventListener('click', function () {
console.log('wrapper')
}, true)
content.addEventListener('click', function () {
console.log('content')
}, true)
box.addEventListener('click', function () {
console.log('box')
}, true)

// wrapper
// content
// boxBubble
// box
// contentBubble
// wrapperBubble

取消冒泡

此时控制台只会打印出 boxBubble。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
wrapper.addEventListener('click', function () {
console.log('wrapperBubble')
}, false)
content.addEventListener('click', function () {
console.log('contentBubble')
}, false)
box.addEventListener('click', function (e) {
console.log('boxBubble')
stopBubble(e);
}, false)

function stopBubble(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}

阻止默认事件

  1. return false; 以对象属性的方式注册的事件才生效
  2. event.preventDefault(); W3C标注,IE9以下不兼容
  3. event.returnValue = false; 兼容IE
1
2
3
4
5
6
7
8
9
10
11
12
13
// 第一种方法 return false 对 addEventListener 不生效
document.oncontextmenu = function (e) {
console.log('a');
cancelHandler(e);
}

function cancelHandler(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}

阻止 a 标签的默认事件,点击会回到顶部。

1
2
<!-- void相当于return -->
<a href="javascript:void(false)">百度一下</a>

事件对象

event || window.event

事件源对象:

  1. event.target 火狐只有这个
  2. event.srcElement IE只有这个
  3. chrome上面的都有
1
2
3
4
5
6
7
// 获取事件源对象
let wrapper = document.getElementsByClassName('wrapper')[0];
wrapper.onclick = function (e) {
let event = e || window.event;
let target = event.target || event.srcElement;
console.log(target);
}

事件委托

事件冒泡 + 事件源对象结合,避免过多地遍历 li。

优点:

  1. 性能:不需要循环所有的元素一个个绑定事件
  2. 灵活:当有新的子元素时不需要重新绑定事件
1
2
3
4
5
6
7
8
9
10
11
12
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>10</li>
</ul>
1
2
3
4
5
6
let ul = document.getElementsByTagName('ul')[0];
ul.onclick = function (e) {
let event = e || window.event;
let target = event.target || event.srcElement;
console.log(target.innerText)
}

浏览器的加载时间线

发表于 2019-09-03 更新于 2020-02-13 分类于 浏览器

加载时间线

  1. 创建 Document 对象,开始解析 web 页面。解析HTML元素和他们的文本内容后添加 Element 对象和 Text 节点到文档中。这个阶段 document.readyState = ‘loading’.

  2. 遇到 link 外部 css,创建线程加载,并继续解析完档。

  3. 遇到 script 外部 js,并且没有设置 async、defer,浏览器加载,并阻塞,等待js加载完成并执行该脚本,然后继续解析文档。

  4. 遇到script外部js,并且有设置async、defer,浏览器创建线程加载,并继续解析文档。对于async属性的脚本,脚本加载完后立即执行。(异步禁止使用document.write())

  5. 遇到img等,先正常解析dom结构,然后浏览器异步加载src,并继续解析文档。

  6. 当文档解析完成,document.readyState = ‘interactive’。

  7. 文档解析完成后,所有设置defer的脚本会按照顺序执行。(注意与async的不用,但同样禁止使用document.write())。

  8. document对象触发DOMContentLoaded事件,这也标志着程序执行从同步脚本执行阶段,转化为事件驱动阶段。

  9. 当所有的async的脚本加载完成并执行完后,img等加载完后,document.readyState = ‘complete’,window对象触发load事件。

  10. 此后,异步响应方式处理用户输入,网络事件等。

RN-《高性能JavaScript》

发表于 2019-09-03 更新于 2020-02-06 分类于 读书笔记

第一章 加载和执行

脚本位置

当浏览器遇到 <script> 标签时,浏览器会停止处理页面,先执行 JavaScript 代码,然后继续解析和渲染页面,同样的情况也发生在使用 src 属性加载 JavaScript 的过程中,浏览器必须先花时间下载外链文件中代码,然后解析并执行,在这个过程中,页面渲染和用户交互是完全被阻塞的。

浏览器在解析到 <body> 标签之前,不会渲染页面的任何部分。

推荐将所有的 <script> 标签尽可能放到 <body> 标签的底部,以尽量减少对整个页面下载的影响。

组织脚本

浏览器在解析HTML页面的过程中每遇到一个 <script> 标签,都会因脚本而导致一定的延时,因此最小化延时时间将会明显改善页面的总体性能。

因此下载单个 100KB 的文件将比下载 4 个 25KB 的文件更快。也就是说,减少页面中外链脚本文件的数量将会改善性能。

异步加载js

在页面加载完成后才加载JavaScript代码。

三种方式

  1. defer 异步加载,但要等到 dom 文档全部解析完才会被执行。只有IE能用,也可以将代码写到内部。
  2. async 异步加载,dom 文档加载完就执行,async只能加载外部脚本,不能把 Javascript 代码写到 <script> 标签里。
  3. 创建 script ,插入到DOM中,加载完毕后callback

动态加载

第三种技术的重点在于:无论在何时启动下载,文件的下载和执行过程不会阻塞页面其他进程。

如果要求不是太高,那么就可以不用动态加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function loadScript(url, callback) {
let script = document.createElement('script');
script.type = "text/javascript";
if (script.readyState) {
script.onreadystatechange = function () { //IE
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
// 只要其中一个状态触发,就删除事件处理器(以确保事件不会处理两次)
callback();
}
}
} else {
script.onload = function () {
// 确保加载完成后才能执行回调函数,如果没有这个保证的话,代码执行非常快,在没有下载完成的情况下执行callback会报错。
callback();
}
}
script.src = url;
document.head.appendChild(script);
// 通常来讲,把新创建的<script>标签添加到<head>标签里比添加到<body>里更保险
}
loadScript('b.js', function () {
test();
});

b.js代码,可以正常调用。

1
2
3
function test() {
console.log("b.js执行了");
}

如果像下面这样调用的话,会报错,Uncaught ReferenceError: test is not defined,原因就是执行的时候浏览器根据执行上下文不知道 test 是什么,因此要传入一个匿名函数,在匿名函数里面执行 test。

1
loadScript('b.js', test);

最终写法:

脚本位置放在 </body> 之前,确保 JavaScript 执行过程不会阻碍页面其他内容的显示,然后进行动态加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<h1>hhhh</h1>
<script>
function loadScript(url, callback) {
let script = document.createElement('script');
script.type = "text/javascript";
if (script.readyState) {
script.onreadystatechange = function () { //IE
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
// 只要其中一个状态触发,就删除事件处理器(以确保事件不会处理两次)
callback();
}
}
} else {
script.onload = function () {
// 确保加载完成后才能执行回调函数,如果没有这个保证的话,代码执行非常快,在没有下载完成的情况下执行callback会报错。
callback();
}
}
script.src = url;
document.head.appendChild(script);
// 通常来讲,把新创建的<script>标签添加到<head>标签里比添加到<body>里更保险
}
loadScript('b.js', function () {
test();
});
</script>
</body>

</html>

第二章 数据存取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function initUI() {
let bd = document.body, // 1
links = document.getElementsByTagName("a"), // 2
i = 0,
len = links.length;

while (i < len) {
update(links[i++]);
}
document.getElementById("go-btn").onclick = function () { // 3
start();
}
bd.className = "active";
}

上面的函数中引用了三次 document,而 document 是个全局对象。搜索该变量的过程必须遍历整个作用域链,直到最后在全局变量对象中找到。因此可以先将全局变量的引用存储在一个局部变量中,然后使用这个局部变量代替全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function initUI() {
let doc = document,
bd = doc.body, // 1
links = doc.getElementsByTagName("a"), // 2
i = 0,
len = links.length;

while (i < len) {
update(links[i++]);
}
doc.getElementById("go-btn").onclick = function () { // 3
start();
}
bd.className = "active";
}

第三章 DOM编程

减少访问DOM的次数

访问DOM元素是有代价的,修改元素则更为昂贵,因为它会导致浏览器重新计算页面的几何变化。

最坏的情况是在循环中访问或修改元素,尤其是对HTML元素集合循环操作。例如下面这段代码:

1
2
3
4
5
function innerHTMLLoop() {
for(let i = 0; i < 10000; i ++) {
document.getElementById('here').innerHTML += "a";
}
}

上面的循环函数修改页面元素的内容的问题在于每次循环,该元素都被访问两次:一次读取 innerHTML 属性值,另一次重写它。

可以这样改进:

1
2
3
4
5
6
7
function innerHTMLLoop() {
let content = "";
for(let i = 0; i < 10000; i ++) {
content += "a";
}
document.getElementById('here').innerHTML += content;
}

可以很直观地体验到速度的变快。

通用的经验法则是:减少访问DOM的次数,把运算尽量留在ECMAScript这一端处理。

重绘与重排

浏览器下载完页面中的所有组件–HTML标记、JS、CSS、图片–之后会解析并生成两个内部数据结构:

  • DOM 树:表示页面结构
  • 渲染树:表示DOM节点如何显示

重排是什么:重新生成布局。当DOM 的变化影响了元素的几何属性(宽和高)–比如改变边框宽度或给段落增加文字导致行数增加–浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为重排。

重绘是什么:重新绘制。完成重排后,浏览器会重新绘制受影响的部分到屏幕中。这个过程称为重绘。

重排一定会导致重绘,重绘不一定导致重排。如果DOM变化不影响几何属性(比如改变一个元素的背景色并不影响它的宽和高),则只发生一次重绘(不需要重排),因为元素的布局没有改变。

更多内容可以参考这篇文章:https://juejin.im/post/5c15f797f265da61141c7f86

第四章 算法和流程控制

使用递归的时候,尽量结合缓存。

如果遇到栈溢出错误,可以将方法改为迭代算法。

基于函数的迭代,比如 forEach,性能远不如 for 循环。

第五章 字符串和正则表达式

未看

第六章 快速响应的用户界面

这一章第一部分其实就是利用 setTimeout 可以处理复杂的任务,不再进行记录,可以查看 JavaScript 忍者秘籍13章的 setTimeout 部分。

第二部分 Web Workers 未看

第七章 Ajax

介绍了几种数据格式,现在主流的就是 JSON

第八章 编程实践

延时加载

1
2
3
4
5
6
7
8
// 未改进之前
function addHandler(target, eventType, handler) {
if (target.addEventListener) {
target.addEventListener(eventType, handler, false);
} else { // IE
target.attachEvent("on" + eventType, handler);
}
}

上面代码的问题就是:如果第一次调用 addHandler() 时就确定 addEventListener() 是存在的,那么随后每次调用时它应该也都存在,每次调用一个函数都重复相同的工作是一种浪费,有几种方法可以避免它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function addHandler(target, eventType, handler) {
// 复写现有函数
if (target.addEventListener) {
addHandler = function (target, eventType, handler) {
target.addEventListener(eventType, handler, false);
}
} else { // IE
addHandler = function (target, eventType, handler) {
target.attachEvent("on" + eventType, handler);
}
}
// 调用新函数
addHandler(target, eventType, handler);
}

上面这种方法就是延时加载,第一次调用时,会先检测并决定使用哪种方法去绑定事件处理器,然后原始函数被包含正确操作的新函数覆盖,最后一步调用新的函数,随后每次调用 addHandler() 都不会再做检测,因为检测代码已经被新的函数覆盖。

当一个函数在页面中不会立即调用时,延时加载是最好的选择。

条件预加载

这种方式会在脚本加载期间提前检测,而不会等到函数被调用。检测的操作依然只有一次,只是它在过程中来得更早,例如:

1
2
3
4
5
6
7
let addHandler = document.body.addEventListener ? 
function (target, eventType, handler) {
target.addEventListener(eventType, handler, false);
}:
function (target, eventType, handler) {
target.attachEvent("on" + eventType, handler);
};

条件预加载确保所有函数调用消耗的时间相同。其代价是需要在脚本加载时就检测,而不是加载后。预加载适用于一个函数马上就要被用到,并且在整个页面的生命周期中频繁出现的场合。

位操作

有好几种方法来利用位操作符提升JavaScript的速度。首先是使用位运算代替纯数学操作。比如通常采用对 2 取模运算实现表格行颜色交替,例如:

1
2
3
4
5
6
7
8
for(let i = 0, len = rows.length; i < len; i ++) {
if (i % 2) {
className = "even";
} else {
className = "odd";
}
// 增加class
}

如果你看到 32 位数字的二进制底层表示,会发现偶数的最低位是 0,奇数的最低位是 1。让给定数与数字 1 进行按位与运算,如果此数为偶数,那么它和 1 进行按位与运算的结果是 0,如果此数为奇数,那么它和 1 进行按位与运算的结果就是 1。

1
2
3
4
5
6
7
8
for(let i = 0, len = rows.length; i < len; i ++) {
if (i & 1) {
className = "even";
} else {
className = "odd";
}
// 增加class
}

原生方法

当原生方法可用时,尽量使用它们。特别是数学运算(Math)和DOM操作。这样用编译后的代码做越多的事情,你的代码就会越快。

第九章 第十章

这两章感觉没什么参考价值了

第五章:未看

第六章的第二部分 Web Workers 未看

1…141516…32

gzl

96 日志
14 分类
37 标签
© 2020 gzl
由 Hexo 强力驱动 v3.7.1
|
主题 – NexT.Pisces v7.2.0