什么是异步?
程序中现在运行的部分和将来运行的部分之间的关系就是异步编程,简单来说,就是有一部分程序不需要现在立即执行,而是在将来的某一个时间点,切换过去执行。
就如同你正在等水烧开,突然你妈喊你去干另一件事情,然后你10分钟后才回厨房关火。
异步的前身? - 回调函数
在ES6之前是没有正式的异步概念的,但事实上却有很多异步操作,这个时候,都是通过回调函数来实现的。
什么是回调函数?
是指一个函数作为参数传递给另一个函数,并且在特定事件发生或条件满足时被调用执行的函数。
回调函数使用场景
常见于定时器,事件绑定,Ajax响应
js
function now() {
return 21;
}
function later() {
answer = answer * 2;
console.log( "Meaning of life:", answer );
}
var answer = now();
setTimeout( later, 1000 ); // Meaning of life: 42
插个题外话,setTimeout并不保证定时器到时后就能执行(尽管从代码上看起来是这样的🐶),事实上,setTimeout所做的不过是设定一个定时器,当定时器时间到时,把回调函数放回事件循环中。这就导致函数有可能在那个时刻运行,也可能在那之后运行,取决于事件队列的状态。
Ajax响应
js
$ajax( "http://some.url", function myCallbackFunction(data){
console.log( data ); // 输出一些数据
});
事件绑定
js
const button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("点了!点了!点了!");
});
传说中的回调地狱?
问题1 - 大量嵌套函数,可读性差
js
listen( "click", function handler(evt){
setTimeout( function request(){
ajax( "http://some.url.1", function response(text){
if (text == "hello") {
handler();
}else if (text == "world") {
request();
}
} );
}, 500) ;
} );
上面的代码实际的执行顺序并不是从上到下顺序执行的,而是如下面所示。
先执行
js
listen( "click", function handler(evt){
...
});
再执行
js
setTimeout( function request(..){
...
}, 500) ;
再执行
js
ajax( "..", function response(..){
...
} );
最后执行
js
if ( .. ) {
...
}else ..
我们在解读这段代码的时候需要不断的分析才能得出正确的执行顺序,如果嵌套更多,那简直就是灾难。
问题2 - 缺乏信任性:控制反转
很多时候,你所写的回调函数是给别人调用的,也意味着自己程序一部分的执行控制交给了某个第三方。 至于第三方,是否调用?调用太早?调用太晚?回调次数超出预期?未能传递所需的环境和参数?吞掉可能出现的错误和异常......会出现很多开发过程中没有考虑的情况,你不得不写很多逻辑去针对这些未知情况。
😏所谓"他人即地狱",回调地狱就是这么来的。
Promise - 异步的开始,升级版回调
ES6正式提出了异步的概念Promise
如何理解Promise?
用生活中的例子来类比的话
- 使用Promise就是你去快餐店点餐(发送请求,Promise pending状态),
- Promise就是你手中的单号,在商家备餐的时候,我们可以切换去做点其他的事情,比如刷一会儿手机
- 接下来商家可能会通知你取餐(Promise fulfilled状态)
- 也可能通知你订单中的某某已经售罄(Promise rejected状态)
Promise状态
- 待定(pending) :初始状态,既没有被兑现,也没有被拒绝。
- 已兑现(fulfilled) :意味着操作成功完成。
- 已拒绝(rejected) :意味着操作失败。
Promise把回调的控制反转,再反转回来
回调函数的信任问题,主要是因为把函数控制权交给了第三方,从而产生很多信任问题。 而Promise改变了传递回调的位置,我们并不是把回调函数传给谁,而是从调用中得到一个通知(决议状态),然后由我们的代码来决定下一步做什么。
Promise一旦决议,它就永远保持在这个状态,成为了不变值,可以根据需求多次查看。我们可以安全地把这个值传递给第三方,并确信它不会被有意无意地修改。
回调函数
js
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('得到最终结果: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
Promise改写后
js
doSomething().then(function(result) {
return doSomethingElse(result); //返回一个Promise,里面的值会拆出来作为下一个promise的值
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('得到最终结果: ' + finalResult);
})
.catch(failureCallback);
Promise用法
Promise构造函数
js
//用promise构造函数生成实例
const p = new Promise(function(resolve, reject){})
Promise.resolve() 创建一个已完成的Promise
js
const p1 = Promise.resolve(foo);
妙用Promise.resolve提升代码健壮性
如果你传入的已经是真正的Promise,那么你得到的就是它本身,如果传入的是thenable, Promise.resolve(..)可以将其解封为它的非thenable值。返回一个真正的Promise,一个可以信任的值。建议:对于用Promise.resolve(..)为所有函数的返回值(不管是不是thenable)都封装一层。
thenable陷阱
js
// 这是一个thenable
var p = {
then: function(cb, errcb) {
cb( 42 );
errcb( "陷阱!" );
}
};
p.then(
function fulfilled(val){
console.log( val ); // 42
},
function rejected(err){ // 啊,不应该运行!
console.log( err ); // 陷阱!
});
使用promise.resolve解决
js
// 这是一个thenable
var p = {
then: function(cb, errcb) {
cb( 42 );
errcb( "陷阱!" );
}
};
Promise.resolve( p )
.then(
function fulfilled(val){
console.log( val ); // 42
},
function rejected(err){
// 永远不会执行
}
);
Promise.reject() 创建一个已拒绝的Promise
js
const p1 = new Promise( function(resolve, reject){
reject( "Oops" );
} );
//等同于
const p2 = Promise.reject( "Oops" );
then()和catch()
then(fn1, fn2)
fn1 - 完成回调的函数
fn2 - 拒绝回调的函数
js
const p1 = new Promise((resolve, reject) => {
resolve("成功!");
// 或
// reject(new Error("错误!"));
});
p1.then(
(value) => {
console.log(value); // 成功!
},
(reason) => {
console.error(reason); // 错误!
},
);
catch(错误处理函数)
js
const p1 = new Promise((resolve, reject) => {
resolve("成功!");
});
p1.then((value) => {
console.log(value); // "成功!"
throw new Error("噢,不!");
})
.catch((e) => {
console.error(e.message); // "噢,不!"
})
.then(
() => console.log("在 catch 后,调用链恢复了"),
() => console.log("因为有了 catch 而不会被触发"),
);
then(..)和catch(..)都会创建并返回一个新的promise,也就是说后面可以继续跟then()/catch()实现Promise链式流程控制.
⚠️这里有一个小小的问题,如果最后一个catch里面的处理函数报错了,是无法捕获到的。
Promise.all() - 全兑现才兑现,一拒绝就结束
只有当所有的任务都兑现时,整个Promise才兑现 任何一个被拒绝,Promise.all()返回的promise就会立即被拒绝,并丢弃来自其他所有promise的全部结果。
js
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// [3, 42, "foo"]
Promise.allSettled() - 接收所有的结果无论兑现/拒绝
js
Promise.allSettled([
Promise.resolve(33),
new Promise((resolve) => setTimeout(() => resolve(66), 0)),
99,
Promise.reject(new Error("一个错误")),
]).then((values) => console.log(values));
// [
// { status: 'fulfilled', value: 33 },
// { status: 'fulfilled', value: 66 },
// { status: 'fulfilled', value: 99 },
// { status: 'rejected', reason: Error: 一个错误 }
// ]
Promise.race() - 竞态:只选跑的最快的
Promise.race()总是采用第一个决议(无论兑现或者拒绝)的promise
js
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one');
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two');
});
Promise.race([promise1, promise2]).then((value) => {
console.log(value);
// promise2 更快
});
// 输出:"two"
Promise.race()妙用:解决永不决议的问题 - 设定一个超时工具跟异步请求竞争。
js
// 用于超时一个Promise的工具
function timeoutPromise(delay) {
return new Promise( function(resolve, reject){
setTimeout( function(){
reject( "Timeout! " );
}, delay );
} );
}
// 设置foo()超时 - 给它3秒钟
Promise.race([foo(),timeoutPromise( 3000 )])
.then(function(){
// foo(..)及时完成!
},
function(err){
// 或者foo()被拒绝,或者只是没能按时完成
// 查看err来了解是哪种情况
});
Promise.any() - 竞态:只选第一个成功的
Promise.any()忽略所有被拒绝的Promise,直到第一个被兑现的 Promise
js
const pErr = new Promise((resolve, reject) => {
reject("总是失败");
});
const pSlow = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "最终完成");
});
const pFast = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "很快完成");
});
Promise.any([pErr, pSlow, pFast]).then((value) => {
console.log(value);
// pFast 第一个兑现
});
// 打印:
// 很快完成
Promise.try() 和 Promise.withResolvers()
支持性不广泛,感兴趣的去MDN看看👀
生成器 - 同步书写风格革命
怎么让异步变同步风格呢?
别急,我们先来学学生成器怎么用
js
function* generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator(); // "Generator { }"
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
- function* 定义一个生成器函数
- 创建生成器实例gen
- 通过next()调用,返回yield表达式生成的值
先看一个函数
js
function foo(x, y, cb) {
ajax("http://some.url.1/? x=" + x + "&y=" + y, cb);
}
foo(11, 31, function(err, text) {
if (err) {
console.error( err );
}else {
console.log( text );
}
});
然后,把它改成generator写法
js
function foo(){
ajax("http://some.url.1/? x=" + x + "&y=" + y, function(err, data){
if(err){
//向 *main()跑出一个错误
it.throw(err);
}else{
//用收到的data恢复 *main()
it.next(data);
}
});
}
function* main(){
try{
var text = yield foo(11, 31);
console.log(text);
}catch{
console.error(err);
}
}
var it = main();
it.next(); //这里开始启动
是不是觉得代码变复杂了?No,No,No......实际上理解起来比回调要好得多,截取一段关键代码来看看
js
var text = yield foo(11, 31);
console.log(text);
yield暂停了生成器代码的执行,但并不影响异步任务foo的执行,等foo执行完了后拿到结果,再执行text赋值或者catch的错误处理。(热知识:try catch只能是同步,无法用于异步代码模式。
)
😏如果用过async, await的盆友,看到这里,应该会觉得"有那个味儿了" 猜的没错
async, await语法糖 - 终极异步解决方案
promise + generator的终极组合
我们再来回顾一下generator写法
js
function* fetch(){
yield ajax('a')
yield ajax('b')
yield ajax('c')
}
let gen = fetch();
let result1 = gen.next(); // {value: 'a', done: false}
let result2 = gen.next(); // {value: 'b', done: false}
let result3 = gen.next(); // {value: 'c', done: false}
let result4 = gen.next(); // {value: undefined, done: true}
然后用async await改写
js
async function fetch(){
await ajax('a');
await ajax('b');
await ajax('c');
}
接着逐步拆解async, await
async function中,每次异步函数的返回值是一个promise对象。异步函数可以包含零个或者多个await表达式.
没有await表达式时
⚠️不包含 await 表达式的异步函数是同步运行的
js
async function foo() {
return 1;
}
类似于
js
function foo() {
return Promise.resolve(1);
}
有await表达式时,则一定异步完成
js
async function foo() {
await 1;
}
等价于
js
function foo() {
return Promise.resolve(1).then(() => undefined);
}
await 表达式
js
function resolveAfter2Seconds() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function asyncCall() {
console.log('calling');
const result = await resolveAfter2Seconds();
console.log(result);
// Expected output: "resolved"
}
asyncCall();
await 表达式通过暂停执行使返回Promise
的函数表现得像同步函数一样,直到返回的 promise 被兑现或拒绝
- await只能在async函数/模块顶层使用
- await 后跟要等待的
Promise
实例,Thenable 对象,或任意类型的值。 - await 返回从
Promise
实例或 thenable 对象取得的处理结果
到这里就是目前Javascript的异步发展史及讲解了。谢谢您的观看~