前言
本文会讲解 ES6
中的 promise
,但是并不通过类似于笔记文档式的方法进行书写。我会从 promise
诞生之前讲起,在什么坏境下出现?为什么出现?早期的社区中是怎么实现的,直到成为语言的规范的整个过程。之后介绍核心语法,以及在实战中的使用。
为了大家的理解,我会尽量用通俗易懂的方法谈及,最后不论之前你会不会 promise
,都希望可以通过此文对 promise
有一个更加全新的认识。
Promise 的历史
你大可不必认为这是单纯的来讲历史而觉得不重要,相反这对你理解事物本身有锦上添花的妙处。
Promise
其实是一个古老的概念,在很多的常见语言的标准库中都有 Promise
关联的特性。即使没有,像在 javascript
中,早期也是由第三方即社区已经实现好了。
在 JavaScript
中,最早得到广泛使用的 Promise
是 jQuery
中的 jQuery.Deferred()
。你或许没有使用过 jq ,又或者不知道这个方法,但是你绝对听说过 jQuery 中的 ajax
吧。这其实就是早期的 Promise
。
最终其终于称为了语言的规范存在于 ES6
中。
为什么需要 Promise
假设我们现在需要完成大量的异步任务,但是这些异步任务的执行是有条件的,就是必须等待上一个异步任务成功后执行。
这通常在异步的网络请求或者 Node.js
中的异步操作用很常见。
比如一段异步读取文件的代码:
const fs = require('fs');
fs.readFile('1.txt', (_, data) => {
console.log(data.toString());
fs.readFile('2.txt', (_, data) => {
console.log(data.toString());
fs.readFile('3.txt', (_, data) => {
console.log(data.toString());
})
})
});
可以看到这个嵌套的层级是很严重的,当我们有更多需要依次执行的任务时,就会出现回调地狱。
为了解决回调地狱的问题,Promise
就诞生了。
社区中的实现
这里我会给您使用图文的方式阐述早期在 jQuery
中是如何实现 Promise
的,但是并不会带你手写其内部的实现,旨在让你对 Promise
有一个大概的初步认识,并且掌握其原理,了解它的运转流程。
容器
你可以想象现在有一个容器,你可以把他当成一个数组。容器中有很多的回调函数等待执行:
需要注意的是容器中的这些回调函数还没有执行。我们可以对这个容器进行以下操作:
- 添加回调函数(
add
) - 删除回调函数(
remove
) - 依次执行所有的回调函数(
fire
) - 并且当我们
fire
后还可以继续往容器中add
回调函数,此时新加的函数会被立即执行
三个容器
我们已经对上述的容器有了一个认识,其实 Promise
就是管理了这样的三个容器,分别对应三种状态:pending
(进行中)、fulfilled
(已成功)、rejected
(已失败):
最终的状态只由异步任务的结果决定,任何其他的操作都没有办法改变这个状态。
Promise
对象的状态改变只有两种可能:
- 从
pending
变为fulfilled
。 - 从
pending
变为rejected
。
社区版 Promise
的原理就是维护着三个容器,当异步任务的结果确定后会自动切换状态,然后找到当前状态所管理着的容器,依次执行容器中的回调函数。
接着,Promise
的实例对象拥有 then
和 catch
方法,他们的原理是:
then
方法可以接受两个回调函数,第一个回调函数会被添加到fulfilled
这个状态所管理的容器中;第二个回调函数是可选的,会被添加到rejected
这个状态所管理的容器中。catch
方法可以接受一个回调函数,会被添加到rejected
这个状态所管理的容器中。
当异步任务的状态确定后,例如从 pending
到 fulfilled
,首先会执行 fulfilled
所管理的容器中的回调函数,接着如果有 then
方法,第一个参数会被继续添加到当前的容器。由上面提交的容器的最后一条性质可以知道,新添加的方法会被立即执行,所以当前 then
函数中的第一个回调函数会被立即执行。
正式学习 Promise
在了解了 Promise
的一些基础概念以及早期社区版本的一个实现原理后,我们再来学习 ES6
中的 Promise
,这时就会觉得轻松易懂好上手。
Promise 的构造函数
想要构造一个 Promise
,基本的用法如下(伪代码):
const promise = new Promise((resolve, reject) => {
if (...) { // succeed
resolve(result);
} else { // failed
reject(Error(errMessage));
}
});
使用 Promise 封装 ajax
了解了基础的 Promise
使用后,我们可以讲平时经常使用的 XMLHttpRequest
进行封装:
const ajax = (function() {
'use strict';
const DONE = 4, OK = 200;
// 该方法处理 get 请求的参数
function serialize(obj) {
const str = [];
for (const p in obj) {
if (obj.hasOwnProperty(p)) {
str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]));
}
}
return str.join('&');
}
// 核心方法
function core(method, url, data, headers) {
if (method === 'GET' && data) {
url += '?' + serialize(data);
}
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
if (headers) {
for (const key in headers) {
if (headers.hasOwnProperty(key)) {
xhr.setRequestHeader(key, headers[key]);
}
}
}
xhr.onreadystatechange = e => {
if (xhr.readyState === DONE) {
if (xhr.status === OK) {
resolve(xhr.responseText);
} else {
reject(Error(xhr.statusText));
}
}
};
xhr.onerror = e => {
reject(Error('XMLHttpRequest failed'));
};
if (method === 'GET' || !data) {
xhr.send();
} else {
xhr.send(data);
}
});
}
// 向外暴露四个可用的方法
return {
'get': (url, data, headers) => {
return core('GET', url, data, headers);
},
'post': (url, data, headers) => {
return core('POST', url, data, headers);
},
'put': (url, data, headers) => {
return core('PUT', url, data, headers);
},
'delete': (url, headers) => {
return core('DELETE', url, headers);
}
};
})();
相信对上面封装 ajax
的操作后你应该对 Promise
有了更深刻的体会,就只需要在异步成功时调用 resolve
,失败时调用 reject
并且传递响应的参数就可以了。
实例方法
介绍完 Promise
的构造函数后,接着来介绍它的实例方法。我们使用上面已经封装好的 ajax
来进行测验。
then
第一个实例方法就是已经提到过的 then
方法,它接受两个参数,第一个参数时成功的回调函数,第二个参数是失败的回调函数。
我们使用上面封装的 ajax
来请求 dog.ceo/api/breeds/… 网址,我们可以得到一张随机的狗狗的照片地址:
const img = document.querySelector('img');
ajax.get('https://dog.ceo/api/breeds/image/random')
.then(res => {
res = JSON.parse(res);
img.src = res.message;
});
通过网络请求,在 then
方法中传递一个回调函数,他会默认在成功后执行,并且成功后返回封装的方法中的 resolve
方法的参数。所以 res
是一个 JSON
字符串,这里我们对其解析后赋值给 img
的 src
属性。
这时可以在页面中看到随机的狗狗的照片:
当然如果异步任务失败了,你可以向 then
方法中传递第二个参数,它会在失败后自动执行,并且参数是一个错误信息。
catch
catch
方法使用时传递一个回调函数:promise.catch(onRejected)
,它等同于 promise.then(undefined, onRejected)
。也就是在失败后会调用其回调函数。
finally
最后一个实例方法是 finally
,接受一个回调函数作为参数,该方法在不管 Promise
最后的状态如何都会调用,不过他是 ES2018
引入的标准。
ajax.get('https://dog.ceo/api/breeds/image/random')
.then(res => {
res = JSON.parse(res);
img.src = res.message;
}).catch(err => {
console.log(err);
}).finally(() => {
console.log('请求结束');
});
这样,不论失败还是成功,都会打印请求结束
:
静态方法
介绍完了实例方法后,有几个很重要的静态方法需要我们记住并熟练掌握:
all
race
resolve
reject
Promise.all
Promise.all
方法接受一个数组作为参数,只有参数中所有的 promise
实例都 resolve
后才会执行成功的回调函数,只要有一个 reject
就会执行失败的回调函数。
Promise.all([promise1, promise2, promise3]).then(res => {
console.log(res); // [res1, res2, res3]
});
Promise.race
Promise.race
方法同样接受一个数组作为参数,但是只要有一个 promise
实例的状态变为成功就会执行成功的回调函数,并且参数为最快的那个任务的结果。只有全部任务失败后才会执行失败的回调函数。
Promise.race([promise1, promise2, promise3]).then(res => {
console.log(res); // res1 if promise1 is resolved first, etc.
});
Promise.resolve
Promise.resolve
快速创建一个 Promise
,如果参数原来就是 Promise,返回值跟原来的 Promise 一致。如果原来不是 Promise,参数会被包装给 then 的第一个参数函数。
该方法可以用来封装不确定是不是 Promise 的值。
Promise.reject
Promise.reject
快速创建一个被 reject
的 Promise
,如果参数是 Promise,返回值跟原来的 Promise 一致。如果原来不是 Promise,参数值会被包装给 then 的第二个参数函数或者 catch。
该方法可以用来封装不确定是不是 Promise 的值。
使用 Promise
学习完所有的 Promise 相关的 API 后,我们可以在之后的项目或者练习中面对异步操作时尽可能地使用 Promise 来书写,这样写出来得代码既可以回避回调地狱的问题,也可以让代码更具有可读性。例如本文中我们已经使用 Promise 来封装了一个 ajax
。
当然你也可以配合 async
函数来使用更深刻得体会异步操作得同步写法。
总结
我们在学习一项新颖得技术得时候一定要思其所以然,从为什么到怎么样再到实线得过程完全掌握它。更要在实践中体验新技术的优缺点并总结。
比如说了一大推 Promise 的优点,它有什么缺点呢,主要是有三个:
- 无法取消 Promise,一旦创建就会立即执行无法中途取消。
- 如果不设置回调函数,内部抛出的错误我们无法感知。
- 当处于 pending 状态时,无法得知目前的进展(刚刚开始还是即将完成)。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!