8.异步处理之Promise
8.0 [回顾]事件循环
JS运行的环境称之为宿主环境。
执行栈:call stack,一个数据结构,用于存放各种函数的执行环境,每一个函数执行之前,它的相关信息会加入到执行栈。函数调用之前,创建执行环境,然后加入到执行栈;函数调用之后,销毁执行环境。
JS引擎永远执行的是执行栈的最顶部。
异步函数:某些函数不会立即执行,需要等到某个时机到达后才会执行,这样的函数称之为异步函数。比如事件处理函数。异步函数的执行时机,会被宿主环境控制。
浏览器宿主环境中包含5个线程:
-
JS引擎:负责执行执行栈的最顶部代码
-
GUI线程:负责渲染页面
-
事件监听线程:负责监听各种事件
-
计时线程:负责计时
-
网络线程:负责网络通信
当上面的线程发生了某些事请,如果该线程发现,这件事情有处理程序,它会将该处理程序加入一个叫做事件队列的内存。当JS引擎发现,执行栈中已经没有了任何内容后,会将事件队列中的第一个函数加入到执行栈中执行。
JS引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环。
事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:
-
宏任务(队列):macroTask,计时器结束的回调、事件回调、http回调等等绝大部分异步函数进入宏队列
-
微任务(队列):MutationObserver,Promise产生的回调进入微队列
MutationObserver用于监听某个DOM对象的变化
当执行栈清空时,JS引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。
console.log("a")
setTimeout(() => {
console.log("b")
}, 0);
for (let i = 0; i < 1000; i++) {
console.log("c")
}
//执行顺序 a 1000个c b
MutationObserver
例子:
<ul id="container">
</ul>
<button id="btn">点击</button>
let count = 1;
const ul = document.getElementById("container");
document.getElementById("btn").onclick = function A() {
setTimeout(function C() {
console.log("添加了一个li")
}, 0);
var li = document.createElement("li")
li.innerText = count++;
ul.appendChild(li);
}
//监听ul
const observer = new MutationObserver(function B() {
//当监听的dom元素发生变化时运行的回调函数
console.log("ul元素发生了变化")
})
//监听ul
observer.observe(ul, {
attributes: true, //监听属性的变化
childList: true, //监听子元素的变化
subtree: true //监听子树的变化
})
//取消监听
// observer.disconnect();
// 当点击按钮后会先执行打印 ul元素发生了变化, 后打印 添加了一个li; 因为在添加li那里有一个定时器,执行时会把该执行内容放入宏队列,然后MutationObserver的回调会放入微队列,执行事件队列时会优先把微队列任务放入JS引擎执行,然后才会执行宏队列任务。
8-1. 事件和回调函数的缺陷
我们习惯于使用传统的回调或事件处理来解决异步问题
事件:某个对象的属性是一个函数,当发生某一件事时,运行该函数
dom.onclick = function(){
}
回调:运行某个函数以实现某个功能的时候,传入一个函数作为参数,当发生某件事的时候,会运行该函数。
dom.addEventListener("click", function(){
})
本质上,事件和回调并没有本质的区别,只是把函数放置的位置不同而已。
一直以来,该模式都运作良好。
直到前端工程越来越复杂…
目前,该模式主要面临以下两个问题:
- 回调地狱:某个异步操作需要等待之前的异步操作完成,无论用回调还是事件,都会陷入不断的嵌套
- 异步之间的联系:某个异步操作要等待多个异步操作的结果,对这种联系的处理,会让代码的复杂度剧增
回调地狱例子1:
<p>
<button id="btn1">按钮1:给按钮2注册点击事件</button>
<button id="btn2">按钮2:给按钮3注册点击事件</button>
<button id="btn3">按钮3:点击后弹出hello</button>
</p>
const btn1 = document.getElementById("btn1"),
btn2 = document.getElementById("btn2"),
btn3 = document.getElementById("btn3");
btn1.addEventListener("click", function() {
//按钮1的其他事情
btn2.addEventListener("click", function() {
//按钮2的其他事情
btn3.addEventListener("click", function() {
alert("hello");
})
})
})
回调地狱例子2:
/*
邓哥心中有三个女神
有一天,邓哥决定向第一个女神表白,如果女神拒绝,则向第二个女神表白,直到所有的女神都拒绝,或有一个女神同意为止
用代码模拟上面的场景
*/
function biaobai(god, callback) {
console.log(`邓哥向女神【${god}】发出了表白短信`);
setTimeout(() => {
if (Math.random() < 0.1) {
//女神同意拉
//resolve
callback(true);
} else {
//resolve
callback(false);
}
}, 1000);
}
biaobai("女神1", function(result) {
if (result) {
console.log("女神1答应了,邓哥很开心!")
} else {
console.log("女神1拒绝了,邓哥表示无压力,然后向女神2表白");
biaobai("女神2", function(result) {
if (result) {
console.log("女神2答应了,邓哥很开心!")
} else {
console.log("女神2十分感动,然后拒绝了邓哥,邓哥向女神3表白");
biaobai("女神3", function(result) {
if (result) {
console.log("女神3答应了,邓哥很开心!")
} else {
console.log("邓哥表示生无可恋!!");
}
})
}
})
}
})
回调地狱例子3:
//获取李华所在班级的老师的信息 (数据在本地)
ajax({
url: "./data/students.json?name=李华",
success: function(data) {
for (let i = 0; i < data.length; i++) {
if (data[i].name === "李华") {
const cid = data[i].classId;
ajax({
url: "./data/classes.json?id=" + cid,
success: function(data) {
for (let i = 0; i < data.length; i++) {
if (data[i].id === cid) {
const tid = data[i].teacherId;
ajax({
url: "./data/teachers.json?id=" + tid,
success: function(data) {
for (let i = 0; i < data.length; i++) {
if (data[i].id === tid) {
console.log(data[i]);
}
}
}
})
return;
}
}
}
})
return;
}
}
}
})
异步之间的联系例子:
/*
邓哥心中有二十个女神,他决定用更加高效的办法
他同时给二十个女神表白,如果有女神同意,就拒绝其他的女神
并且,当所有的女神回复完成后,他要把所有的回复都记录到日志进行分析
用代码模拟上面的场景
*/
function biaobai(god, callback) {
console.log(`邓哥向女神【${god}】发出了表白短信`);
setTimeout(() => {
if (Math.random() < 0.05) {
//女神同意拉
callback(true);
} else {
callback(false);
}
}, Math.floor(Math.random() * (3000 - 1000) + 1000));
}
let agreeGod = null; //同意邓哥的第一个女神
const results = []; //用于记录回复结果的数组
for (let i = 1; i <= 20; i++) {
biaobai(`女神${i}`, result => {
results.push(result);
if (result) {
console.log(`女神${i}同意了`)
if (agreeGod) {
console.log(`邓哥回复女神${i}: 不好意思,刚才朋友用我手机,乱发的`)
} else {
agreeGod = `女神${i}`;
console.log(`邓哥终于找到了真爱`);
}
} else {
console.log(`女神${i}拒绝了`)
}
if (results.length === 20) {
console.log("日志记录", results)
}
})
}
8-2. 异步处理的通用模型
ES官方参考了大量的异步场景,总结出了一套异步的通用模型,该模型可以覆盖几乎所有的异步场景,甚至是同步场景。
值得注意的是,为了兼容旧系统,ES6 并不打算抛弃掉过去的做法,只是基于该模型推出一个全新的 API,使用该API,会让异步处理更加的简洁优雅。
理解该 API,最重要的,是理解它的异步模型
- ES6 将某一件可能发生异步操作的事情,分为两个阶段:unsettled 和 settled
- unsettled: 未决阶段,表示事情还在进行前期的处理,并没有发生通向结果的那件事
- settled:已决阶段,事情已经有了一个结果,不管这个结果是好是坏,整件事情无法逆转
事情总是从 未决阶段 逐步发展到 已决阶段的。并且,未决阶段拥有控制何时通向已决阶段的能力。
- ES6将事情划分为三种状态: pending、resolved、rejected
- pending: 挂起,处于未决阶段,则表示这件事情还在挂起(最终的结果还没出来)
- resolved:已处理,已决阶段的一种状态,表示整件事情已经出现结果,并是一个可以按照正常逻辑进行下去的结果
- rejected:已拒绝,已决阶段的一种状态,表示整件事情已经出现结果,并是一个无法按照正常逻辑进行下去的结果,通常用于表示有一个错误
既然未决阶段有权力决定事情的走向,因此,未决阶段可以决定事情最终的状态!
我们将 把事情变为resolved状态的过程叫做:resolve,推向该状态时,可能会传递一些数据
我们将 把事情变为rejected状态的过程叫做:reject,推向该状态时,同样可能会传递一些数据,通常为错误信息
始终记住,无论是阶段,还是状态,是不可逆的!
3. 当事情达到已决阶段后,通常需要进行后续处理,不同的已决状态,决定了不同的后续处理。
- resolved状态:这是一个正常的已决状态,后续处理表示为 thenable
- rejected状态:这是一个非正常的已决状态,后续处理表示为 catchable
后续处理可能有多个,因此会形成作业队列,这些后续处理会按照顺序,当状态到达后依次执行
4. 整件事称之为Promise
理解上面的概念,对学习Promise至关重要!