阅读顺序推荐:
关于两种回调函数
首先回忆一下回调函数的概念,函数A 作为参数传给 函数B,由函数B自动调用,那么函数A就被称为回调函数。
也就是说目前能确定:
- 函数A是参数
- 函数A不是被开发人员调用写作
函数A()
- 函数A是被函数B调用的
接着,来讨论回调函数的两种方式:同步回调函数和异步回调函数。
同步回调函数
const arr = [1, 2, 3];
arr.forEach(item => console.log(item)); // forEach(callback)
console.log('hello');
对于数组的 forEach()
必定不会陌生了,它就接收一个 callback 作为实参。输出结果也很明显,数组元素 1 2 3 会被输出完毕后才会输出 hello。代码就是从上至下依顺序执行,同时还能看出被 forEach 调用的该 callback 就是一个同步回调函数。
异步回调函数
setTimeout(() => console.log('timeout'), 0);
console.log('hello');
对于这个输出呢?是先输出了 hello 后输出的 timeout,即便延迟时间是 0ms。
原因就在于计时器是异步任务,被放入在队列中,将来执行,所以对于 setTimeout 的 callback 就是一个异步回调函数。
1. callback
一个模拟间隔打印
假设现在需求是:每隔 2s 分别输出1、2、3,代码可能被写作:
setTimeout(() => {
console.log(1);
}, 2000);
setTimeout(() => {
console.log(2);
}, 2000);
setTimeout(() => {
console.log(3);
}, 2000);
但运行结果却是在 2s 后,同时打印出了 1 2 3。原因就在于 3 个计时器,都是异步代码,同时被扔进异步处理模块,同时计时。但需求却是等待上一次的异步操作结束以后,再进入下一个异步代码。所以代码只能改作:
setTimeout(() => {
console.log(1); // 2s 后打印 1
setTimeout(() => { // 开始计时,2s 后打印 2
console.log(2);
setTimeout(() => { // 打印完 2 后,开始计时,2s 后打印 3
console.log(3);
}, 2000);
}, 2000);
}, 2000);
现在只做 3 件事(log 1、2、3),代码就已经嵌套得没眼看了,如果有更多的需求,代码更加惨不忍睹。
在以前,只能通过俄罗斯套娃式的 callback 来实现异步,但这种方式会造成一个最明显的问题就是回调地狱(callback hell)
回调地狱:回调函数嵌套调用,外部回调函数异步处理的结果是内层回调函数执行的条件。
一个 IO 操作
假设当前存在 3 个 .txt 文件(自己去建!)需要去读取里面的内容:
file1.txt 里内容:1 file2.txt 里内容:2 file3.txt 里内容:3
const fs = require('fs'); // 引入 fs 模块
// fs.readFile(url, callback) callback 参数:错误、数据
fs.readFile('./file1.txt', function (err, data) {
console.log(data.toString());
});
fs.readFile('./file2.txt', function (err, data) {
console.log(data.toString());
});
fs.readFile('./file3.txt', function (err, data) {
console.log(data.toString());
});
(为什么图那么花?喔是因为我不是尊贵的语雀会员,所以不能上传高清动图)
每一次运行其实都会发现 log 的结果不一样,可能是输出 1 2 3、可能是输出 2 1 3、可能输出 3 1 2 等。原因就在于每一次读取的速度不同,如果想按照固定 1-2-3 的顺序输出,只能通过 callback hell 去嵌套
fs.readFile('./file1.txt', function (err, data) {
console.log(data.toString()); // 打印完 file1 的内容,再读 file2
fs.readFile('./file2.txt', function (err, data) {
console.log(data.toString()); // 打印完 file2 的内容,再读 file3
fs.readFile('./file3.txt', function (err, data) {
console.log(data.toString());
});
});
});
2. Promise
从第一种解决方案可以看到,单纯完全用 callback 确实可以实现异步的操作,但致命的缺点就是 callback hell。代码的书写嵌套导致可读性很差。ES6 中的 promise 就是 js 中进行异步编程的新的解决方案。
promise 译为 “承诺”,即当前不能完成,但在将来会完成。表示一个尚未完成且预计未来会完成的操作。
从语法上来说 promise 是一个构造函数,从功能上来说 promise 对象用来封装一个异步操作并且可以获取结果数据。参数接收一个 callback(也被叫做执行器函数用于执行异步操作),接收参数 resolve
和 reject
,当请求成功会自动调用 resolve ,失败则调用 reject。并且会将执行结果返回给 promise 的实例对象。
状态和改变状态
Promise 对象的状态有且仅有这两种情况的改变,只能改变一次:
- pending 变 resolved 即 待定状态变解决完毕
- pending 变 rejected 即待定状态变失败
let p = new Promise(() => {}); console.log(p); // Promise {<pending>} 所有 Promise 初始状态都为 pending
promise 状态的改变(2选1)
无论成功或失败,都会有一个结果数据,通常我们将成功数据称为 value,失败的数据称为 reason。分解 promise
Promise 构造函数接收一个 callback 作为参数,这个 callback 被称为执行器函数,用于执行异步操作:
执行器函数也接收两个参数new Promise(()=>{}); // 这个箭头函数就是执行器函数
resolve
和reject
,数据类型都为函数:
异步操作结果可能成功,可能失败。 如果成功,调用 resolve;如果失败,调用 reject。同时还可以传入参数:new Promise((resolve, reject)=>{ // resolve 和 reject 是两个函数 // 执行异步操作 });
在得到结果以后,会返回在实例对象 p 身上:new Promise((resolve, reject)=>{ // 执行异步操作 // 成功调用 resolve(value) // 失败调用 reject(reason) });
实例对象 p 身上有一个let p = new Promise((resolve, reject)=>{ // 执行异步操作 // 成功调用 resolve(value) // 失败调用 reject(reason) });
then()
方法,同样接收两个函数作为参数,一旦成功调用了 resolve 将会执行 then 中第一个参数,失败则执行 then 的第二个参数:// p.then(()=>{}, ()=>{}); p.then( value => {}, // 接收得到成功的 value reason => {} // 接收得到失败的 reason );
不得不提到的一点还有,对于then()
在执行完成后,还会返回新的 promise 实例。这其实也是实现 promise 链式调用的前提,放后边儿再说吧!一个 demo
分析输出: ```javascript let p = new Promise((resolve, reject) => { let a = 5; if (a >= 5) { resolve(a); } else { reject(a); } });
p.then( value => { console.log(‘成功’, value) }, reason => { console.log(‘失败’, reason) } );
分析输出:
```javascript
let p = new Promise((resolve, reject) => {
let a = 0;
if (a >= 5) {
resolve(a);
} else {
reject(a);
}
});
p.then(
value => { console.log('成功', value) },
reason => { console.log('失败', reason) }
);
扩展阅读,如果上一 part 看明白后可以不看这一 part
— 扩展开始 ——————————-Promise()
作为类,使命就是构建出它的实例,即 new Promise()
,并负责管理这些实例,每个实例都包含以下 3 种状态:
- pending:初始值,等待中,表示还没有得到结果
let pm = new Promise((resolve, reject) => {}); console.log(pm); // Promise {<pending>}
fulfilled:成功、完成,表示已经得到正确结果,可以继续向后执行
let pm = new Promise((resolve, reject) => { resolve(); // 如果有参,同样可以传参 resolve(value) }); console.log(pm); // Promise {<fulfilled>: undefined}
rejected:失败,表示虽然得到结果但不是我想要的,因此,拒绝往下执行
let pm = new Promise((resolve, reject) => { reject(); // 如果有参,同样可以传参 reject(reason) }); console.log(pm); // Promise {<rejected>: undefined}
状态的改变就会导致事件的触发:
- 当
resolve()
被调用,状态就会从 pending 变成 fulfilled ,即 等待 - 成功 - 当
reject()
被调用,状态就会从 pending 变成 rejected,即 等待 - 失败
Promise 的实例上都有一个 then()
,该方法有两个函数作为参数 .then(resolvedFunc, rejectedFunc)
- resolvedFunc 状态成功时执行的回调函数
- rejectedFunc 状态失败时执行的回调函数
这个 then() 就相当于之前回调地狱写法中嵌套的那个内层函数。
语法:
let pm = new Promise((resolve, reject) => { // 一个实例对象 pm
// 待处理的异步操作逻辑
// 处理结束后,调用 resolve 或 reject 方法
});
// 实例 pm 拥有 then 方法
pm.then(() => {}, () => {}); // 第一个回调状态成功后被执行的内容,第二个回调状态失败后执行的内容
改成链式:
new Promise((resolve, reject) => {
// 待处理的异步操作逻辑
// 处理结束后,调用相应的回调函数
}).then(() => {}, () => {});
也是一个 demo:
new Promise((resolve, reject) => {
let a = 5;
if (a > 0) {
resolve();
} else {
reject();
}
}).then(() => {
console.log('进入到了 resolve');
}, () => {
console.log('进入到了 reject');
});
使用 catch 替代 then 的第二个回调
虽然 then()
接收 2 个 callback ,但推荐只写成功的回调,而用 catch()
去实现失败的功能,使语义和结构更清晰:
let p = new Promise((resolve, reject) => {
let a = 0;
if (a >= 5) {
resolve();
} else {
reject();
}
});
p.catch(reason => console.log('执行的 reject'));
// catch 的链式
new Promise((resolve,reject) => {
}).then(callback).catch(callback);
值得注意的一点是,Promise () 参数的那个 callback(执行器函数)实际上是一个同步回调函数,只不过遇到 resolve()
或者 reject()
会调用 then()
方法,而 then()
方法其实就是以前的回调,所以 then()
中的 callback 是异步回调函数。
分析输出:
let pm = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
});
console.log('全局的 log 1号');
pm.then(() => {
console.log('then');
});
console.log('全局的 log 2号');
/*
1
2
全局的 log 1号
全局的 log 2号
then
*/
then 方法的链式调用
连续执行两个或多个异步操作是一个常见的需求。在上一个操作执行成功之后,带着返回的结果,开始下一步操作。可以通过 创造一个 Promise 链 来实现这种需求。
这就像是工厂制作蛋糕的流水线:素蛋糕胚子 -> (带着制作好的蛋糕胚子)抹奶油 -> (抹好奶油的蛋糕胚子)裱花 -> (裱好花的蛋糕)装盒
实例对象的 then()
调用完成以后,会返回一个新的 Promise 实例对象:
// 为了代码看起来精简,省略了失败的调用
let pm1 = new Promise((resolve, reject) => {
resolve();
});
let pm2 = pm1.then(() => {
console.log('pm1 的 then');
});
let pm3 = pm2.then(() => {
console.log('pm2 的 then');
});
pm3.then(() => {
console.log('pm3 的 then');
});
/*
pm1 的 then
pm2 的 then
pm3 的 then
*/
它表示 new Promise()
调用完毕会得到一个实例对象pm1
,该实例对象 pm1 拥有 .then()
,调用完毕又会得到一个新的 promise 实例 pm2
,也拥有 .then()
以此类推。所以链式调用可以改成:
new Promise((resolve, reject) => {
resolve();
}).then(() => {
console.log('pm1 的 then');
}).then(() => {
console.log('pm2 的 then');
}).then(() => {
console.log('pm3 的 then');
});
promise 的 all()
用于多个 Promise 的实例包装成一个新的 Promise 实例,方法接收一个数组,数组中传入 n 个 Promise 对象,返回一个新的 Promise 实例。
let p1 = new Promise((resolve,reject) => {
setTimeout(function(){
resolve(1);
},1000)
})
let p2 = new Promise((resolve,reject) => {
setTimeout(function(){
resolve(2);
},5000)
})
let p3 = new Promise((resolve,reject) => {
setTimeout(function(){
resolve(3);
},200)
})
Promise.all([p1,p2,p3]).then(values => {
console.log(values); // [1, 2, 3]
})
异步的执行顺序会按照 all()
参数数组里指定的顺序来执行。
当数组中所有 Promise 为成功状态,则返回的实例也为成功状态,若有一个为失败状态:
let p1 = new Promise((resolve, reject) => {
setTimeout(function () {
resolve(1);
}, 1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(function () {
resolve(2);
}, 5000)
})
let p3 = new Promise((resolve, reject) => {
setTimeout(function () {
reject(3); // 将这里改成了 reject()
}, 200)
})
Promise.all([p1, p2, p3]).then(values => {
console.log(values);
}).catch(reason => console.log(reason)); // 得到失败的 reason 就是 3
回到读取文件的 IO 操作来,需求还是:先读取文件1,读取完后,读取文件2,读取完后,读取文件3:
new Promise((resolve, reject) => {
fs.readFile('./file1.txt', (err, data) => {
console.log(data.toString()); // 读取完文件1
resolve(); // 成功,运行第 6 行的 callback
});
}).then(() => {
fs.readFile('./file2.txt', (err, data) => { // 读取文件2
console.log(data.toString());
});
}).then(() => { // 读取文件3
fs.readFile('./file3.txt', (err, data) => {
console.log(data.toString());
});
});
按照代码逻辑应该是没问题的,但运行结果却同样可能会 log 1-2-3 也可能出现 log 1-3-2。原因就在于应该把第二步操作也作为异步,第二步文件2读取完以后,再读取文件3,所以代码应该改为:
new Promise((resolve, reject) => {
fs.readFile('./data/file1.txt', (err, data) => {
console.log(data.toString());
resolve();
});
}).then(() => {
return new Promise((resolve, reject) => { // 将第二步读取文件2的操作,做完以后,返回 Promise 对象给第3步做 then 操作
fs.readFile('./data/file2.txt', (err, data) => {
console.log(data.toString());
resolve(); // 成功,调用 13 行 then
});
});
}).then(() => {
fs.readFile('./data/file3.txt', (err, data) => {
console.log(data.toString());
});
});
then() 返回的新 promise 的结果状态由什么决定
由then()
指定的回调函数执行的结果决定。
新 promise 实例的状态,主要根据前一个 Promise 对象的 then()
中的返回值来决定的,大致分为以下几种情况:
- 若抛出异常,新 promise 状态变为 rejected,reason 为抛出的异常原因
- 若返回非 promise 的值,新 promise 状态变为 resolved,value 为返回的值
- 若返回新的 promise 结果就成为新 promise 的结果
先来看到代码段:
new Promise((resolve, reject) => {
resolve(1);
}).then(
value => {
console.log('resolved1,value:', value)
},
reason => {
console.log('rejected1, reason:', reason)
}
).then(
value => {
console.log('resolved2,value:', value)
},
reason => {
console.log('rejected2, reason:', reason)
}
);
第 5 行能输出 value 为 1 自然特别好去判断了,因为第 2 行调用的是 resolve() 并且传了实参 1。
但第 10 行第二个 then() 的输出结果,会受到第一个 then() 返回的 promise 的结果影响。
当前第一个 then() 是成功的状态,所以返回的是 resolved 状态的 promise,自然调用到第 11 行。但在第一个 then() 中没有明确 return XXX,所以第 12 行的 value 保持在 undefiend,相当于在第 5 行 log 完毕后 return undefined。
那如果代码改成:
new Promise((resolve, reject) => {
resolve(1);
}).then(
value => {
console.log('resolved1,value:', value);
return 5; // 这里明确返回了 5
},
reason => {
console.log('rejected1, reason:', reason)
}
).then(
value => {
console.log('resolved2,value:', value)
},
reason => {
console.log('rejected2, reason:', reason)
}
);
// resolved1,value: 1
// resolved2,value: 5
第 6 行的 return 5 等同于 return Promise.resolve(5) 都表示返回成功状态的 promise 并且携带数据 5。
new Promise((resolve, reject) => {
resolve(1);
}).then(
value => {
console.log('resolved1,value:', value);
return Promise.reject(0); // 返回 rejected 状态的 promise
/*
其实就是:
return new Promise((resolve, reject) => {
reject(0);
});
*/
},
reason => {
console.log('rejected1, reason:', reason);
}
).then(
value => {
console.log('resolved2,value:', value)
},
reason => {
console.log('rejected2, reason:', reason)
}
);
// resolved1,value: 1
// rejected2, reason: 0
第 2 行调用 resolve() 进入到第 4 行,自然第 5 行 value 接收到 1。接着返回了一个 rejected 状态的 promise 所以在第二个 then() 中 15 行被调用,失败的 reason 就是接收到的 0。
3. async await
ES2017 提出的 async
函数,让 JavaScript 对于异步操作有了终极解决方案。No more callback hell。它们是基于 Promise 的语法糖,使异步代码更易于编写和阅读。
基础语法
当前存在:
function foo(){
await console.log(1);
}
foo(); // 报错:Uncaught SyntaxError: await is only valid in async function
即 await
只能用在一个 异步函数 中:
async function foo(){ // 声明 foo 是个异步函数
await console.log(1); // await 关键字只能放在 async 函数内部
}
foo(); // 1
// 表达式写法
let foo = async function(){
awiat console.log(1);
}
foo();
再来看下一个语法:
function foo(){
return 1;
}
console.log(foo()); // 1
会得到返回值 1 这没有什么好奇怪的,但如果改成:
async function foo(){
return 1;
}
console.log(foo()); // Promise {<fulfilled>: 1}
现在调用 foo 会得到一个 promise。这是异步函数的特征之一 —— 它保证函数的返回值为 promise。或者说 async 用于定义一个异步函数,返回一个 Promise 对象。
async
先来看一个最简单的“返回普通参数值”的写法:
async function foo() { // 声明 foo 是一个异步函数
let a = 5;
return a;
}
let pro = foo(); // 异步函数 foo 调用后返回的 pro 就是一个 Promise
pro.then(data => { // 既然 pro 是 Promise 自然可以使用 then(),形参 data 接收到 return 出的值
console.log(data);
});
即 将 async
关键字加到函数申明中,可以告诉函数,返回的是 Promise,而不是直接返回值。
await
await
可以放在任何异步的、基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值,获取的就是 Promise 函数中 resolve 或者 reject 的值。如果 await 后面并不是一个 Promise 的返回值,则会按照同步程序返回值处理
又回到文件读取的 IO 操作(一个异步函数中套所有 await)
async function readFile(){
await new Promise((resolve, reject) => { // 表示“等待一个 Promise 的结果”
fs.readFile('./file1.txt', (err, data) => {
console.log(data.toString());
resolve();
});
});
await new Promise((resolve, reject) => {
fs.readFile('./file2.txt', (err, data) => {
console.log(data.toString());
resolve();
});
});
await new Promise((resolve, reject) => {
fs.readFile('./file3.txt', (err, data) => {
console.log(data.toString());
resolve();
});
});
}
readFile();
也可以写作:(将每一步封装成函数,用一个“主函数”分别去调用得到每一步的值)
function readFile1() {
return new Promise((resolve, reject) => {
fs.readFile('./file1.txt', (err, data) => {
console.log(data.toString());
resolve();
});
})
}
function readFile2() {
return new Promise((resolve, reject) => {
fs.readFile('./file2.txt', (err, data) => {
console.log(data.toString());
resolve();
});
})
}
function readFile3() {
return new Promise((resolve, reject) => {
fs.readFile('./file3.txt', (err, data) => {
console.log(data.toString());
resolve();
});
})
}
async function main(){
await readFile1();
await readFile2();
await readFile3();
}
main();
那如果仿照上面的写法,实现“蛋糕的制作过程”呢?(每隔 2s 分别打印:第一步制作蛋糕胚子 第二步涂抹奶油 第三步裱花 第四部包装出售)
function make() { // 制作
return new Promise((resolve) => {
setTimeout(() => {
console.log('第一步制作蛋糕胚子');
resolve();
}, 2000);
});
}
function cream() { // 涂奶油
return new Promise((resolve) => {
setTimeout(() => {
console.log('第二步涂抹奶油');
resolve();
}, 2000);
});
}
function decoration() { // 装饰
return new Promise((resolve) => {
setTimeout(() => {
console.log('第三步裱花');
resolve();
}, 2000);
});
}
function sale() { // 出售
return new Promise((resolve) => {
setTimeout(() => {
console.log('第四部包装出售');
resolve();
}, 2000);
});
}
async function main() {
await make();
await cream();
await decoration();
await sale();
}
main();