一、Promise里的代码为什么比setTimeout先执行?
1、ES3版本以及之前,JavaScript本身还没有异步执行代码的能力,宿主环境传递给JavaScript引擎,然后按顺序执行,由宿主发起任务。
2、ES5之后,JavaScript引入了Promise,不需要浏览器的安排,JavaScript引擎本身也可以发起任务。
3、采纳JSC引擎术语,把宿主发起的任务称为宏观任务,把JavaScript引擎发起的任务称为微观任务。
宏观和微观任务
1、用伪代码来表示:跑在独立线程中的循环
while(TRUE) {
r = wait();
execute(r);
}
2、整个循环做的事情基本上就是反复 等待 - 执行,这里的执行过程,其实都是一个宏观任务。可以大致理解为:宏观任务的队列就相当于时间循环。 3、在宏观任务中,JavaScript 的 Promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列。 例如:Promise 永远在队列尾部添加微观任务。setTimeout 等宿主 API,则会添加宏观任务。
Promise
JavaScript 语言提供的一种标准化的异步管理方式,当进行 io、等待或者其它异步操作的函数,不返回真实结果,而返回一个承诺,函数的调用方可以在合适的时机,选择等待这个承诺兑现。
1、基本用法示例
function sleep(duration) {
return new Promise(function(resolve, reject) {
setTimeout(resolve,duration);
})
}
sleep(1000).then( ()=> console.log("finished"));
Promise 的 then 回调是一个异步的执行过程。
2、Promise 函数中的执行顺序
var r = new Promise(function(resolve, reject){
console.log("a");
resolve()
});
r.then(() => console.log("c"));
console.log("b")
// 输出顺序:a b c
3、setTimeout 混用的 Promise
var r = new Promise(function(resolve, reject){
console.log("a");
resolve()
});
setTimeout(()=>console.log("d"), 0)
r.then(() => console.log("c"));
console.log("b")
// 输出顺序:a b c d
Promise 产生的是 JavaScript 引擎内部的微任务,而 setTimeout 是浏览器 API,它产生宏任务。所以d 必定在 c 之后输出。
4、一个耗时 1 秒的 Promise
setTimeout(()=>console.log("d"), 0)
var r = new Promise(function(resolve, reject){
resolve()
});
r.then(() => {
var begin = Date.now();
while(Date.now() - begin < 1000);
console.log("c1")
new Promise(function(resolve, reject){
resolve()
}).then(() => console.log("c2"))
});
// 输出顺序:c1 c2 d
这个例子很好的解释了微任务优先的原理。
5、如何分析异步执行的顺序
- 1、首先我们分析有多少个宏任务
- 2、在每个宏任务中,分析有多少个微任务
- 3、根据调用次序,确定宏任务中的微任务执行次序
- 4、根据宏任务的触发规则和调用次序,确定宏任务的执行次序
- 5、确定整个顺序
function sleep(duration) {
return new Promise(function(resolve, reject) {
console.log("b");
setTimeout(resolve,duration);
})
}
console.log("a");
sleep(5000).then(()=>console.log("c"));
// 输出顺序:a b c(c要等5秒)
第一个宏观任务中,包含了先后同步执行的 console.log("a"); 和 console.log("b");。 setTimeout 后,第二个宏观任务执行调用了 resolve,然后 then 中的代码异步得到执行,调用了 console.log("c")。
新特性:async/await
async/await 是 ES7 新加入的特性,它提供了用 for、if 等代码结构来编写异步的方式,并且运行时基础是 Promise。 1、async 函数是在 function 关键字之前加上 async 关键字,这样就定义了一个 async 函数,可以在其中使用 await 来等待一个 Promise。
function sleep(duration) {
return new Promise(function(resolve, reject) {
setTimeout(resolve,duration);
})
}
async function foo(){
console.log("a")
await sleep(2000)
console.log("b")
}
foo();
// 输出顺序:a b(b要等两秒)
async 嵌套
function sleep(duration) {
return new Promise(function(resolve, reject) {
setTimeout(resolve,duration);
})
}
async function foo(name){
await sleep(2000)
console.log(name)
}
async function foo2(){
await foo("a");
await foo("b");
}
foo2();
// 输出顺序:a(a等两秒) b(b也等两秒)
多个await同时执行方法
function Fun(){
return new Promise((resolve) => {
setTimeout(resolve('fun'),3000);
})
}
function Fun2(){
return new Promise((resolve) => {
setTimeout(resolve('fun2'),3000);
})
}
let [fun1, fun2] = await Promise.all([Fun(),Fun2()]);
console.log(fun1);
console.log(fun2);
我们的异步操作async await 如果错了就不会继续执行,如果我想让他继续执行应该怎么做? try cache?
function Fun(){
return new Promise((resolve,reject) => {
setTimeout(reject(new Error('你错了')),3000);
})
}
function Fun2(){
return new Promise((resolve) => {
setTimeout(resolve,3000);
})
}
async function g() {
// try{
await Fun();
// }catch(e){
// console.log('错了');
// }
console.log(123);
await Fun2();
console.log(123);
}
g();
除了try catch 还可以怎么样呢?
function Fun(){
return new Promise((resolve,reject) => {
setTimeout(reject(new Error('你错了')),3000);
})
}
function Fun2(){
return new Promise((resolve) => {
setTimeout(resolve,3000);
})
}
async function g() {
await Fun().catch((e)=>{
console.log(e);
});
console.log(123);
await Fun2();
console.log(123);
}
g();
以上是基础用法,现在我们就开始试着去理解这些现象,和内层的原理 但是想要了解 这些东西我们需要很多的基础知识储备,有了这些知识储备,其实也是很好理解的。现在让我们开始整理我们需要知道的知识点
首先去查 async await ,查到的结果是
ES2016 标准引入了 async 函数,使得异步操作变得更加方便。 (也就是说 async 是 es7 的内容)
async 函数就是 Generator 函数的语法糖。
async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await
Generator 函数
generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。 generator由function * 定义(注意多出的 * 号),并且,除了return语句,还可以用yield返回多次。 我们以一个著名的斐波那契数列为例,它由0,1开头: 0 1 1 2 3 5 8 13 21 34 ... 要编写一个产生斐波那契数列的函数,可以这么写:
function fib(max) {
var
t,
a = 0,
b = 1,
arr = [0, 1];
while (arr.length < max) {
[a, b] = [b, a + b];
arr.push(b);
}
return arr;
}
// 测试:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
函数只能返回一次,所以必须返回一个Array。但是,如果换成generator,就可以一次返回一个数,不断返回多次。用generator改写如下:
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}
直接调用试试:
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
直接调用一个generator和调用函数不一样,fib(5)仅仅是创建了一个generator对象,还没有去执行它。 调用generator对象有两个方法,一是不断地调用generator对象的next()方法:
var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}
generator对象的next方法的运行逻辑如下。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
当执行到done为true时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。
第二个方法是直接用for ... of循环迭代generator对象,这种方式不需要我们自己判断done:
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}
for (var x of fib(10)) {
console.log(x); // 依次输出0, 1, 1, 2, 3, ...
}
generator和普通函数相比,有什么用?
因为generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能。例如,用一个对象来保存状态,得这么写:
var fib = {
a: 0,
b: 1,
n: 0,
max: 5,
next: function () {
var
r = this.a,
t = this.a + this.b;
this.a = this.b;
this.b = t;
if (this.n < this.max) {
this.n ++;
return r;
} else {
return undefined;
}
}
};
用对象的属性来保存状态,相当繁琐。 generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。 没有generator之前的黑暗时代,用AJAX时需要这么写代码:
ajax('http://url-1', data1, function (err, result) {
if (err) {
return handle(err);
}
ajax('http://url-2', data2, function (err, result) {
if (err) {
return handle(err);
}
ajax('http://url-3', data3, function (err, result) {
if (err) {
return handle(err);
}
return success(result);
});
});
});
回调越多,代码越难看。
有了generator的美好时代,用AJAX时可以这么写:
try {
r1 = yield ajax('http://url-1', data1);
r2 = yield ajax('http://url-2', data2);
r3 = yield ajax('http://url-3', data3);
success(r3);
}
catch (err) {
handle(err);
}
看上去是同步的代码,实际执行是异步的。 下面我们在回到Promise,看下面两个题目:
题目:
new Promise(resolve => { // p1
resolve(1);
// p2
Promise.resolve().then(() => {
console.log(2); // t1
});
console.log(4)
}).then(t => {
console.log(t); // t2
});
console.log(3);
答案 & 解析:
// 解析:
// 1. new Promise(fn), fn 立即执行,所以先输出 4;
// 2. p1和p2的Promise在执行then之前都已处于resolve状态,
// 故按照then执行的先后顺序,将t1、t2放入microTask中等待执行;
// 3. 完成执行console.log(3)后,macroTask执行结束,然后microTask
// 中的任务t1、t2依次执行,所以输出3、2、1;
// 答案:
// 4 3 2 1
题目:
Promise.reject('a')
.then(()=>{
console.log('a passed');
})
.catch(()=>{
console.log('a failed');
});
Promise
.reject('b')
.catch(()=>{
console.log('b failed');
})
.then(()=>{
console.log('b passed');
})
答案 & 解析:
// 解析:p.then(fn)、p.catch(fn)中的fn都是异步执行,上述代码可理解为:
// setTimeout(function(){
// setTimeout(function(){
// console.log('a failed');
// });
// });
// setTimeout(function(){
// console.log('b failed');
//
// setTimeout(function(){
// console.log('b passed');
// });
// });
// 答案:b failed
// a failed
// b passed