(1)认识
Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。
Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数。
可以简单的将Promise理解为一个容器,里面保存着未来才会结束的事件的结果。
(2)Promise 对象的状态
Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。
【1】异步操作未完成(pending)
【2】异步操作成功(fulfilled)
【3】异步操作失败(rejected)
上面三种状态里面,fulfilled和rejected合在一起称为resolved(已定型)。
这三种的状态的变化途径只有两种。
【1】从“未完成”到“成功”
【2】从“未完成”到“失败”
一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。
因此,Promise 的最终结果只有两种。异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled。异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected。
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
(3)Promise 构造函数
JavaScript 提供原生的Promise构造函数,用来生成 Promise 实例。
let promise = new Promise(function (resolve, reject) {
console.log("处理Promise任务...")
// 判断当前任务的处理结果
if (true) {
// 执行成功
resolve("ok");
} else {
// 执行失败
reject(new Error());
}
});
console.log(promise);
输出结果:
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。
需要注意当前Promise执行任务的结果,并不是说里面的代码执行完毕就结束了,如果有一个then()的话,但是在promise并没有去声明处理结果即resolve或者reject,那么是无法继续执行then里面的流程的。
let p1 = new Promise(function (resolve, reject) {
console.log("f1()...")
})
function f2() {
console.log("f2()...")
}
p1.then(f2)
上面的代码执行结果如下:
并没有去执行then中的f2方法代码,这是因为Promise的代码还没有一个确切的结果。
let p1 = new Promise(function (resolve, reject) {
console.log("f1()...")
resolve("ok")
})
function f2(param) {
console.log("f2()..." + param)
}
p1.then(f2)
增加代码行resolve(“ok”)执行完毕,其中还将执行结果作为参数传递出去,并且在then方法中获取到了该参数。
(4)Promise.prototype.then()
Promise 实例的then方法,用来添加回调函数。
then方法可以接受两个回调函数,第一个是异步操作成功时(变为fulfilled状态)的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。
let p1 = new Promise(function (resolve, reject) {
console.log("f1()...")
// 这里分别启用进行测试
reject(new Error('失败'));
// resolve("完成")
})
function f2(param) {
console.log("f2()..." + param)
}
function f3(param) {
console.log("f3()..." + param)
}
p1.then(f2, f3)
上面的代码中分别对成功和失败进行了测试,当resolve时执行第一个参数函数,即f2,当reject时执行第二个参数,即f3。
then方法可以链式使用。
p1
.then(step1)
.then(step2)
.then(step3)
.then(
console.log,
console.error
);
【1】可以通过链式的方式执行多个流程
【2】可以不指定reject处理函数
【3】上面代码中如果上一步的处理结果是resolve,那么会依次执行所有的,直到console.log()
【4】如果上面任意一个step流程发生异常,那么Promise就会去寻找reject处理函数,上面代码中只有一个就是console.error()
【5】在then中的方法可以不指定resolve或者reject,后面也可以继续执行
【6】想要获取上一步的处理结果可以通过return的方法返回结果,并在下一步中通过参数获取
function f2(param) {
console.log("f2()..." + param)
return param
}
function f3(param) {
console.log("f3()..." + param)
return param
}
function f4(param) {
console.log("f4()..." + param)
return param
}
function f5(param) {
console.error("异常:", param)
}
p1.then(f2).then(f3).then(f4).then(console.log, f5)
【7】基于上面的代码如果某一步发生了异常,那么就会寻找下一个reject函数,即f5。此外,当某一步发生了异常,那么后面的step不会在执行了。
function f3(param) {
console.log("f3()..." + param)
throw new Error("ERROR")
}
(5)微任务
Promise 的回调函数属于异步任务,会在同步任务之后执行。
new Promise(function (resolve, reject) {
resolve(1);
}).then(console.log);
console.log(2);
// 输出:2 1
上面的输出结果是先输出2后输出Promise里面的代码,异步任务要晚于同步任务。但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function (resolve, reject) {
resolve(2);
}).then(console.log);
console.log(3);
// 3
// 2
// 1
上面代码的输出结果是321。这说明then的回调函数的执行时间,早于setTimeout(fn, 0)。因为then是本轮事件循环执行,setTimeout(fn, 0)在下一轮事件循环开始时执行。
(6)图片加载
var loadImage = function (path) {
return new Promise((resolve, reject) => {
let image = new Image();
image.onload = resolve
image.onerror = reject
image.src = path
})
}
// 加载图片
loadImage("https://www.runoob.com/try/demo_source/logo.png").then(function (e) {
e.target.width = 400
e.target.height = 400
document.getElementById("myImg").appendChild(e.target);
}).then(function () {
console.log("图片加载成功")
}, function () {
console.log("图片加载失败")
})
【1】image是一个图片对象的实例。它有两个事件监听属性,onload属性在图片加载成功后调用,onerror属性在加载失败调用。
【2】上面的image.onload = resolve和 image.onerror = reject就是把图片加载完成和异常和Promise进行了绑定
【3】图片加载成功以后,onload属性会返回一个事件对象,因此第一个then()方法的回调函数,会接收到这个事件对象。该对象的target属性就是图片加载后生成的 DOM 节点。
【4】扩展小知识:Image()函数将会创建一个新的HTMLImageElement实例。
var myImage = new Image(100, 200);
myImage.src = 'picture.jpg';
document.body.appendChild(myImage);
上面的代码相当于在 中定义了下面的 HTML:
<img width="100" height="200" src="picture.jpg">
实现AJAX
const myAjax = function (url) {
return new Promise((resolve, reject) => {
// 定义handler
const handler = function () {
if (this.readyState !== XMLHttpRequest.DONE) return;
if (this.status === 200) {
// 请求成功,将响应结果返回
resolve(this.response)
} else {
// 请求失败,返回异常信息
reject(new Error(this.statusText))
}
}
// 创建XMLHttpRequest对象
let client = new XMLHttpRequest();
client.onreadystatechange = handler
client.open("GET", url)
client.responseType = "json"
client.setRequestHeader("Accept", "application/json")
client.send()
})
}
myAjax("a.json").then(function (json) {
console.log('响应结果: ' + JSON.stringify(json));
}, function (error) {
console.error('发生异常', error);
});
上面代码中,是对 XMLHttpRequest 对象的封装,用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个Promise对象。需要注意的是,在其内部,resolve函数和reject函数调用时,都带有参数。
如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例
小结
【1】Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。
【2】Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。
【3】Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。