在 ajax 中,我们提到了 Promise 式封装
那么什么是 Promise 呢?
首先要来介绍一下什么是异步回调
我们在计算机网络中学过通信方式一般有三种:全双工、半双工、单工
最理想的方式当然是全双工
异步回调
异步
异步也是一种全双工
简单来说,就是当一个任务发出后,发件人不需要等待执行方的返回结果,而可以先去做其它事情,等待
比如你加某个人为 qq 好友,发出请求后你就不需要管了,可以自己先去聊天,等到对方同意或拒绝你的好友请求,你才会收到通知,回来处理这件事
回调
那回调又是什么呢?回调其实解决了异步的一个非常重要的问题——我要这个结果做什么用,简单说就是当异步结果返回的时候,该做什么呢
在上文 qq 好友的例子中,qq 通知你好友请求返回结果了,你根据结果做出的动作就是回调
比如好友请求通过,你的回调就是找好友聊天
比如失败,你的回调可能是自我分析是什么情况,也可以是什么都不做
在浏览器中,就是浏览器的异步请求取回结果的时候,如果是成功,要进行什么处理、给出什么输出呢?失败的话要不要处理、怎么处理这个错误呢?
举个例子,就是 setTimeout
1 | const fn = ()=>console.log('success') |
执行 setTimeout 后,将在 1000ms 后执行 fn
此处的 fn 就是回调函数
异步和回调的关系
上文可以看出,回调函数其实就是事件完成后,处理结果的方法
但其实事件不一定是异步的,也可以是同步的
即便是同步的函数,也可以使用回调函数,区别只不过是异步任务执行后可以先做别的再收通知,同步任务执行后必须原地等通知罢了
但是如果一层层回调,会变成什么样呢
1 | setTimeout(()=>{ |
看,是不是随着回调层数的增加,代码逐渐向右突出。。。这也太难看了
这就是回调地狱
而为了解决这个问题,提出了 Promise
Promise 定义
Promise 是将“生产者代码”和“消费者代码”连接在一起的一个特殊的 JavaScript 对象。
用我们的类比来说:这就是就像是“订阅列表”。
“生产者代码”花费它所需的任意长度时间来产出所承诺的结果,而 “promise” 将在它准备好时,将结果向所有订阅了的代码开放。
Promise 用途
一开始主要是为了解决回调地狱而产生的
现在则已经是前端异步处理的统一解决方案
Promise 用法
一般在函数中作为函数返回值,例如
1 | function task(){ |
上述代码也可以直接赋值
1 | let task = new Promise((resolve,reject)=>{ |
首先介绍一下,Promise 有以下状态
- pending,Promise 尚未返回结果
- settled,Promise 已经返回结果,但不确定是成功还是失败
- fulfilled,Promise 已经成功(resolve)返回
- rejected,Promise 已经失败(reject)返回
可见,Promise 的构造函数要求传入一个包含两个参数的 executor(可执行代码段),两个参数分别是 Promise 给出的两个函数,分别指示 resolve 和 reject 调用的函数
当然两个参数也可以叫别的名字,只要顺序不变就没有影响,但是一般都是叫 resolve 和 reject
这段 executor 是立即执行的,但是对结果的处理是异步的,直到 settled 之前,这个 Promise 都不算结束
即使在 executor 中,半路上就 resolve/reject 了,executor 的代码段还是会完整执行,不会中断
那么怎么处理 Promise 的返回结果呢?
Promise 方法
Promise 的 prototype 上有 3 个重要的回调方法
- then
- catch
- finally
3 个方法都支持链式调用
then
then 方法的函数声明伪代码如下
1 | promise.then( |
可以接受两个参数,分别表示对于 fulfilled 和 rejected 状态的 Promise 结果的处理函数
第一个函数接受一个参数,表示成功的结果
第二个函数接受一个参数,表示失败的原因
其中 fulfilled 结果处理函数是必要的,rejected 处理函数可以不写出,但如果 Promise 被 reject,而没有其它地方处理这个 error 的话,会直接报错,停止解释器
上例中的代码,加上 then 回调后,变为如下模样
1 | let task = new Promise((resolve,reject)=>{ |
then 可以多级传递,比如
1 | let task = new Promise((resolve,reject)=>{ |
链式调用中,结果会沿着链传递
但是分开调用是不对的,比如
1 | let task = new Promise((resolve,reject)=>{ |
分开调用,结果不会互相传递
then 中除了被动出错,也可以使用 throw 子句主动抛出错误
catch
上例中使用 catch 的话,一般可以写成以下模样
1 | let task = new Promise((resolve,reject)=>{ // executor |
如果在 Promise 的 executor 或者 then 中出错了,都可以用 catch 捕获错误,因为 catch 在他们的下游
此时如果是 executor 出错,then 会被跳过,沿着 Promise 链往下寻找 catch 直到找到第一个 catch 为止
catch 既然支持链式调用,那么 catch 自然也可以再抛出
比如
1 | let task = new Promise((resolve,reject)=>{ // executor |
注意,此处要使用 throw 子句来抛出,不能是 return
如果是 return,接下来的结果就要用 then 来接收
finally
上例中使用 finally 的话,一般可以写成以下模样
1 | let task = new Promise((resolve,reject)=>{ // executor |
显然,finally 不一定要写在最后,一般只是用来做阶段性封口而已
比如此处,在 executor 执行完毕后,不论 executor 是否出错,都会执行 finally,告诉用户 promise 开始了
然后,不论是 resolve 还是 reject,结果都会越过 finally 传递到可以处理这个结果的回调
以上就是 Promise 的基本用法
现在来深究一下 Promise 这么个好东
Promise 手写
面试必考的手写 Promise 部分,你会多少呢?
Promise
首先先实现基本的 Promise 功能——resolve 和 reject
容易想到以下基本形
1 | function P(executor){ |
那么 resolve 和 reject 从哪里来?显然要由 Promise 提供
这两个函数还兼顾了取结果和改状态的效果,所以得到如下形状
1 | function P(executor){ |
修改数据的时候,要注意不能随便覆盖结果
注意,我们要在 resolve 和 reject 中使用访问 this 的 status 和 result,所以这两个函数
- 必须有自己的 this,不能是箭头函数
- 必须指向当前操作的对象,所以要使用 bind 绑定 this
这样 Promise 最基本的功能就实现了,接下来实现最重要的功能 then
Promise.then
容易得到以下形状
1 | P.prototype.then = function(success,fail){ |
但是这时候我们发现,这个 then 只能执行同步的链式调用
那怎么异步调用呢?答案就是发布订阅模式
可以得到以下代码
1 | function P(executor){ |
运行测试例
1 | let p=new P((resolve,reject)=>{ |
可以得到以下结果
1 | start |
现在 then 也可以支持异步返回了!但是 catch 似乎还没实现,不过我水平不够了,到此为止了2333
Promise.all
Promise.all 的核心要义就是传入多个 Promise,然后按顺序返回所有结果
容易想到以下形状
1 | P.all = function(promises){ |
但是我们还需要一个 P.resolve 来得到一个新的 P
所以还要写
1 | P.resolve = data => { |
运行以下测试例
1 | let promise1 = P.resolve(3) |
可得结果
1 | [3, "foo", 42] |
Promise.race
Promise.race 的核心要义就是传入多个 Promise,然后返回第一个决议的结果,无论是成功还是失败
其实把 all 的计数器去掉就可以了
容易想到以下形状
1 | P.race = function(promises){ |
运行以下测试例
1 | let promise1 = new P(function(resolve, reject) { |
可得结果
1 | two |
完整的 Promise
总结一下,可以得到如下的手写 Promise
1 | function P(executor){ |
参考链接
谢谢阅读