文章目录
第1部分 热身
第1章 无处不在的JavaScript
1.1 “理解”JavaScript语言
JavaScript与其他语言的差异:
- 函数是一等公民(一级对象)。
- 函数闭包。
- 基于原型的面向对象。
1.2 理解浏览器
- 文档对象模型(DOM)。
- 事件。
- 浏览器API。
1.3 使用当前的最佳实践
-
调试技巧。
-
测试。
-
性能分析。
console.time('xxx')
和console.timeEnd('xxx')
。
第2章 运行时的页面构建过程
2.1 生命周期概览
!
生命周期
- 页面构建——创建用户界面。
- 事件处理。
2.2 页面构建阶段
页面构建阶段的两个步骤:
- 解析HTML代码并构建文档对象模型(DOM)。
- 执行JavaScript代码。
步骤一会在浏览器处理HTML节点的过程中执行,步骤二会在HTML解析到script标签时执行。页面构建阶段中,这两个步骤会交替多次。
2.2.1 HTML解析和DOM构建
每个HTML元素都是一个节点,除了根元素,每个节点只有一个父节点。
HTML代码是浏览器页面UI构建初始DOM的蓝图,为了正确构建每个DOM,浏览器还会修复它在蓝图中发现的问题。
每当解析到脚本元素时,浏览器就会停止从HTML构建DOM,并开始执行JavaScript代码。
2.2.2 执行JavaScript代码
所有在某个JavaScript代码执行期间用户创建的全局变量都能正常地被其他脚本元素中的JavaScript代码所访问到,其原因在于全局window
对象会存在于整个页面的生存期之间。
2.3 事件处理
一次只能处理一个事件,所以采用事件队列的方式处理事件。
放置事件的队列是在页面构建阶段和事件处理阶段以外的。
2.3.3 处理事件
事件循环会一直执行到用户关闭了Web应用。
第2部分 理解函数
第3章 定义与参数
3.1 函数式的不同点到底是什么
函数是一等公民,与对象的区别在于它是可调用的。
3.1.2 回调函数
每当我们建立了一个将在随后调用的函数,无论是在事件处理阶段通过浏览器还是通过其他代码,我们都是在建立一个回调(callback)。
3.3 函数定义
JavaScript提供了几种定义函数的方式:
- 函数声明(function declarations)和函数表达式(function expressions)。
- 箭头函数。
- 函数构造函数——不常用,可以以字符串形式动态构造。
- 生成器函数——这种函数能够退出再重新进入,在这些再进入之间保留函数内变量的值。
3.3.1 函数声明和函数表达式
函数声明
声明以强制性的function开头,其后紧接着强制性的函数名。
函数表达式
通过变量赋值,或者作为其他函数的参数或返回值。
<script>
var myFunc = function(){};
myFunc(function(){
return function(){};
});
(function namedFunctionExpression () {
})();
+function(){}();
-function(){}();
!function(){}();
~function(){}();
</script>
函数声明和函数表达式一个重要的不同点是:函数声明的函数名是强制性的,而函数表达式的函数名是可选的。
立即函数(IIFE)
(function () {})()
。使用括号是告诉JavaScript引擎它正在处理的是表达式而不是声明。
3.4 函数的实参和形参
- 形参是我们定义函数时所列举的变量。
- 实参是我们调用函数时所传递给函数的值。
当函数调用时提供了一系列实参,这些实参就会以形参在函数中定义的顺序被赋值到形参上。实参的数量大于形参的数量时并不会抛出错误。
第4章 函数进阶:理解函数调用
4.1 使用隐式函数参数
函数调用时会传递arguments
和this
两个隐式的参数。
在严格模式下,arguments
对象将不再作为参数的别名,所以无法真正修改参数的值。
<script>
"use strict"
function infiltrate(person){
assert(person === 'gardener',
'The person is a gardener');
assert(arguments[0] === 'gardener',
'The first argument is a gardener');
arguments[0] = 'ninja';
assert(arguments[0] === 'ninja',
'The first argument is now a ninja');
assert(person === 'gardener',
'The person is still a gardener');
}
infiltrate("gardener");
</script>
4.1.2 this参数
this参数的指向不仅是又定义函数的方式和位置决定的,同时还严重受到函数调用方式的影响。
4.2 函数调用
4种方式调用一个函数:
- 作为一个函数(function)——
skulk()
,直接被调用。 - 作为一个方法(method)——
ninja.skulk()
,关联在一个对象上,实现面向对象编程。 - 作为一个构造函数(constructor)——
new Ninja()
,实例化一个新的对象。 - 通过函数的
apply
或call
方法——skulk.apply(ninja)
或者skulk.call(ninja)
。
4.2.1 作为函数直接被调用
function ninja() {};
ninja();
(function() {})()
在非严格模式下,this
是全局上下文(window对象),而在严格模式下,它将是undefined。
4.2.2 作为方法被调用
当函数作为对象的方法被调用时,this
指向该对象。
4.2.3 作为构造函数被调用
new
的过程:
- 创建一个新的空对象。
- 将构造函数的原型指向该对象的原型。
- 该对象作为
this
参数传递给构造函数,从而成为构造函数的函数上下文。 - 新构造的对象作为
new
运算符的返回值。
构造函数返回值
- 如果构造函数返回一个对象,则该对象将作为整个表达式的值访问,而传入构造函数的this将被丢弃。
- 如果构造函数返回的是非对象类型,则忽略返回值,返回新创建的对象。
函数和方法命名通常以动词小写开头,而构造函数通常以名词大写开头。
4.3 解决函数上下文的问题
4.3.1 使用箭头函数绕过函数上下文
箭头函数没有单独的this值,箭头函数的this与声明所在的上下文相同。
箭头函数自身不含上下文,从定义时的所在函数继承上下文。
<body>
<button id="test">Click Me!</button>
<script>
function Button() {
this.clicked = false;
this.click = () => {
this.clicked = true;
assert(button.clicked, "The button has been clicked");
}
}
var button = new Button();
var elem = document.getElementById("test");
elem.addEventListener("click", button.click);
</script>
</body>
箭头函数在对象字面量中定义时,this指向对象字面量所处的上下文。
<body>
<button id="test">Click Me!</button>
<script type="text/javascript">
assert(this == window, "this == window");
var button = {
clicked: false,
click: () => {
this.clicked = true;
assert(button.clicked,"The button has been clicked"); // false
assert(this == window, "In arrow function this == window");
assert(window.clicked, "clicked is stored in window");
}
}
var elem = document.getElementById("test");
elem.addEventListener("click", button.click);
</script>
</body>
4.3.2 使用bind方法
调用bind
方法不会修改原始函数,而是创建一个全新的函数。
箭头函数使用bind
、call
、apply
方法时,this
都将被忽略。
第5章 闭包和作用域
5.1 理解闭包
闭包允许函数访问并操作函数外部的变量。只要变量或函数存在于声明函数时的作用域内,闭包即可使函数能够访问这些变量或函数。
每一个通过闭包访问变量的函数都具有一个作用域链,作用域链包含闭包的全部信息。
5.2 使用闭包
5.2.1 封装私有变量
function Ninja() {
let feints = 0
this.getFeints = () = > feints
this.feint = () => { feints++ }
}
const ninja1 = new Ninja()
ninja1.feint()
ninja1.getFeints() // 1
const ninja2 = new Ninja()
ninja2.getFeints() // 0
闭包不是在创建的那一时刻的状态的快照,而是一个真实的状态封装,只要闭包存在,就可以对变量进行修改。
5.3 通过执行上下文来跟踪代码
两种执行上下文:全局执行上下文和函数执行上下文
全局执行上下文只有一个,而函数执行上下文在每次函数调用时,就会创建一个。
JavaScript是单线程,一旦函数发生调用,当前的执行上下文必须停止执行,并创建新的函数执行上下文来执行函数,当函数执行完成后,将函数执行上下文销毁,并重新回到发生调用时的执行上下文中,调用栈(后进先出)。
5.4 使用词法环境跟踪变量的作用域
词法环境(lexical environment)(也称为作用域scopes)是JavaScript引擎内部用来跟踪标识符与特定变量之间的映射关系。
在作用域范围内,每次执行代码时,代码结构都获得与之关联的词法环境。内部代码结构可以访问外部代码结构中定义的变量。
每个执行上下文都有一个与之关联的词法环境,词法环境中包含了在上下文中定义的标识符的映射表。
理解JavaScript中的变量类型
JavaScript代码的执行分为两个阶段进行,第一个阶段会访问并注册在当前词法环境中所声明的变量和函数,第二个阶段具体如何执行取决于变量的类型(let、var、const和函数声明)以及环境类型(全局环境、函数环境或块级作用域)。
5.6 研究闭包的工作原理
闭包与作用域密切相关。闭包对JavaScript的作用域规则产生了直接影响。
通过闭包可以访问创建闭包时所处环境中的全部变量。闭包为函数创建时所处的作用域中的函数和变量,创建“安全气泡”。通过这种方式,即使创建函数时所处的作用域已经消失,但是函数仍然能够获得执行时所需的全部内容。
第6章 生成器和promise
6.2 使用生成器函数
调用生成器并不会执行生成器函数,相反,它会创建一个叫做迭代器(iterator)的对象。
<script>
"use strict"
function* WeaponGenerator(){
yield "Katana";
yield "Wakizashi";
yield "Kusarigama";
}
for(let weapon of WeaponGenerator()) {
assert(weapon !== undefined, weapon);
}
</script>
6.2.1 通过迭代器对象控制生成器
调用生成器函数生成迭代器对象,不会执行生成器函数体,通过迭代器对象,可以与生成器通信。
迭代器用于控制生成器的执行,通过next方法,执行next函数后,生成器就开始执行代码,当遇到yield
关键字时,就会生成一个中间结果然后返回一个新对象。
每当生成一个当前值后,生成器就会非阻塞的挂起执行。
function* WeaponGenerator(){
yield "Katana";
yield "Wakizashi";
}
const weaponsIterator = WeaponGenerator();
const result1 = weaponsIterator.next();
const result2 = weaponsIterator.next();
<script>
// 对迭代器进行迭代
function* WeaponGenerator(){
yield "Katana";
yield "Wakizashi";
}
const weaponsIterator = WeaponGenerator();
var item;
while(!(item = weaponsIterator.next()).done) {
assert(item !== null, item.value);
}
</script>
// 把执行权交给下一个生成器
function* WarriorGenerator(){
yield "Sun Tzu";
yield* NinjaGenerator();
yield "Genghis Khan";
}
function* NinjaGenerator(){
yield "Hatori";
yield "Yoshi";
}
for(let warrior of WarriorGenerator()){
assert(warrior !== null, warrior); // Sun Tzu,Hatori,Yoshi,Genghis Khan
}
在迭代器上使用yield*
操作符,程序会跳转到另外一个生成器上执行。
6.2.2 使用生成器
用生成器生成ID序列
function* IdGenerator() {
let id = 0;
while (true) {
yield ++id;
}
}
迭代器遍历DOM树
function* DomTraversal(element) {
yield element;
element = element.firstElementChild;
while (element) {
yield* DomTraversal(element);
element = element.nextElementSibling;
}
}
const subTree = document.getElementById("subTree");
for(let element of DomTraversal(subTree)) {
assert(element !== null, element.nodeName);
}
6.2.3 与生成器交互
作为生成器函数参数发送值
function* NinjaGenerator(action) {
const imposter = yield ("Hattori" + action);
yield `Yoshi (${imposter}) ${action}`;
}
const ninjaIterator = NinjaGenerator("skulk");
const result1 = ninjaIterator.next();
const result2 = ninjaIterator.next("Hanzo");
6.2.4 探索生成器内部构成
- 挂起开始——创建了一个生成器后,它最先以这种状态开始。其中的任何代码都未执行。
- 执行——生成器中的代码执行的状态。执行要么是刚开始,要么是从上次挂起的时候继续。当存在可执行的代码且迭代器调用了
next
方法,生成器都会转移到这个状态。 - 挂起让渡——当生成器在执行过程中遇到一个
yield
表达式,它会创建一个包含着返回值的新对象,随后再挂起执行。 - 完成——在生成器执行期间,如果代码执行到
return
语句或者全部代码执行完毕,生成器就进入该状态。
生成器与普通函数的区别
- 在于当生成器执行完毕后,对应的执行环境上下文会从栈中弹出,由于迭代器保持着对生成器的引用,所以生成器不会被销毁。而普通函数会被销毁。
- 普通函数每次调用都会创建一个新的执行环境上下文,并放入栈中,而生成器会重新激活对应的执行上下文。
6.3 Promise
一旦promise进入到完成态或者拒绝态,它的状态都不能再切换了。
6.3.3 隐式拒绝promise
const promise = new Promise((resolve, reject) => {
undeclaredVariable++;
});
promise.then(() => fail("Happy path, won't be called!"))
.catch(error => pass("The promise was rejected because an exception was thrown!"));
6.3.5 链式调用promise
getJSON("data/ninjas.json")
.then(ninjas => getJSON(ninjas[0].missionsUrl))
.then(missions => getJSON(missions[0].detailsUrl))
.then(mission => assert(mission !== null, "Ninja mission obtained!"))
.catch(error => fail("An error has occured"));
6.3.6 等待多个promise
Promise.all([getJSON("data/ninjas.json"),
getJSON("data/mapInfo.json"),
getJSON("data/plan.json")]).then(results => {
const ninjas = results[0], mapInfo = results[1], plan = results[2];
assert(ninjas !== undefined && mapInfo !== undefined
&& plan !== undefined,
"The plan is ready to be set in motion!");
}).catch(error => fail("A problem in carrying out our plan!"));
6.4 把生成器和promise相结合
function async(generator) {
const iterator = generator();
function handle(iteratorResult) {
if (iteratorResult.done) return;
const iteratorValue = iteratorResult.value;
if (iteratorValue instanceof Promise) {
iteratorValue.then(res => handle(iterator.next(res)))
.catch(err => iterator.throw(err));
}
}
try {
handle(iterator.next());
}
catch (e) { iterator.throw(e); }
}
async(function* () {
try {
const ninjas = yield getJSON("data/ninjas.json");
const missions = yield getJSON(ninjas[0].massionUrl);
const missionDescription = yield getJSON(missions[0].detailsUrl);
}
catch(e) {}
})
面向未来的async函数
await
关键字用来告诉JavaScript引擎,请在不阻塞应用执行的情况下在这个位置上等待执行结果。
第3部分 深入钻研对象,强化代码
第7章 面向对象与原型
7.1 理解原型
每个对象都可以有一个原型,每个对象的原型也可以拥有一个原型。
7.2 对象构造器与原型
构造函数是用来初始化对象为已知的初始状态。
- 每一个函数都具有一个原型prototype对象。
- 每一个函数的原型都具有一个constructor属性,该属性指向函数本身。
- constructor对象的原型设置为新创建的对象的原型。
函数的原型对象prototype和原型[[Prototype]]是不同的概念。
7.2.2 JavaScript动态特性的副作用
对象与函数原型之间的引用关系是在对象创建时建立的。
function Ninja(){
this.swung = true;
}
const ninja1 = new Ninja();
Ninja.prototype.swingSword = function(){
return this.swung;
};
assert(ninja1.swingSword(),
"Method exists, even out of order.");
Ninja.prototype = {
pierce: function() {
return true;
}
}
assert(ninja1.swingSword(),
"Our ninja can still swing!");
const ninja2 = new Ninja();
assert(ninja2.pierce(),"Newly created ninjas can pierce");
assert(!ninja2.swingSword, "But they cannot swing!");
7.2.3 通过构造函数实现对象类型
function Ninja(){}
const ninja = new Ninja();
const ninja2 = new ninja.constructor();
assert(ninja2 instanceof Ninja, "It's a Ninja!");
assert(ninja !== ninja2, "But not the same Ninja!");
7.3 实现继承
原型继承
function Person(){}
Person.prototype.dance = function(){};
function Ninja(){}
Ninja.prototype = new Person()
// 会造成`constructor`属性丢失。
解决constructor
问题
function Person(){}
Person.prototype.dance = function(){};
function Ninja(){}
Ninja.prototype = new Person()
Object.defineProperty(Ninja.prototype, 'constructor', {
emumerable: false,
value: Ninja,
writable: true
})
7.3.2 instanceof操作符
检查操作符右边的函数的原型是否存在于操作符左边的对象的原型链上。
7.4 class
实现继承
class Person {
constructor(name) {
this.name = name;
}
dance() {
return true;
}
}
class Ninja extends Person{
constructor(name, weapon) {
super(name);
this.weapon = weapon;
}
wieldWeapon() {
return true;
}
}
第8章 控制对象的访问
8.1 使用getter与setter控制属性访问
8.1.1 定义getter和setter
有两种方式定义getter和setter
- 通过对象字面量定义,或在ES6的class中定义。
- 通过使用内置的
Object.defineProperty
方法。
// 缺点是无法定义私有对象属性,原因是它们不是在同一个作用域下定义的,使用Object.defineProperty可以解决这个问题
// 字面量
const ninjaCollection = {
ninjas: ["Yoshi", "Kuma", "Hattori"],
get firstNinja() {
report("Getting firstNinja");
return this.ninjas[0];
},
set firstNinja(value){
report("Setting firstNinja");
this.ninjas[0] = value;
}
};
// class
class NinjaCollection {
constructor(){
this.ninjas = ["Yoshi", "Kuma", "Hattori"];
}
get firstNinja(){
report("Getting firstNinja");
return this.ninjas[0];
}
set firstNinja(value){
report("Setting firstNinja");
this.ninjas[0] = value;
}
}
// Object.defineProperty
function Ninja() {
let _skillLevel = 0;
Object.defineProperty(this, 'skillLevel', {
get: () => {
return _skillLevel;
},
set: val => {
_skillLevel = val
}
})
}
8.2 使用代理控制访问
代理与setter和getter区别是每个setter与getter仅能控制单个对象属性,而代理可用于对象交互的通用处理,包括调用对象的方法。
第9章 处理集合
9.1 数组
shift
和unshift
方法修改第一个元素,之后的每一个元素的索引都需要调整,非特殊情况不建议使用shift
和unshift
。
合计数组元素
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((aggregated, number) => aggregated + number, 0);
9.2 Map
对象:
- 传统对象的value值如果是对象,则还可以访问到原型的属性。
- 对象的key只能是string。
Map
9.2.2 创建map
const ningjaIslanMap = new Map();
const ninja1 = { name: "Yoshi"};
const ninja2 = { name: "Hatori"};
const ninja3 = { name: "Kuma"};
ningjaIslanMap.set(ninja1, { homeIsland: "Honshu"});
ningjaIslanMap.set(ninja2, { homeIsland: "Hokkaido"});
console.log(ninjaIslandMap.get(ninja1).homeIsland); // Honshu
console.log(ninjaIslandMap.get(ninja3)); // undefined
console.log(ninjaIslandMap.size === 2);
console.log(ninjaIslandMap.has(ninja1) && ninjaIslandMap.has(ninja2));
ninjaIslandMap.delete(ninja1);
ninjaIslandMap.clear();
9.2.3 遍历map
for (let item of map.entries()) {
console.log(item[0]);
console.log(item[1]);
}
for (let k of map.keys()) {}
for (let val of map.values()) {}
9.3 Set
new Set([1,2,3])
。
使用展开运算符可以将Set集合转成数组
[...new Set(['a', 'b', 'a', 'c'])]
9.3.2 并集
const a = [1, 2, 3, 4];
const b = [3, 4, 5, 6];
const sum = [...new Set([...a, ...b])];
9.3.3 交集
const a = [1, 2, 2, 3, 4];
const b = [2, 2, 4, 6, 7, 8, 9]
const c = [...new Set(...[a.filter((item) => b.indexOf(item) !== -1)])];
9.3.4 差集
const ninjas = new Set(["Kuma", "Hattori", "Yagyu"]);
const samurai = new Set(["Hattori", "Oda", "Tomoe"]);
const pureNinjas = [...ninjas].filter(item => !samurai.has(item));
第11章 代码模块化
11.1 在JavaScript ES6之前的版本中模块化代码
11.1.1 使用对象、闭包和IIFE实现模块
// 模块模式
(function() {
let numClicks = 0;
const handleClick = () => {
++numClicks
}
return {
countClicks: () => {
document.addEventListener("click", handleClick);
}
}
})()
// 模块扩展
// 缺点:无法共享模块之间的变量
const MouseCounterModule = (function() {
let numClicks = 0;
const handleClick = () => {
++numClicks
}
return {
countClicks: () => {
document.addEventListener("click", handleClick);
}
}
})()
(function(module) {
let numScroll = 0;
const handleScroll = () => {
++numScroll;
}
module.countScolls = () => {}
})(MouseCounterModule)
11.1.2 使用AMD和CommonJS模块化JavaScript应用
AMD的设计理念是明确基于浏览器,而CommonJS则是面向通用JavaScript环境,不局限于浏览器。
AMD是异步加载,CommonJS是同步加载,基于文件的模块。
UMD,这种模式同时支持AMD和CommonJS。
11.2 ES6模块
ES6模块优点:
- 基于文件。
- 支持异步加载模块。
// 导出的全部标识符
import * as module from "./ninja.js";
ES6模块和CommonJS模块的区别:ES6模块不能覆盖导入的模块的值,而CommonJS引入模块后会浅拷贝模块,修改模块的值(基础类型)不会导致模块的值改变,而修改引用类型的值会修改值。
第4部分 洞悉浏览器
第12章 DOM操作
12.1 向DOM中注入HTML
12.1.1 将HTML字符串换成DOM
// 确保自闭合元素被正确解释
const tags = /^(area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i;
function convert(html) {
return html.replace(/(<(\w+)[^>]*?)\/>/g, (all, front, tag) => {
return tags.test(tag) ? all :
front + "></" + tag + ">";
});
}
assert(convert("<a/>") === "<a></a>", "Check anchor conversion.");
assert(convert("<hr/>") === "<hr/>", "Check hr conversion.");
// 将元素标转为一系列DOM节点
function getNodes(htmlString, doc) {
const map = {
"<td":[3,"<table><tbody><tr>","</tr></tbody></table>"],
"<th":[3,"<table><tbody><tr>","</tr></tbody></table>"],
"<tr":[2,"<table><thead>","</thead></table>"],
"<option":[1,"<select multiple='multiple'>","</select>"],
"<optgroup":[1,"<select multiple='multiple'>","</select>"],
"<legend":[1,"<fieldset>","</fieldset>"],
"<thead":[1,"<table>","</table>"],
"<tbody":[1,"<table>","</table>"],
"<tfoot":[1,"<table>","</table>"],
"<colgroup":[1,"<table>","</table>"],
"<caption":[1,"<table>","</table>"],
"<col":[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],
"<link":[3,"<div></div><div>","</div>"]
};
const tagName = htmlString.match(/<\w+/);
const mapEntry = tagName ? map[tagName[0]] : null;
if (!mapEntry) { mapEntry = [0, " "," " ];}
let div = (doc || document).createElement("div");
div.innerHTML = mapEntry[1] + htmlString + mapEntry[2];
while (mapEntry[0]--) {
div = div.lastChild;
}
return div.childNodes;
}
assert(getNodes("<td>test</td><td>test2</td>").length === 2,
"Get two nodes back from the method.");
assert(getNodes("<td>test</td>")[0].nodeName === "TD",
"Verify that we're getting the right node.");
12.3 令人头疼的样式特性
12.3.1 样式在何处
样式对象style
中不反映从CSS样式表中继承的任何样式信息。
12.3.3 获取计算后样式
使用getComputedStyle
方法。
function fetchComputedStyle(element, property) {
const computedStyle = getComputedStyle(element);
if (computedStyle) {
property = property.replace(/([A-Z])/g, '$1').toLowerCase();
return computedStyle.getPropertyValue(property);
}
}
12.3.5 测量元素的高度和宽度
获取一个隐藏元素的宽高:
- 设置
diplay: block
。 - 设置
visibility: hidden
。 - 设置
position: absolute
。 - 获取元素尺寸。
- 恢复先前更改的属性。
第13章 历史弥新的事件
13.1 深入事件循环
事件循环不仅仅包含事件队列,而是具有至少两个队列,除了事件,还要保持浏览器执行的其他操作,这些操作被称为任务,分为宏任务(或通常称为任务)和微任务。
宏任务的例子很多,包括创建主文档对象、解析HTML、执行主线(或全局)JavaScript代码,更改当前URL以及各种事件,如页面加载、输入、网络事件和定时器事件。宏任务代表一个个离散的、独立工作单元。运行完任务后,浏览器可以继续其他调度,如重新渲染页面的UI或执行垃圾回收。
微任务是更小的任务。微任务更新应用程序的状态,但必须在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面的UI。微任务的案例包括promise回调函数、DOM发生变化等。微任务需要尽可能快地、通过异步方式执行,同时不能产生全新的微任务。微任务使得我们能够在重新渲染Ui之前执行指定的行为,避免不必要的重绘。
事件循环的实现至少应该含有一个用于宏任务的队列和至少一个用于微任务的队列。
事件循环基于两个基本原则:
- 一次处理一个任务。
- 一个任务开始后直到运行完成,不会被其他任务中断。
单次循环迭代中,最多处理一个宏任务,而队列中的所有微任务都会被处理。
- 两类任务队列都是独立于事件循环的,这意味着任务队列的添加行为也发生在事件循环之外
- 因为JavaScript基于单线程执行模型,所以这两类任务都是逐个执行的。当一个任务开始执行后,在完成前,中间不会被任何其他任务中断。
- 所有微任务会在下一次渲染之前执行完成,因为它们的目标是在渲染前更新应用程序状态。
- 浏览器通常会尝试每秒渲染60次页面,这意味着浏览器会尝试在16ms内渲染一帧。在页面渲染时,任何任务都无法再修改。如果想要实现平滑流畅的应用,理想情况下,单个任务和该任务附属的所有微任务,都应在16ms内完成。
13.1.1 仅含宏任务的示例
事件监测和添加任务是独立于事件循环的,尽管主线程仍在执行,但仍然可以向队列添加任务。
13.1.2 同时含有宏任务和微任务的示例
13.2 计时器
浏览器不会同时创建两个相同的间隔计时器,如果interval
事件触发,并且队列中已经有对应的任务等待执行时,则不会再添加新任务。
13.3 处理事件
事件的处理方式有两种:
- 捕获——首先被顶部元素捕获,并依次向下传递。
- 冒泡——目标元素捕获之后,事件处理转向冒泡,从目标元素向顶部元素冒泡。
默认采用事件冒泡。
13.3.2 自定义事件
自定义事件是模拟真实的事件。
function triggerEvent(target, eventType, eventDetail) {
const event = new CustomEvent(eventType, {
detail: eventDetail
});
target.dispatchEvent(event);
}
function performAjaxOperation() {
triggerEvent(document, 'ajax-start', { url: 'my-url'});
setTimeout(() => {
triggerEvent(document, 'ajax-complete');
},5000);
}