ES6总结--Promise 、Generator 、Async/Await

1、Promise

开篇首先设想一个日常开发常常会遇到的需求:在多个接口异步请求数据,然后利用这些数据来进行一系列的操作。一般会这样去写:

$.ajax({
    url: '......',
    success: function (data) {
        $.ajax({
            // 要在第一个请求成功后才可以执行下一步
            url: '......',
            success: function (data) {
                 // ......
            }
        });
    }
});

这样的写法的原理是,当执行一些异步操作时,我们需要知道操作是否已经完成,所有当执行完成的时候会返回一个回调函数,表示操作已经完成。

使用回调函数的形式理解起来并不困难,但是实际的应用当中会有以下的缺点:

在需要多个操作的时候,会导致多个回调函数嵌套,导致代码不够直观,就是常说的 Callback Hell。
如果几个异步操作之间并没有前后顺序之分(例如不需要前一个请求的结果作为后一个请求的参数)时,同样需要等待上一个操作完成再实行下一个操作。
为了解决上述的问题,Promise 对象应运而生,ES6将其写进了语言标准。

什么是 Promise

一个 Promise 对象可以理解为一次将要执行的操作(常常被用于异步操作),使用了 Promise 对象之后可以用一种链式调用的方式来组织代码,让代码更加直观。而且由于 Promise.all 这样的方法存在,可以让同时执行多个操作变得简单。接下来就来简单介绍 Promise 对象。

resolve 和 reject

首先来看一段使用了 Promise 对象的代码。

function helloWorld (ready) {
    return new Promise(function (resolve, reject) {
        if (ready) {
            resolve("Hello World!");
        } else {
            reject("Good bye!");
        }
    });
}

helloWorld(true).then(function (message) {
    alert(message);
}, function (error) {
    alert(error);
});

上面的代码实现的功能非常简单,helloWord 函数接受一个参数,如果为 true 就打印 “Hello World!”,如果为 false 就打印错误的信息。helloWord 函数返回的是一个 Promise 对象。

在 Promise 对象当中有两个重要方法————resolve 和 reject。

resolve 方法可以使 Promise 对象的状态改变成成功,同时传递一个参数用于后续成功后的操作,在这个例子当中就是 Hello World! 字符串。

reject 方法则是将 Promise 对象的状态改变为失败,同时将错误的信息传递到后续错误处理的操作。

Promise 的三种状态

上面提到了 resolve 和 reject 可以改变 Promise 对象的状态,那么它究竟有哪些状态呢?

Promise 对象有三种状态:

Fulfilled 可以理解为成功的状态
Rejected 可以理解为失败的状态
Pending 既不是 Fulfilld 也不是 Rejected 的状态,可以理解为 Promise 对象实例创建时候的初始状态
helloWorld 的例子中的 then 方法就是根据 Promise 对象的状态来确定执行的操作,resolve 时执行第一个函数(onFulfilled),reject 时执行第二个函数(onRejected)。

then 和 catch

then

helloWorld 的例子当中利用了 then(onFulfilld, onRejected) 方法来执行一个任务打印 “Hello World!”,在多个任务的情况下 then 方法同样可以用一个清晰的方式完成。

function printHello (ready) {
    return new Promise(function (resolve, reject) {
        if (ready) {
            resolve("Hello");
        } else {
            reject("Good bye!");
        }
    });
}

function printWorld () {
    alert("World");
}

function printExclamation () {
    alert("!");
}

printHello(true)
    .then(function(message){
        alert(message);
    })
    .then(printWorld)
    .then(printExclamation);

上述例子通过链式调用的方式,按顺序打印出了相应的内容。then 可以使用链式调用的写法原因在于,每一次执行该方法时总是会返回一个 Promise 对象。另外,在 then onFulfilled 的函数当中的返回值,可以作为后续操作的参数,因此上面的例子也可以写成:

printHello(true).then(function (message) {
    return message;
}).then(function (message) {
    return message  + ' World';
}).then(function (message) {
    return message + '!';
}).then(function (message) {
    alert(message);
});

同样可以打印出正确的内容。

catch

catch 方法是 then(onFulfilled, onRejected) 方法当中 onRejected 函数的一个简单的写法,也就是说可以写成 then(fn).catch(fn),相当于 then(fn).then(null, fn)。使用 catch 的写法比一般的写法更加清晰明确。

Promise.all 和 Promise.race

Promise.all 可以接收一个元素为 Promise 对象的数组作为参数,当这个数组里面所有的 Promise 对象都变为 resolve 时,该方法才会返回。

var getJSON = function(url) {
  var promise = new Promise(function(resolve, reject){
    var client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

    function handler() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
  });

  return promise;
};

let p1 = getJSON("https://api.github.com/search/users?q=1")
let p2 = getJSON("https://api.github.com/search/users?q=2")
Promise.all([p1,p2])
.then(function([post1,post2]) {
  let a = String(post1.items[0].id).substr(0,1);
  let b = String(post2.items[0].id).substr(0,1);
  getJSON("https://api.github.com/search/users?q="+a+b)
    .then(function (response) {
      console.log(response)
    })
}).catch(function(error){
  console.log('出错啦!', error);
});

上面的例子传输的两个数据可能需要不同的时长,即使 p2 的速度比 p1 要快,但是 Promise.all 方法会按照数组里面的顺序将结果返回。

日常开发中经常会遇到这样的需求,在不同的接口请求数据然后拼合成自己所需的数据,通常这些接口之间没有关联(例如不需要前一个接口的数据作为后一个接口的参数),这个时候 Promise.all 方法就可以派上用场了。

还有一个和 Promise.all 相类似的方法 Promise.race,它同样接收一个数组,不同的是只要该数组中的 Promise 对象的状态发生变化(无论是 resolve 还是 reject)该方法都会返回。

Generator

什么是Generator?

当你调用一个generator时,它将返回一个迭代器对象。这个迭代器对象拥有一个叫做next的方法来帮助你重启generator函数并得到下一个值。

next方法不仅返回值,它返回的对象具有两个属性:done和value。value是你获得的值,done用来表明你的generator是否已经停止提供值。

function* test(p){
    console.log(p); 
    var a = yield p + 1;
    console.log(a); 
}

var g = test(1);
var ret;
ret = g.next();
console.log(ret);
ret = g.next(ret.value + 1);
console.log(ret);
//1
//Object {value: 2, done: false}
//3
//Object {value: undefined, done: true}

yield关键字可以让当前函数暂停执行并保存现场,并跳出到调用此函数的代码处继续执行。
可以利用函数执行时的返回句柄的next方法回到之前暂停处继续执行
next执行的返回值的value即是yield关键字后面部分的表达式结果
下一个next的唯一参数值可以作为yield的整体返回值,并赋值给a变量
看下执行顺序就能比较清楚Generator是怎么工作的了

var g = test(1);//1
var ret;//2
ret = g.next();//3
console.log(p);//4 输出1
yield p + 1;//5
console.log(ret);//6 输出{value: 2, done: false}
ret = g.next(ret.value + 1);//7
var a = yield p + 1;//8 ,此时yield p + 1 表达式的值为ret.value + 1,即为3
console.log(a);//9 输出3
console.log(ret);//10 输出 {value: undefined, done: true}

同步场景下生成器的使用

function * Square(){
  for(var i=1;;i++){
    yield i*i;
  }
}
var square = Square();
square.next(); // 1
square.next(); // 4
square.next(); // 9
......

我们在循环中并没有设中止条件,因为调用一个square.next()方法,它才会执行一次,不调用则不执行,所以不用担心死循环的问题。

异步场景下的生成器使用

如何用生成器解决异步场景下的回调金字塔
从前面的例子中,其实已经可以体会出来了,生成器的用法中并不包含对异步的处理,所以其实没有办法帮助我们对异步回调进行封闭。那么为什么大家将它视为解决回调嵌套的神器呢?在翻阅了不少资料后找到这篇文章,文章作者一开始也认为生成器并不能解决回调嵌套的问题,但下面自己做了解释,如果生成器的返回的是一系列的Promise对象的话,情况就会不一样了,举个粟子:

function myAjax(){
  return fetch('https://api.github.com/search/users?q=1');
}

我们使用window.fetch方法来处理ajax请求,这个方法会返回一个Promise对象。然后,我们使用一个生成器来包装这个操作:

function * MyLogic(){
  var serverData = yield myAjax();
  console.log('MyLogic after myAjax');
  console.log('serverStatus:%s',serverData.status);
}

使用的时候这样用:

var myLogic = MyLogic();
var promise = myLogic.next().value; 
promise.then(function(serverData){
  myLogic.next(serverData);
});

可以看到,我们这里的myAjax()以及MyLogic()函数中,并没有使用回调,就完成了异步操作。

这里有几个值得注意的点:

myAjax()函数返回的是一个Promise对象
myLogic中的第一个语句,返回的是 {value: Promise, done: false}等外界再次调用next()方法时将Promise对象数据传进来,赋值给serverDate
promise的状态是由第三段代码,在外部进行处理,完成的时候调用myLogic.next()方法并将serverData再传回MyLogic()中
你一定会问,下面这个promise.done不就是回调操作么?Bingo!这正是精华所在!我们来看一下这段代码做了什么:

首先,myLogic.next().value返回了一个Promise对象(promise),然后,promise.then中的回调函数所做的事情就是调用myLogic.next()方法并将serverData传入就行了,除了调用next()方法,其它的什么事情都没有。

异步封装

首先,我们保持myAjax()MyLogic定义不变,而将myLogic.next()放到一个函数来调用,这个函数专门负责调用myLogic.next(),得到返回的Promise对象,然后在Promise被resolve的时候再次调用myLogic.next()

var myLogic = MyLogic();
function genRunner(){
  // 调用next()获取promise
  var yieldValue = myLogic.next();
  var promise = yieldValue.value;
  if(promise){
    promise.then(function(data){
      // promise被resolve的时候再次调用genRunner
      // 以继续执行MyLogic中后面的逻辑
      genRunner();
    });
  }
}

这样我们就把不停地调用myLogic.next()和不停地promise.then()的过程进行了封装。运行genRunner()跑一下:

MyLogic after myAjax
Uncaught (in promise) TypeError: Cannot read property 'status' of undefined(…)

可见MyLogic在yield后的语句的确被执行了,但是serverData却没有值,这是因为我们在调用myLogic.next()的时候没有把值传回去。稍微修改下代码:

// diff1: genRunner接受参数val
function genRunner(val){
  // diff2: .next调用时把参数传过去,yield左边可以被赋值
  var yieldValue = myLogic.next(val);
  var promise = yieldValue.value;
  if(promise){
    promise.then(function(data){
      // diff3: 调用genRunner时传递参数
      genRunner(data);
    });
  }
}

这次一切都对了:

MyLogic after myAjax
serverStatus:200

至此我们已经把封装最核心的部分抽离出来了,我们的业务代码MyLogic()已经是“异步操作,同步写法”,再封装得更通用一些

var genRunner = function(GenFunc){
  return new Promise(function(resolve, reject){
    var gen = GenFunc();
    var innerRun = function(val){
      var val = gen.next(val);
      // 如果已经跑完了,则resolve
      if(val.done){
        resolve(val.value);
        return;
      }
      // 如果有返回值,则调用`.then`
      // 否则直接调用下一次innerRun()
      // 为简单起见,假设有值的时候永远是promise
      if(val.value){
        val.value.then(function(data){
          innerRun(data);
        });
      }else{
        innerRun(val.value);    
      }
    }
    innerRun();
  });
};

这里我们将刚刚看过的封装改成了innerRun(),并加上了自动调用。外面再封装了一层genRunner(),返回一个Promise。在GenFunc()全程调用完之后,Promise被resolve。

用起来是这样:

genRunner(function*(){
  var serverData = yield myAjax();
  console.log('MyLogic after myAjax');
  console.log('serverStatus:%s',serverData.status);
}).then(function(message){
  console.log(message);
});

Async/Await

Async/Await应该是目前最简单的异步方案了,首先来看个例子。

这里我们要实现一个暂停功能,输入N毫秒,则停顿N毫秒后才继续往下执行。

const sleep = time => {
    return new Promise((resolve, reject)=> {
        setTimeout( ()=> {
            resolve();
        }, time);
    })
};

(async ()=> {
    // 在这里使用起来就像同步代码那样直观
    console.log('start');
    await sleep(3000);
    console.log('end');
})();

控制台先输出start,稍等3秒后,输出了end。

基本规则

async 表示这是一个async函数,await只能用在这个函数里面。
await 表示在这里等待promise返回结果了,再继续执行。
await 后面跟着的应该是一个promise对象(当然,其他返回值也没关系,只是会立即执行,不过那样就没有意义了…)

获得返回值

await等待的虽然是promise对象,但不必写.then(..),直接可以得到返回值。

const sleep = time => {
    return new Promise( (resolve, reject) => {
        setTimeout( () => {
            // 返回 ‘ok’
            resolve('ok');
        }, time);
    })
};

(async function () {
    let result = await sleep(3000);
    console.log(result); // 收到 ‘ok’
})();

捕捉错误

既然.then(..)不用写了,那么.catch(..)也不用写,可以直接用标准的try catch语法捕捉错误。

const sleep = time => {
    return new Promise( (resolve, reject) => {
        setTimeout( () => {
            // 模拟出错了,返回 ‘error’
            reject('error');
        }, time);
    })
};

(async () => {
    try {
        console.log('start');
        await sleep(3000); // 这里得到了一个返回错误      
        // 所以以下代码不会被执行了
        console.log('end');
    } catch (err) {
        console.log(err); // 这里捕捉到错误 `error`
    }
})();

循环多个await

await看起来就像是同步代码,所以可以理所当然的写在for循环里,不必担心以往需要闭包才能解决的问题。

const sleep = time => {
    return new Promise( (resolve, reject) => {
        setTimeout( () => {
            resolve();
        }, time);
    })
};
(async function () {
    for (var i = 1; i <= 10; i++) {
        console.log(`当前是第${i}次等待..`);
        await sleep(1000);
    }
})();

值得注意的是,await必须在async函数的上下文中的。

..省略以上代码

// 错误示范
(async function(){
    let arr = [1,2,3,4]
    arr.forEach(function (v) {
        console.log(`当前是第${v}次等待..`);
        await sleep(1000); // 错误!! await只能在async函数中运行
    });
})()

// 正确示范
(async function(){
   let arr = [1,2,3,4]
   for(var v of arr) {
       console.log(`当前是第${v}次等待..`);
       await sleep(1000); // 正确, for循环的上下文还在async函数中
   }
})()

多个await并发

var getJSON = function(url) {
    var promise = new Promise(function(resolve, reject){
        var client = new XMLHttpRequest();
        client.open("GET", url);
        client.onreadystatechange = handler;
        client.responseType = "json";
        client.setRequestHeader("Accept", "application/json");
        client.send();

        function handler() {
          if (this.readyState !== 4) {
            return;
          }
          if (this.status === 200) {
            resolve(this.response);
          } else {
            reject(new Error(this.statusText));
          }
        };
    });

    return promise;
};
const getdata = async function() {
    let p1 = getJSON("https://api.github.com/search/users?q=1")
    let p2 = getJSON("https://api.github.com/search/users?q=2")
    // let foo = await p1;
    // let bar = await p2;
    //这样比较耗时,因为只有p1完成以后,才会执行p2,完全可以让它们同时触发。
    let [foo, bar] = await Promise.all([p1,p2]);
    let a = String(foo.items[0].id).substr(0,1);
    let b = String(bar.items[0].id).substr(0,1);
    return await getJSON("https://api.github.com/search/users?q="+a+b)
}
getdata().catch(err => {
    console.log(err);
})

Async/Await与Promise

使用Async/Await明显节约了不少代码。我们不需要写.then,不需要写匿名函数处理Promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值