ECMA2015(ES6)简单入门-7-关于Promise和 Async/await

1 篇文章 0 订阅
1 篇文章 0 订阅

promise的含义
Promise 是异步编程的一种解决方案,
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。

Promise对象有以下两个特点。
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)
行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。

基本用法:
ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

下面代码创造了一个Promise实例。

const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

下面是一个Promise对象的简单例子。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}
timeout(100).then((value) => {
  console.log(value);
});

上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。因为 源码中,创建实例的时候传递个那个参数参数是自执行函数。

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出
实现Promise
ES6中Promise提供了以下API。
(1) 构造函数

function Promise(resolver) {}

原型方法

 Promise.prototype.then = function(){}
 Promise.prototype.catch = function(){}

静态方法

Promise.resolve = function(){}
Promise.reject = function(){}
Promise.all = function(){}
Promise.race = function(){}

Es6中通过构造函数创建对象的简单用法如下

const promise = new Promise((resolve) => {
    setTimeout(()=> {
        resolve(1);
    }, 2000);
});
promise.then(a=> alert(a));
promise.then(a => alert(a+1));

构造函数接受了一个executor立即执行函数
executor立即执行函数接受了一个resolve函数,
promise对象的then方法绑定状态为fufilled时的回调
resolve函数被调用时会触发then方法中的回调

构造函数的简单实现

class Xyz{
        constructor(executor) {
            this.status = 'pending';
            this.data = undefined;
            this.onResolvedCallBack = [];
            executor(this.resolve.bind(this))
        }
        resolve(value) {
            if(this.status === 'pending'){
                this.status = 'resolved'
                this.data = value
            }
            for(let i =0; i<this.onResolvedCallBack.length; i++){
                this.onResolvedCallBack[i](value)
            }
        }
        then(onResolve){
            this.onResolvedCallBack.push(onResolve);
        }
    }
     function timeout(ms) {
         return new Xyz((resolve) => {
             setTimeout(resolve, ms, 'done');
         });
     }
     timeout(100).then((value) => {
         console.log(value);
     })

下面是一个用Promise对象实现的 Ajax 操作的例子。

const getJSON = function(url) {
  const promise = new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

  });

  return promise;
};
getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('出错了', error);
});

上面代码中,getJSON是对 XMLHttpRequest 对象的封装,用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个Promise对象。需要注意的是,在getJSON内部,resolve函数和reject函数调用时,都带有参数。

如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误;
Promise.prototype.then()
Promise实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的,她的作用是为Promise实例添加状态改变时的回调函数,

then方法返回的是一个新的promise实例,所以可以采用链式写法

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function funcA(comments) {
  console.log("resolved: ", comments);
}, function funcB(err){
  console.log("rejected: ", err);
});

上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,就调用funcA,如果状态变为rejected,就调用funcB。

采用箭头函数,上面的代码可以写得更简洁。

getJSON("/post/1.json").then(
post => getJSON(post.commentURL)
).then(
comments => console.log("resolved: ", comments),
err => console.log("rejected: ", err)
);
**Promise.prototype.catch()**方法用于指定发生错误的回调函数是.then(null,rejection)或者。then(undefined,rejection)的别名

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

上面代码中,getJSON方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。

下面是一个例子

// 写法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

上面两种写法,可以发现reject方法的作用,等同于抛出错误。

如果 Promise 状态已经变成resolved,再抛出错误是无效的。
因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。

getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // 处理前面三个Promise产生的错误
});

一共有三个 Promise 对象:一个由getJSON产生,两个由then产生。它们之中任何一个抛出的错误,都会被最后一个catch捕获。

一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch方法,而不使用then方法的第二个参数
Promise.prototype.finally()
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

finally本质上是then方法的特例。

Promise.all() § ⇧
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)

p的状态由p1、p2、p3决定,分成两种情况。

(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

下面是一个具体的例子。

// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON(’/post/’ + id + “.json”);
});

Promise.all(promises).then(function (posts) {
// …
}).catch(function(reason){
// …
});
上面代码中,promises是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。

下面是另一个例子。

const databasePromise = connectDatabase();

const booksPromise = databasePromise
.then(findAllBooks);

const userPromise = databasePromise
.then(getCurrentUser);

Promise.all([
booksPromise,
userPromise
])
.then(([books, user]) => pickTopRecommendations(books, user));
上面代码中,booksPromise和userPromise是两个异步操作,只有等到它们的结果都返回了,才会触发pickTopRecommendations这个回调函数。

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。

const p1 = new Promise((resolve, reject) => {
resolve(‘hello’);
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
throw new Error(‘报错了’);
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// [“hello”, Error: 报错了]
上面代码中,p1会resolved,p2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。

如果p2没有自己的catch方法,就会调用Promise.all()的catch方法。

const p1 = new Promise((resolve, reject) => {
resolve(‘hello’);
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
throw new Error(‘报错了’);
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了
Promise.race()
Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve。

const p = Promise.race([
fetch(’/resource-that-may-take-a-while’),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error(‘request timeout’)), 5000)
})
]);

p
.then(console.log)
.catch(console.error);
上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。

Async/await

原文文章地址:https://javascript.info/async-await
Async/await
有一种特殊的语法可以更舒适地与promise协同工作,它叫做async/await,它是非常的容易理解和使用。

Async functions
让我们先从async关键字说起,它被放置在一个函数前面。就像下面这样:

async function f() {
    return 1
}

函数前面的async一词意味着一个简单的事情:这个函数总是返回一个promise,如果代码中有return <非promise>语句,JavaScript会自动把返回的这个value值包装成promise的resolved值。

例如,上面的代码返回resolved值为1的promise,我们可以测试一下:

async function f() {
    return 1
}
f().then(alert) // 1

我们也可以显式的返回一个promise,这个将会是同样的结果:

async function f() {
    return Promise.resolve(1)
}
f().then(alert) // 1

所以,async确保了函数返回一个promise,即使其中包含非promise。
还有另一个关键词await,只能在async函数里使用。

Async/Await语法如下:

// 只能在async函数内部使用
let value = await promise
关键词await可以让JavaScript进行等待,直到一个promise执行并返回它的结果,JavaScript才会继续往下执行。

以下是一个promise在1s之后resolve的例子:

async function f() {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve('done!'), 1000)
    })
    let result = await promise // 直到promise返回一个resolve值(*)
    alert(result) // 'done!' 
}
f()

函数执行到(*)行会‘暂停’,当promise处理完成后重新恢复运行, resolve的值成了最终的result,所以上面的代码会在1s后输出’done!’

我们强调一下:await字面上使得JavaScript等待,直到promise处理完成,
然后将结果继续下去。这并不会花费任何的cpu资源,因为引擎能够同时做其他工作:执行其他脚本,处理事件等等。

这只是一个更优雅的得到promise值的语句,它比promise更加容易阅读和书写。

不能在常规函数里使用await
如果我们试图在非async函数里使用await,就会出现一个语法错误:

function f() {
   let promise = Promise.resolve(1)
   let result = await promise // **syntax error**
}

如果忘记了在函数之前放置async,我们就会得到这样一个错误(syntax error)。上述,await只能在async函数中工作。

让我们来看promise链式操作一章中提到的showAvatar()例子,并用async/await重写它。

1.我们需要将.then()替换为await
2.此外,我们应该让函数变成async,这样await才能够工作

async function showAvatar() {
    // read our JSON
    let response = await fetch('/article/promise-chaining/user.json')
    let user = await response.json()
    
    // read github user
    let githubResponse = await fetch(`https://api.github.com/users/${user.name}`)
    let githubUser = await githubResponse.json()
    
    // 展示头像
    let img = document.createElement('img')
    img.src = githubUser.avatar_url
    img.className = 'promise-avatar-example'
    documenmt.body.append(img)
    // 等待3s
    await new Promise((resolve, reject) => {
        setTimeout(resolve, 3000)
    })
    img.remove()
    return githubUser
}
showAvatar()

相当的简洁和易读,比以前的要好得多。

await不能工作在顶级作用域
那些刚开始使用await的人们老是忘记这一点,那就是我们不能将await放在代码的顶层,那样是行不通的:

// 顶层代码处syntax error

let response = await fetch('/article/promise-chaining/user.json')
let user = await response.json()

所以我们需要将await代码包裹在一个async函数中,就像上面的例子一样。

await 接受“可能的”

就像promise.then,await也允许使用可用对象(那些具有可调用的then方法的对象)。同样,第三方对象可能不是一个promise,但是promise的兼容性表示,如果它支持.then方法,那么它就能用于await。

例如,这里await接受了new Thenable(1)

class Thenable {
   constructor(num) {
       this.num = num
   }
   then(resolve, reject) {
       alert(resolve) // function() {native code}
       // 1000ms后将this.num*2作为resolve值
       setTimeout(()=> {resolve(this.num * 2), 1000})
   }
}
async function(f) {
   // 等待1s,result变为2
   let result = await new Thenable(1)
   alert(result)
}
f()

如果await得到了一个带有then方法的非promise对象,它将会调用提供原生函数resolve、reject作为参数的方法,然后await一直等待,直到他们其中的一个被调用(在上面的例子它发生在(*)行)。

async方法
一个class方法同样能够使用async,只需要将async放在它之前就可以
就像这样:

class Waiter {
   async wait () {
       return await Promise.resolve(1)
   }
}

new Waiter().wait().then(alert) // 1

这里的意思是一样的:它确保了返回值是一个promise,支持await

错误处理
如果一个promise正常resolve,那么await返回这个结果,但是在reject的情况下会抛出一个错误,就好像在那一行有一个throw语句一样。

async function f() {
await Promise.reject(new Error(‘whoops!’))
}
和下面一样

async function f() {
throw new Error(‘Whoops!’)
}
在真实的使用场景中,promise在reject抛出错误之前可能需要一段时间,所以await将会等待,然后才抛出一个错误。
我们可以使用try-catch语句捕获错误,就像在正常抛出中处理异常一样:

async function f() {
    try {
        let response = await fetch('http://no-such-url')
    } catch (err) {
        alet(err) // TypeError: failed to fetch
    }
}
f()

如果发生了一个错误,控制会跳转到catch块。当然我们也能够捕获多行语句:

async function f() {
    try {
        let response = await fetch('/no-user-here')
        let user = await response.json()
    } catch(err) {
        // 在fetch和response.json中都能捕获错误
        alert(err)
    }
}
f()

如果我们不使用try-catch,然后async函数f()的调用产生的promise变成reject状态的话,我们可以添加.catch去处理它:

async function f() {
    let response = await fetch('http://no-such-url')
}
// f()变成了一个rejected的promise
f().catch(alert) // TypeError: failed to fetch

如果我们忘记添加.catch,我们就会得到一个未被处理的promise错误(能够在控制台里看到它),这时我们可以通过使用一个全局的事件处理器去捕获错误,就像在Promise链式操作一章讲的那样。

async/await和promise.then/catch
当我们使用async/await,我们很少需要.then,因为await总是等待着我们,而且我们能够使用常规的try-catch而不是.catch,这通常(并不总是)更方便。
但是在代码的顶层,当我们在async函数的外部时,我们在语法上是不能使用await的,所以通常添加.then/catch去处理最终结果或者错误。

async/await能够与Promise.all友好的协作
当我们需要等待多个promise时,我们可以将他们包装在Promise.all中然后使用await:

// 直到数组全部返回结果

let results = await Promise.all([
   fetch(url1),
   fetch(url2),
   ...
])

如果发生了一个错误,它就像普通情况一样:从一个失败状态的promise到Promise.all,然后变成了一个我们能够使用try-cathc去捕获的异常。

总结
放在一个函数前的async有两个作用:
1.使函数总是返回一个promise
2.允许在这其中使用await

promise前面的await关键字能够使JavaScript等待,直到promise处理结束。然后:
1.如果它是一个错误,异常就产生了,就像在那个地方调用了throw error一样。
2.否则,它会返回一个结果,我们可以将它分配给一个值

他们一起提供了一个很好的框架来编写易于读写的异步代码。

有了async/await,我们很少需要写promise.then/catch,但是我们仍然不应该忘记它们是基于promise的,因为有些时候(例如在最外面的范围内)我们不得不使用这些方法。Promise.all也是一个非常棒的东西,它能够同时等待很多任务。

async-await和Promise的关系async-await是promise和generator的语法糖。
是为了让我们书写代码时更加流畅,增强了代码的可读性。简单来说:async-await 是建立在 promise机制之上的,并不能取代其地位。

举例说明啊,你有三个请求需要发生,第三个请求是依赖于第二个请求的解构第二个请求依赖于第一个请求的结果。若用 ES5实现会有3层的回调,若用Promise 实现至少需要3个then。一个是代码横向发展,另一个是纵向发展。今天指给出 async-await 的实现哈~

//我们仍然使用 setTimeout 来模拟异步请求

function sleep(second, param) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(param);
        }, second);
    })
}

async function test() {
    let result1 = await sleep(2000, 'req01');
    let result2 = await sleep(1000, 'req02' + result1);
    let result3 = await sleep(500, 'req03' + result2);
    console.log(`
        ${result3}
        ${result2}
        ${result1}
    `);
}

test();
//req03req02req01
//req02req01
//req01

对于初学者来说一不小心就将 ajax 的并发请求发成了阻塞式同步的操作了,我就真真切切的在工作中写了这样的代码。await 若等待的是 promise 就会停止下来。业务是这样的,我有三个异步请求需要发送,相互没有关联,只是需要当请求都结束后将界面的 loading 清除掉即可。 刚学完 async await 开心啊,到处乱用~

function sleep(second) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('request done! ' + Math.random());
        }, second);
    })
}

async function bugDemo() {
    await sleep(1000);
    await sleep(1000);
    await sleep(1000);
    console.log('clear the loading~');
}

bugDemo();

loading 确实是等待请求都结束完才清除的。但是你认真的观察下浏览器的 timeline 请求是一个结束后再发另一个的(若观察效果请发真实的 ajax 请求)

那么,正常的处理是怎样的呢?

async function correctDemo() {
    let p1 = sleep(1000);
    let p2 = sleep(1000);
    let p3 = sleep(1000);
    await Promise.all([p1, p2, p3]);
    console.log('clear the loading~');
}
correctDemo();// clear the loading~

恩, 完美。看吧~ async-await并不能取代promise.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值