[ES6] promise & async/await
ECMAscript 6 原生提供了 Promise 对象。
Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。
为什么需要 promise 对象
有时为了实现异步结束的后续操作,需要执行回调,但是当有多个异步操作时,可能导致回调嵌套过深,或者回调地狱
以前是通过"函数瀑布"实现的异步,例如:
setTimeout(function () {
console.log("First");
setTimeout(function () {
console.log("Second");
setTimeout(function () {
console.log("Third");
}, 3000);
}, 4000);
}, 1000);
以上是一个比较简单的示例,但是在一个复杂的程序当中,用 “函数瀑布” 实现的程序,无论是维护还是异常处理都是特别繁琐的事情,而且会让缩进格式变得非常冗赘
new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("First");
resolve();
}, 1000);
})
.then(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("Second");
resolve();
}, 4000);
});
})
.then(function () {
setTimeout(function () {
console.log("Third");
}, 3000);
});
以上嵌套的代码,变成了顺序格式的代码
promise 是为了更加优雅的书写异步任务
Promise 不是一种将异步转换为同步的方法,只不过是一种更良好的编程风格。
promise 对象与函数的区别
promise 对象能够保存状态,而函数不可以(闭包函数除外)
promise 主要是为了处理异步编程的情况
什么时候适合用 Promise 而不是传统回调函数?当需要多次顺序执行异步操作的时候,例如,通过异步方法先后检测用户名和密码,需要先异步检测用户名,然后再异步检测密码的情况下就很适合 Promise。
Promise 对象两个特点
对象的状态不受外界影响
Promise 对象代表一个异步操作,有三种状态:
- pending: 初始状态,不是成功或失败状态。
- resolved: 已完成,意味着操作成功完成,也称之为 fulfilled
- rejected: 已失败,意味着操作失败。
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
一旦状态改变,就不会再变
一旦状态改变,就不会再变,对于当前 promise 对象任何时候都得到都是这个结果
Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对当前 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
Promise 优缺点
Promise 优点
有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。
Promise 缺点
-
无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
-
如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
-
当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Promise 使用
Promise 构造函数
要想创建一个 promise 对象、可以使用 new 来调用 Promise 的构造器来进行实例化,下面是创建 promise 的步骤:
var promise = new Promise(function (resolve, reject) {
// 异步处理
// 处理结束后、调用resolve 或 reject
});
Promise 构造函数是 JavaScript 中用于创建 promise 对象的内置构造函数。
Promise 构造函数参数
Promise 构造函数接受一个函数作为参数,该函数是同步的
并且会被立即执行,所以我们称之为起始函数
。
起始函数包含两个参数 resolve 和 reject,分别表示 Promise 成功和失败的状态:
- 起始函数执行成功时,调用 resolve 函数并传递成功的结果
- 起始函数执行失败时,调用 reject 函数并传递失败的原因
resolve 和 reject 的作用域只有起始函数,不包括 then 以及其他序列;
resolve 和 reject 并不能够使起始函数停止运行,所以需要 return
promise 对象
Promise 构造函数返回值是一个 Promise 对象, Promise 对象具有以下几个方法:
- then:用于处理 Promise 成功状态的回调函数
- catch:用于处理 Promise 失败状态的回调函数
- finally:无论 Promise 是成功还是失败,都会执行的回调函数
- 以上三个方法的参数均是函数
var promise = new Promise(function (resolve, reject) {
//使用setTimeout(...)来模拟异步代码
//实际编码时可能是XHR请求或是HTML5的一些API方法.
setTimeout(function () {
resolve("成功!"); //代码正常执行!
}, 250);
});
promise.then(function (successMessage) {
//successMessage是resolve(...)方法传入的值
document.write("Yay! " + successMessage); //Yay!成功!
});
promise 对象方法
promise 对象可以调用 promise.then() 方法,promise.then() 是 promise 最为常用的方法:
promise.then(onResolved, onRejected)
- onResolved 是 Promise 构造函数执行成功时的回调
- onRejected 是 Promise 构造函数执行失败时的回调,
- 以上两个函数只会有一个被调用
promise 对象简化了对 error 的处理,写法为:
promise.then(onResolved).catch(onRejected)
Promise 对象的错误具有"冒泡"性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个 catch 语句捕获
Promise.prototype.then 方法
- then 按顺序执行
Promise.prototype.then 方法可以多次调用,并按顺序执行
getJSON("/posts.json")
.then(function (json) {
return json.post;
})
.then(function (post) {
// proceed
});
上面的代码,getJSON 的 resolve 结果会作为参数传入第一个 then 函数,第一个 then 回调函数完成以后,会将返回结果作为参数,传入第二个 then 回调函数。
getJSON("/post/1.json")
.then(function (post) {
return getJSON(post.commentURL);
})
.then(function (comments) {
// 对comments进行处理
});
如果前一个 then 回调函数返回的是 Promise 对象,这时后一个回调函数就会等待该 Promise 对象有了运行结果,才会进一步调用。
- 链式调用
Promise 链式编程最好保持扁平化,不要嵌套 Promise
所以可以直接在 then 中返回一个 Promise 对象,那么下一个 then 将相当于对这个返回的 Promise 进行操作,用于链式调用,就不需要在 then 中嵌套 Promise 的回调
const p = new Promise(function (resolve, reject) {
resolve(1);
})
.then(function (value) {
// 第一个then // 1
console.log(value);
return value * 2;
})
.then(function (value) {
// 第二个then // 2
console.log(value);
})
.then(function (value) {
// 第三个then // undefined
console.log(value);
//链式调用,返回promise对象
return Promise.resolve("resolve");
})
.then(function (value) {
// 第四个then // resolve
console.log(value);
return Promise.reject("reject");
})
.then(
function (value) {
// 第五个then //接收到第四个then reject:reject,所以不打印
console.log("resolve:" + value);
},
function (err) {
//接收到 第四个then 的reject所以打印
console.log("reject:" + err);
}
);
then() 传入的函数会按顺序依次执行,有任何异常都会直接跳到 catch 序列
const p1 = new Promise(function (resolve, reject) {
resolve(1);
})
.then(function (result) {
p2(result).then((newResult) => p3(newResult));
})
.then(() => p4());
链式编程,创建新 Promise 但忘记返回它时,对应链条被打破
,导致 p4 会与 p2 和 p3 同时进行。
大多数浏览器中不能终止 Promise 链里的 rejection,建议后面都跟上 .catch(error => console.log(error))
注意,总是返回或终止 Promise 链
Promise.prototype.catch 方法:异常处理函数
Promise.prototype.catch 方法是 Promise.prototype.then(null, rejection)
的别名,用于指定发生错误时的回调函数。
getJSON("/posts.json")
.then(function (posts) {
// some code
})
.catch(function (error) {
// 处理前一个回调函数运行时发生的错误
console.log("发生错误!", error);
});
Promise 对象的错误具有"冒泡"性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个 catch 语句捕获。
catch 可以捕获 Promise 构造函数 reject 的异常,也可以捕获 then 中的错误
getJSON("/post/1.json")
.then(function (post) {
return getJSON(post.commentURL);
})
.then(function (comments) {
// some code
})
.catch(function (error) {
// 处理前两个回调函数的错误
});
then 块如何中断?then 块默认会向下顺序执行,return 是不能中断的,可以通过 throw 来跳转至 catch,实现中断。
什么时候需要再写一个 then 而不是在当前的 then 接着编程? 当又需要调用一个异步任务的时候。
Promise.prototype.finally 方法:最终的回调函数
finally() 是在 Promise 执行的最后一定会执行的序列
new Promise(function (resolve, reject) {
resolve(1);
})
.then(function (value) {
console.log(value);
return value * 2;
})
.catch(function (error) {
console.log(error);
})
.finally(function () {
console.log(value);
});
除了 then 块以外,catch/finally 能否多次使用?可以,finally 与 then 一样会按顺序执行,但是 catch 块只会执行第一个,除非 catch 块里有异常。所以最好只安排一个 catch 和 finally 块。
then、catch 和 finally 序列顺序可以颠倒但不建议这样做,最好按 then-catch-finally 的顺序编写程序
Promise.race 方法
Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
var p = Promise.all([p1, p2, p3]);
p1、p2、p3 都是 Promise 对象的实例(Promise.all 方法的参数不一定是数组,但是必须具有 iterator 接口,且返回的每个成员都是 Promise 实例)
p 的状态由 p1、p2、p3 决定,分成两种情况:
- 只有 p1、p2、p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
- 只要 p1、p2、p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。
下面是一个具体的例子:
// 生成一个Promise对象的数组
var promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON("/post/" + id + ".json");
});
Promise.all(promises)
.then(function (posts) {
// ...
})
.catch(function (reason) {
// ...
});
Promise.race 方法
Promise.race 方法与 Promise.all 有些类似,同样是将多个 Promise 实例,包装成一个新的 Promise 实例
var p = Promise.race([p1, p2, p3]);
上面代码中,只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的返回值。
如果 Promise.all 方法和 Promise.race 方法的参数,不是 Promise 实例,就会先调用下面讲到的 Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。
Promise.resolve/reject 方法
有时需要将现有对象转为 Promise 对象,Promise.resolve 方法就起到这个作用:
var jsPromise = Promise.resolve($.ajax("/whatever.json"));
上面代码将 jQuery 生成 deferred 对象,转为一个新的 ES6 的 Promise 对象。
如果 Promise.resolve 方法的参数,不是具有 then 方法的对象(又称 thenable 对象),则返回一个新的 Promise 对象,且它的状态为 fulfilled。
var p = Promise.resolve("Hello");
p.then(function (s) {
console.log(s); // s="Hello",Hello
});
上面代码生成一个新的 Promise 对象的实例 p,它的状态为 fulfilled,所以回调函数会立即执行,Promise.resolve 方法的参数就是回调函数的参数。
如果 Promise.resolve 方法的参数是一个 Promise 对象的实例,则会被原封不动地返回。
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为 rejected。Promise.reject 方法的参数 reason,会被传递给实例的回调函数。
var p = Promise.reject("出错了");
p.then(null, function (s) {
console.log(s); // 出错了,s="出错了"
});
上面代码生成一个 Promise 对象的实例,状态为 rejected,回调函数会立即执行。Promise.reject 方法的参数就是回调函数的参数。
asycn/await 指令
const https = require("https");
function fetch(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
let data = "";
res.on("data", (rd) => (data = data + rd));
res.on("end", () => resolve(data));
res.on("error", reject);
});
});
}
/* ------- 方式1 --------*/
fetch("https://www.javascript.com/").then((data) => {
console.log(data.length);
});
/* ------- 方式2 --------*/
(async function read() {
try {
const data = await fetch("https://www.javascript.com/");
console.log(data);
console.log(data.length);
} catch (err) {
console.log(err); // 会输出 Some error
}
})();
新的指令 asycn/await 的出现将减少链式调用的出现,也增加了代码的可读性,对于应用中出现彼此依赖的 promise 更加友好
该指令使异步操作变得像同步操作一样容易了,async 异步函数前可以使用 await 指令,await 指令后必须跟着一个 Promise 对象,也即 async 异步函数中必须包含 Promise 对象, 异步函数会在这个 Promise 对象运行中暂停,直到其运行结束再继续运行
await 操作符用于等待一个 Promise 对象, 它只能在异步函数 async function 内部使用,否则报错
处理异常的机制将用 try-catch 块实现
async function asyncFunc() {
let value = await new Promise(function (resolve, reject) {
resolve("Return value");
});
console.log(value); //Return value
}
asyncFunc();
所以 Promise 对象的返回值,await 语句也会返回它
定义
async function name([param[, param[, ... param]]]) { statements }
- name: 函数名称。
- param: 要传递给函数的参数的名称。
- statements: 函数体语句。
- async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数
async function helloAsync() {
return "helloAsync";
}
console.log(helloAsync()); // Promise {<resolved>: "helloAsync"}
helloAsync().then((v) => {
console.log(v); // helloAsync
});
await 针对所跟不同表达式的处理方式:
- Promise 对象:await 会暂停执行,等待 Promise 对象 resolve,然后恢复 async 函数的执行并返回解析值。
- 非 Promise 对象:直接返回对应的值。
Promise 实现 Ajax 实例
Promise 对象实现的 Ajax 操作的实例:
function ajax(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open("GET", URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
};
req.send();
});
}
var URL = "/try/ajax/testpromise.php";
ajax(URL)
.then(function onResolved(value) {
document.write("内容是:" + value);
})
.catch(function onRejected(error) {
document.write("错误:" + error);
});
上面代码中,onFulfilled修改成了onResolved,只是方法名的变更,并不影响函数的执行
,resolve 方法和 reject 方法调用时,都带有参数。它们的参数会被传递给回调函数。reject 方法的参数通常是 Error 对象的实例,而 resolve 方法的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。
var p1 = new Promise(function (resolve, reject) {
// ... some code
});
var p2 = new Promise(function (resolve, reject) {
// ... some code
resolve(p1);
});
上面代码中,p1 和 p2 都是 Promise 的实例,但是 p2 的 resolve 方法将 p1 作为参数,这时 p1 的状态就会传递给 p2。如果调用的时候,p1 的状态是 pending,那么 p2 的回调函数就会等待 p1 的状态改变;如果 p1 的状态已经是 resolved(fulfilled) 或者 rejected,那么 p2 的回调函数将会立刻执行。