重学前端

168 阅读8分钟

一、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