关于 JavaScript 异步进化史(Promise)(转)

下面关于promise的说法中,错误的是()

A. resolve()和reject都是直接生成一个进入相应状态的promise对象,其参数就是进入相应状态时传递过去的参数,可以在完成回调的参数中得到

B. Promise.resolve(value),Promise.reject(reason)是Promise构造器上还直接提供了一组静态方法

C. 在调用then方法或者catch方法时都是异步进行的,但是执行速度比较快

D. Promise构造器的prototype上还有两个方法,分别是then和catch。这两个方法的参数也是回调函数,这些函数会在Promise实例进入不同状态后被调用。Then对应到resolve,catch对应到reject

答案:D

JS 中最基础的异步调用方式是 callback,它将回调函数 callback 传给异步 API,由浏览器或 Node 在异步完成后,通知 JS 引擎调用 callback。对于简单的异步操作,用 callback 实现,是够用的。但随着负责交互页面和 Node 出现,callback 方案的弊端开始浮现出来。 Promise 规范孕育而生,并被纳入 ES6 的规范中。后来 ES7 又在 Promise 的基础上将 async 函数纳入标准。

同步与异步

通常,代码是由上往下依次执行的。如果有多个任务,就必需排队,前一个任务完成,后一个任务才会执行。这种执行模式称之为:同步(synchronous)。新手容易把计算机用语中的同步,和日常用语中的同步弄混淆。如,“把文件同步到云端”中的同步,指的是“使...保持一致”。而在计算机中,同步指的是任务从上往下依次执行的模式。比如:

A();
B();
C();

在上述代码中,A、B、C 是三个不同的函数,每个函数都是一个不相关的任务。在同步模式下,计算机会先执行 A 任务,再执行 B 任务,最后执行 C 任务。在大部分情况,同步模式都没问题。但是如果 B 任务是一个耗时很长网络的请求,而 C 任务恰好是展现新页面,B 与 C 没有依赖关系。这就会导致网页卡顿的现象。有一种解决方案,将 B 放在 C 后面去执行,但唯一有些不足的是,B 的网络请求会迟一些再发送。

还有另一种更完美解决方案,将 B 任务分成的两个部分。一部分是,立即执行网络请求的任务;另一部分是,在请求数据回来后执行的任务。这种一部分在立即执行,另一部分在未来执行的模式称为 异步(asynchronous) 。伪代码如下:

A();
// 在现在发送请求 
ajax('url1',function B() {
  // 在未来某个时刻执行
})
C();
// 执行顺序 A => C => B

实际上,JavaScript 引擎先执行了调用了浏览器的网络请求接口的任务(一部分任务),再由浏览器发送网络请求并监听请求返回(这个任务不由 JavaScript 引擎执行,而是浏览器);等请求放回后,浏览器再通知 JavaScript 引擎,开始执行回调函数中的任务(另一部分)。JavaScript 异步能力的本质是浏览器或 Node 的多线程能力。

callback 

未来执行的函数通常也叫 callback。使用 callback 的异步模式,解决了阻塞的问题,但是也带来了一些其他问题。在最开始,我们的函数是从上往下书写的,也是从上往下执行的,这种“线性”模式,非常符合我们的思维习惯,但是现在却被 callback 打断了!在上面一段代码中,现在它跳过 B 任务先执行了 C任务!这种异步“非线性”的代码会比同步“线性”的代码,更难阅读,因此也更容易滋生 BUG。

试着判断下面这段代码的执行顺序,你会对“非线性”代码比“线性”代码更难以阅读,体会更深。

A();

ajax('url1', function(){
    B();

    ajax('url2', function(){
        C();
    }
    D();
    
});
E();
// A => E => B => D => C

这段代码中,从上往下执行的顺序被 Callback 打乱了。我们的阅读代码视线是A => B => C => D => E,但是执行顺序却是A => E => B => D => C,这就是非线性代码带来的糟糕之处。

通过将ajax后面执行的任务提前,可以更容易看懂代码的执行顺序。虽然代码因为嵌套看起来不美观,但现在的执行顺序却是从上到下的“线性”方式。这种技巧在写多重嵌套的代码时,是非常有用的。

A();
E();

ajax('url1', function(){
    B();
    D();

    ajax('url2', function(){
        C();
    }
    
});
// A => E => B => D => C

上一段代码只有处理了成功回调,并没处理异常回调。接下来,把异常处理回调加上,再来讨论代码“线性”执行的问题。

A();

ajax('url1', function(){
    B();

    ajax('url2', function(){
        C();
    },function(){
        D();
    });
    
},function(){
    E();
    
});

加上异常处理回调后,url1的成功回调函数 B 和异常回调函数 E,被分开了。这种“非线性”的情况又出现了。

在 node 中,为了解决的异常回调导致的“非线性”的问题,制定了错误优先的策略。node 中 callback 的第一个参数,专门用于判断是否发生异常 

A();

get('url1', function(error){
    if(error){
        E();
    }else {
        B();

        get('url2', function(error){
            if(error){
                D();
            }else{
                C();
            }
        });
    }
});

到此,callback 引起的“非线性”问题基本得到解决。遗憾的是,使用 callback 嵌套,一层层if else和回调函数,一旦嵌套层数多起来,阅读起来不是很方便。此外,callback 一旦出现异常,只能在当前回调函数内部处理异常,并没有一个整体的异常触底方案。

promise

在 JavaScript 的异步进化史中,涌现出一系列解决 callback 弊端的库,而 Promise 成为了最终的胜者,并成功地被引入了 ES6 中。它将提供了一个更好的“线性”书写方式,并解决了异步异常只能在当前回调中被捕获的问题。

Promise 就像一个中介,它承诺会将一个可信任的异步结果返回。首先 Promise 和异步接口签订一个协议,成功时,调用resolve函数通知 Promise,异常时,调用reject通知 Promise。另一方面 Promise 和 callback 也签订一个协议,由 Promise 在将来返回可信任的值给then和catch中注册的 callback。

// 创建一个 Promise 实例(异步接口和 Promise 签订协议)
var promise = new Promise(function (resolve,reject) {
  ajax('url',resolve,reject);
});

// 调用实例的 then catch 方法 (成功回调、异常回调与 Promise 签订协议)
promise.then(function(value) {
  // success
}).catch(function (error) {
  // error
})

Promise 是个非常不错的中介,它只返回可信的信息给 callback。它对第三方异步库的结果进行了一些加工,保证了 callback 一定会被异步调用,且只会被调用一次。比如在使用第三方库的时候,由于某些原因,(假的)“异步”接口不可靠,它执行了同步代码,而没有进入异步逻辑,如例 :  

var promise1 = new Promise(function (resolve) {
  // 由于某些原因导致“异步”接口,被同步执行了
  if (true ){
    // 同步代码
    resolve('B');
  } else {
    // 异步代码
    setTimeout(function(){
      resolve('B');
    },0)
  }

});

// promise依旧会异步执行
promise1.then(function(value){
    console.log(value)
});

console.log('A');
// A => B (先 A 后 B)

再比如,由于某些原因,异步接口不可靠,resolve 或 reject 被执行了两次。但 Promise 只会通知 callback ,第一次异步接口返回的结果。如下例:

var promise2 = new Promise(function (resolve) {
  // resolve 被执行了 2 次
  setTimeout(function(){
    resolve("第一次");
  },0)
  setTimeout(function(){
    resolve("第二次");
  },0)
});

// 但 callback 只会被调用一次,
promise2.then(function(msg){
    console.log(msg) // "第一次"
    console.log('A')
});
// A (只有一个)  

介绍完 Promise 的特性后,来看看它如何利用链式调用,解决异步代码可读性的问题的。式调用指的是:函数 return 一个可以继续执行的对象,该对象可以继续调用,并且 return 另一个可以继续执行的对象,如此反复达到不断调用的结果。  

// return 一个可以继续执行的 Promise 对象
var fetch = function(url){
    return new Promise(function (resolve,reject) {
        ajax(url,resolve,reject);
    });
}

A();
fetch('url1').then(function(){
    B();
    // 返回一个新的 Promise 实例
    return fetch('url2');
}).catch(function(){
    C();
    // 异常的时候也可以返回一个新的 Promise 实例
    return fetch('url2');
    // 使用链式写法调用这个新的 Promise 实例的 then 方法
}).then(function() {
    // 可以继续 return,也可以不继续 return,结束链式调用
    D();
})
// A B C D (顺序执行)

如此反复,不断返回一个 Promise 对象,再采用链式调用的方式不断地调用。使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。

Promise 解决的另外一个难点是 callback 只能捕获当前错误异常。Promise 和 callback 不同,每个 callback 只能知道自己的报错情况,但 Promise 代理着所有的 callback,所有 callback 的报错,都可以由 Promise 统一处理。所以,可以通过catch来捕获之前未捕获的异常。

Promise 解决了 callback 的异步调用问题,但 Promise 并没有摆脱 callback,它只是将 callback 放到一个可以信任的中间机构,这个中间机构去链接我们的代码和异步接口。

异步(async)函数

异步(async)函数是 ES7 的一个新的特性,它结合了 Promise,让我们摆脱 callback 的束缚,直接用类同步的“线性”方式,写异步函数。

声明异步函数,只需在普通函数前添加一个关键字 async 即可,如async function main(){} 。在异步函数中,可以使用await关键字,表示等待后面表达式的执行结果,一般后面的表达式是 Promise 实例。

async function main{
    // timer 是在上一个例子中定义的
    var value = await timer(100);
    console.log(value); // done (100ms 后返回 done)
}

main();

异步函数和普通函数一样调用 main() 。调用后,会立即执行异步函数中的第一行代码 var value = await timer(100)。等到异步执行完成后,才会执行下一行代码。

除此之外,异步函数和其他函数基本类似,它使用try...catch来捕捉异常。也可以传入参数。但不要在异步函数中使用return来返回值。

var  timer = new Promise(function create(resolve,reject) {
  if(typeof delay !== 'number'){
    reject(new Error('type error'));
  }
  setTimeout(resolve,delay,'done');
});

async function main(delay){
  try{
    var value1 = await timer(delay);
    var value2 = await timer('');
    var value3 = await timer(delay);
  }catch(err){
    console.error(err);
      // Error: type error
      //   at create (<anonymous>:5:14)
      //   at timer (<anonymous>:3:10)
      //   at A (<anonymous>:12:10)
  }
}
main(0);

异步函数也可以被当作值,传入普通函数和异步函数中执行。但是在异步函数中,使用异步函数时要注意,如果不使用await,异步函数会被同步执行。  

async function main(delay){
    var value1 = await timer(delay);
    console.log('A')
}

async function doAsync(main){
  main(0);
  console.log('B')
}

doAsync(main);
// B A

这个时候打印出来的值是 B A。说明 doAsync 函数并没有等待 main 的异步执行完毕就执行了 console。如果要让 console 在 main 的异步执行完毕后才执行,我们需要在main前添加关键字await。

async function main(delay){
    var value1 = await timer(delay);
    console.log('A')
}

async function doAsync(main){
    await main(0);
    console.log('B')
}

doAsync(main);
// A B

由于异步函数采用类同步的书写方法,所以在处理多个并发请求,新手可能会像下面一样书写。这样会导致url2的请求必需等到url1的请求回来后才会发送。

var fetch = function (url) {
  return new Promise(function (resolve,reject) {
    ajax(url,resolve,reject);
  });
}

async function main(){
  try{
    var value1 = await fetch('url1');
    var value2 = await fetch('url2');
    conosle.log(value1,value2);
  }catch(err){
    console.error(err)
  }
}

main();

使用Promise.all的方法来解决这个问题。Promise.all用于将多个Promise实例,包装成一个新的 Promis e实例,当所有的 Promise 成功后才会触发Promise.all的resolve函数,当有一个失败,则立即调用Promise.all的reject函数。

var fetch = function (url) {
  return new Promise(function (resolve,reject) {
    ajax(url,resolve,reject);
  });
}

async function main(){
  try{
    var arrValue = await Promise.all[fetch('url1'),fetch('url2')];
    conosle.log(arrValue[0],arrValue[1]);
  }catch(err){
    console.error(err)
  }
}

main();

  

原文: https://segmentfault.com/a/1190000006138882 

 

  

  

  

posted @ 2017-08-13 20:33  谨言&慎独_yk  阅读(285)  评论(0编辑  收藏  举报