JavaScript学习笔记(5) 异步-- Promise

写在前面

异步编程对Javascript语言非常重要,在Javascript的发展道路上,异步编程的方法也是一直在不断更新。关于这方面的知识,网上已经有很多成熟的教程和讲解,我将对这些教程进行整理和归纳,整理出异步JS异步编程的几种解决方法。

Javascript的异步执行

Javascript的执行环境是单线程的,所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,这就和超市等待付款一样,前面的人还没有结账,后面的人就只能等着。这种模式的坏处很明显,就是如果有一个任务耗时很长,就会拖延整个程序的执行。为了解决这个问题,Javascript将任务的执行模式分为两种:同步(synchronous)和异步(Asynchronous)。

  • 同步模式就是前面提到的这种,后一个任务等待前一个任务结束再执行,程序执行的顺序与任务排列的顺序是一致的,同步的。
  • 异步模式,通俗点说,就是前面排队的人告诉后面排队的一个准确时间,这样后面的人就可以利用这段时间去干些别的事情。异步模式可以通过回调函数实现,假设我们需要进行一个耗时的数据请求,在对外部数据发送请求后,程序的执行权将会交给别的任务,一直等到外部数据返回以后,系统才会通知执行回调函数,而回调函数中通常会包括对获取的数据的处理方法。所以,在这个模式下,程序的执行顺序和任务的排列顺序是不一致的,异步的。

Promise

Promise是异步编程的一种解决方案,ES6原生提供了Promise对象。

特点

  • Promise对象有三种状态:Pending(进行中),Resolved(已完成),Rejected(已失败),只有异步操作的结果可以决定当前是哪种状态。
  • Promise的状态改变可能有两种情况:pending->Resolved或者pending->Rejected,一旦发生,状态就会保持不变。

基本用法

  • Promise对象是一个构造函数,用来生成Promise实例。
  • Promise构造函数接受一个函数作为参数,该函数的两个参数分别为resolve和reject,他们是两个函数,由JS引擎提供,不用自己部署。
  • resolve函数在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
  • reject函数在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
  • then方法可以接受两个回调函数作为参数,第一个回调函数时Promise对象的状态变为resolved时候调用,第二个是Promise对象状态变为reject时调用,第二个函数可选。
  • 如果Promise对象状态变为reject时,会调用catch方法指定的回调函数处理这个错误,所以,除了定义then的第二个参数以外,也可以定义catch方法的回调函数来处理Promise抛出的错误。
例1 (简单例子)
function test(resolve,reject){
    var timeout=Math.random()*2;
    console.log('设置timeout为'+timeout+'秒');
    setTimeout(function () {
      if(timeout<1){
        console.log('call resolve()');
        resolve(" resolved");
      }else {
        console.log('call reject()');
        reject(" rejected");
      }
    }, timeout*1000);
}

//写法1
var p1=new Promise(test);
p1.then(function(result){
  console.log("成功"+result);
},function(result){
  console.log("失败"+result);
});

//写法2
var p1=new Promise(test);
var p2=p1.then(function(result){
  console.log("成功"+result);
});

var p3=p2.catch(function(result){
  console.log("失败"+result);
})

/*
输出:
   设置timeout为1.3983493377635763秒
   call reject()
   失败 rejected
或者:
   设置timeout为0.8257771710631485秒
   call resolve()
   成功 resolved
  */

这个例子中p1是promise实例,方法一和方法二所表达的意思是一样的,只是写法不同,方法1用的就是上面说过的给then传递两个回调函数作为参数的写法。方法二只将then用于函数执行成功的情况,而使用catch来处理函数执行不成功的情况。一般倾向于使用catch方法定义reject状态的回调函数。
上述的test函数执行成功的情况下,即随机数小于1的情况下,我们将调用resolve(' resolved'),在执行失败的情况下,将调用reject(" rejected")。变量p1是一个Promise对象,负责执行test函数。当test函数执行成功时,then方法指定的回调函数,将在当前脚本所有同步任务执行完后执行,也就是过了随机数产生的秒数以后执行这里的console.log("成功"+result)。相同的,当test函数执行失败时,catch方法中指定的回调函数也会在test中的所有任务执行完后执行。
上面提到的console.log("成功"+result)中的result其实就是等于" resolved",因为如果resolve函数和reject函数带有参数,那么它们的参数就会被传递给回调函数。通常情况下,reject函数的参数会使Error对象的实例,表示抛出的错误;resolve函数的参数可能是一个正常值,也有可能是另一个Promise实例,表示异步操作的结果可能是另一个异步操作。接下来就举一个执行若干个异步任务的例子.

例2 (若干个异步任务)
function add(input){
  return new Promise(function(resolve,reject){
    var result=input+input;
    console.log("add");
    setTimeout(function(){
      if(result<1000){
        resolve(result);
      }else {
        reject(result);
      }
    },500);
  })
}

function multiply(input){
  return new Promise(function(resolve,reject){
    var result=input*input;
    console.log("multiply");
    setTimeout(resolve,500,result);
  })
}

var p=new Promise(function(resolve,result){
  console.log("start new promise");
  resolve(10);
})

p.then(add).then(multiply).then(add).then(add).then(function(result){
  console.log("小于1000"+result);
},function(result){
  console.log("大于1000"+result);
});
//输出:大于1000,1600

上面这段代码首先定义了add和multiply,然后在add()函数中定义了成功和失败条件,就是当数字小于1000的时候未成功,大于1000的时候为失败。这里最重要的点是add和multiply都会返回一个Promise对象,所以在第一个Promise对象p运行结束后,会开始调用函数add,并将10传递给函数add当作input,此时multiply方法数就会等待add返回的这个新的promise对象发生变化,依次类推,最后的一个then方法指定的回调函数就会等待最后一个add返回的新的promise对象状态发生变化,如果变为Promise对象的状态为成功,就会调用console.log("小于1000"+result);,反之调用console.log("大于1000"+result);

例3 (all和race)

all方法将用于多个Promise实例,包装成一个新的Promise实例。

  var p=Promise.all([p1,p2,p3]);

在使用all的情况下,p的状态由p1,p2,p3决定:

  • 只有p1,p2,p3的状态都变成Resolved,p的状态才会变成Resolved。
  • p1,p2,p3中只要有一个被Rejected,p的状态就变成了Rejected。
  var p1=new Promise(function(resolve,reject){
    setTimeout(resolve,1000,"p1 finish in 1s");
  });

  var p2=new Promise(function(resolve,reject){
    setTimeout(resolve,2000,"p2 finish in 2s");
  });

  Promise.all([p1,p2]).then(function(result){
    console.log("end"+result);
  });
  //直到2s后'p1 finish in 1s'和‘p2 finish in 2s’会被同时输出

上例中,因为只有p1,p2都为Resolved,由它们包装成的新的Promise实例的状态才会变为Resolved,所以直到2秒以后,p1和p2的返回值才会被同时传递给新的promise实例的回调函数。

race方法和all类似,同样将多个Promise实例包装成一个新的Promise实例。

  var p=Promise.race([p1,p2,p3]);

在使用race的情况下,只要p1,p2,p3之中有一个率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数。

var p1=new Promise(function(resolve,reject){
  setTimeout(resolve,1000,"p1 finish in 1s");
});

var p2=new Promise(function(resolve,reject){
  setTimeout(resolve,2000,"p2 finish in 2s");
});

Promise.race([p1,p2]).then(function(result){
  console.log("end"+result);
});
//1秒后'p1 finish in 1s'和‘p2 finish in 2s’就会被同时输出

上例中因为p1执行的较快,它的状态会率先变为Resolved,所以p1的返回值,就传递给了新的promise实例的回调函数。

Promise应用于Ajax

使用Promise简化Ajax异步处理:

function ajax(method,url,data){
  var request=new XMLHttpRequest();
  return new Promise(function(resolve,reject{
    request.onreadystatechange=function(){
      if(request.readyState==4){
        if(request.status==200){
          resolve(request.responseText);
        }else {
          reject(request.status);
        }
      }
    });
  request.open(method,url);
  request.send(data);

  })
}

var p=ajax('POST',someUrl);
p.then(function(text){
  alert(text);//成功获取到数据
}).catch(function(status){
  alert(status); //请求数据失败,获得相应代码
});

这里的method可以是'GET'或者'POST',url是php或者asp文件的地址。

总结

关于Promise的大部分知识已经在本文涵盖到,不得不说廖雪峰和阮一峰老师的讲解很全面,但也许有些同学对于ES6并不熟悉,直接看阮一峰的Promise这章会有点吃力,所以笔者尽可能地在他们的基础上解释地更加细致一些,当然,想要全面地了解Promise的所有内容,还是要静下心来阅读ES6入门这本书。这是JS异步的第一篇,接下来仍会介绍别的JS异步的解决方案。

推荐阅读

  • 阮一峰-ES6入门
  • 廖雪峰的博客-promise

你可能感兴趣的:(JavaScript学习笔记(5) 异步-- Promise)