前端面试考点之---手写Promise

写在前面:php

在目前的前端分开中,咱们对于异步方法的使用愈来愈频繁,那么若是处理异步方法的返回结果,若是优雅的进行异步处理对于一个合格的前端开发者而言就显得尤其重要,其中在面试中被问道最多的就是对Promise方法的掌握状况,本章将和你们一块儿分析和完成一个Promise方法,但愿对你的学习有必定的帮助。前端

了解Promise

既然咱们是要模仿ES6的Promise,那咱们必然要知道这个方法主要都是用来干什么的,有哪些参数,有什么特性,为何要使用Promise及如何使用等等。面试

为何要使用它?

1.先统一执行AJAX逻辑,不关心如何处理结果,而后,在须要的时候处理AJAX结果

不知道你们有没有思考过下面的问题,JavaScript的运行都是单线程的,可是若是咱们要处理相似于网络请求(ajax),浏览器的一些事件等就要用到异步执行,,大多都是下面这个样子:ajax

function callback() {
    console.log('我是一个回调函数');
}
console.log('异步方法以前');
setTimeout(callback, 1000); // 1秒钟后调用callback函数
console.log('异步方法以后');
复制代码

而后获得下面的结果:promise

异步操做会在未来的某个时间点触发一个函数调用,AJAX就是典型的异步操做。以jq代码为例:浏览器

$.ajax({
   type: "POST",
   url: "some.php",
   data: "name=John&location=Boston",
   success: function(msg){
     alert( "Data Saved: " + msg );
   }
});
复制代码

在上面的代码中咱们虽然可以获得ajax的操做结果,可是这种写法不利于咱们复用,说白了异步的处理和返回结果在同一个块内,很不美观和优雅,下面来看看Promise是怎么处理这样的状况的:bash

let p = new Promise(function (resolve, reject) {
    setTimeout(() => {//使用定时器来模拟异步
        resolve(100)
    }, 1000);
});
p.then(function (data) {
    console.log(data)
})
复制代码

能够看出p.then的调用能够是任什么时候候,只要咱们须要时就能够拿到刚才返回结果。而不是像jq同样在ajax有结果会就要对结果进行当即处理。网络

2.支持链式调用

在过去,咱们要进行多重异步请求的时候,一不当心就会造成回调地狱,相似于下面的这样:异步

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);//三次函数嵌套调用以后获得结果
    }, failureCallback);
  }, failureCallback);
}, failureCallback);
复制代码

无疑,上面的函数在于阅读性和维护性上面都让咱们有些力不从心,下面用Promise来实现一下上面的代码,就清晰的多:函数

doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
复制代码

又细心的小伙伴会发现咱们的错误处理都会被集中到catch中执行,这也就是我想说的第三个特色

3.经过捕获全部的错误,promise解决了回调厄运金字塔的基本缺陷。

说了这么多,我想小伙伴已经多Promise有了必定的认识,那我就根据实际的使用,凭借本身的理解和PromiseA+规范的描述,来实现一个属于本身的promise

手写符合规范的promise

先来看代码:

let p = new Promise((resolve,reject)=>{
  resolve();
  //reject();
})
复制代码

根据上面的代码咱们能够看出,promise内部是一个当即执行的构造器函数,函数中有两个参数分别为resolve,reject,因此咱们本身的代码应该这样写

function Promise() {
    function resolve() { }
    function reject() { }
    executor(resolve,reject) 
 }
复制代码

能够看到咱们获得了两个函数resolve()和reject(),并且根据promiseA+规范文档中说明的:

此处咱们能够获得Promise有三个状态 pending(等待状态),fulfilled(成功状态),rejected(失败状态);这个三个状态之间的关系咱们用一张图来讲明一下:

首先Promise在执行的时候状态都为pending,也就是等待状态,而后等待状态能够分别向成功状态和失败状态转换,可是一旦状态不是pending状态以后,这个promise的状态就没法更改,且失败状态和成功状态之间是不能相互转换的,进一步完善代码以下:

由于promise最强大的地方就在于then方法,因此不论是成功仍是失败咱们最终都要将成功和失败的值传递给then,为了方便调用,咱们用两个变量来接收各自的值

上面已经提到promise最重要的方法就是then方法,那么为了可以在实例以后调用这个方法,咱们必须将这个方法写在他的原型链上面,而且他接受两个参数,一个是成功的回调,一个是失败的回调

看下面的代码咱们继续分析接下来promise是进行怎么操做的:

let p = new Promise((resolve,reject)=>{
  resolve(111);
})
p.then((value) => {
  console.log(value)
}, (reason) => {
  console.log('err', reason);
})
复制代码

上面的代码最终打印结果为111,这时候咱们分析在promise中若是成功了,那么then方法中的成功回调就会当即执行,若是失败了,失败的回调也会当即执行,因此咱们能够继续完善咱们的代码:

在上面的代码中咱们只是使用同步方式,让promise函数当即执行并传入数字:111,若是是异步的状况呐?让咱们进行下面的测试:

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(111);
    }, 1000);
  
})
p.then((value) => {
  console.log(value)
}, (reason) => {
  console.log('err', reason);
})
复制代码

这时候你会发现结果是在一秒钟以后打印出来的,也就是说,then方法中成功和失败的回调,是在promise的异步执行完成以后才被触发的,因此你在调用then方法的时候promise的状态一开始并非成功或者失败,而是先将成功和失败的回调函数保存起来,等待异步完成以后在执行相对应的成功或者失败的回调,因此接下来咱们代码能够这样写:

而后咱们继续进行尝试,此次咱们尝试让promise抛出一个错误看它会怎么处理?

那么反应在咱们的代码中就能够这样写:

在promise中咱们能够进行链式调用的方式来屡次的进行then,如同下面的代码:

let p = new Promise((resolve, reject) => {
    resolve(111)
  
})
p.then((value) => {
  return value+'第二次'
}, (reason) => {
  console.log('err', reason);
    }).then((data) => {
console.log(data)
    }, () => {
        
    })
复制代码

执行代码以后咱们不可贵到打印的结果为:111第二次,那么也就是若是你的then方法的成功回调函数若是返回一个值,那么咱们在下一个then方法中对应的成功回调中也能够继续使用这个值,换句话说,这个值会被看成下一次then中成功回调的参数传递回来。 相同的咱们测试若是出现错误的事情,会发现错误会传递给第二次的失败中

let p = new Promise((resolve, reject) => {
    resolve(111)
  
})
p.then((value) => {
  throw new Error()
}, (reason) => {
  console.log('err', reason);
    }).then((data) => {
console.log(data)
    }, () => {
        console.log('第二获得失败')
    })
复制代码

打印结果为:第二获得失败 固然若是本次回调函数中内容为空,那么下次then中会直接走成功,并且若是是失败以后也仍是能够成功的,获得结果understand,若是你不想在then方法中处理错误,你还可使用catch方法来最终捕获错误,既然成功或者失败中能够不写参数,也就是这能够为一个空函数,也就是说then方法中的两个参数都是可选参数:

上面咱们已经基本上尝试了各类返回值,那么还有一种状况也是咱们须要考虑的,那就是若是返回一个promise方法会放生什么状况?

p.then(() => { 
    return new Promise((resolve, reject) => { 
        resolve(111)
    })
}, (reason) => {
  
}).then((data) => {
    console.log('成功了',data)
}, (reason) => {
    
})
复制代码

打印结果为:成功了 111

通过尝试若是返回的是一个promise函数,那么他会等待这个promise执行完成以后在返回给下一次的then,promise若是成功,就会走下一次then的成功,若是失败就会走下一次then的失败。固然这里须要注意的是,then方法中返回的回调函数不能是本身自己,若是真的这样写,那么函数执行到里面时会等待promise的结果,这样一层层的状态等待就会造成回调地狱

如今咱们的代码已经看上去原生的promise很类似了,可是为了严谨,咱们进行下面的尝试:

let promise = new Promise((resolve,reject)=>{
   resolve();
});
promise.then((value) => { // pending
    return new Promise((resolve,reject)=>{
        return new Promise((resolve,reject)=>{
            resolve(111);
         })
     })
}, (reason) => {
  console.log(reason);
});
复制代码

理论上咱们能够得出下一次then的结果为:111,由于咱们是等待promise执行完才会返回,也就是说刚才咱们的代码只是判断了第一次是promise的状况,若是像上面代码的状况同样,就会出现问题,为了规避这样的问题,咱们使用递归来执行:

细心的你可能发现,上面的截图中我还加入了一个called做为拦截器,那是由于若是有想我同样的小白用户,本身手写的promise是既能够成功也能够失败的,那么这里咱们就要判断一下,不能让两次调用都执行,只调用第一个被调用的

这样咱们的代码基本上就完美了,那咱们就试一下吧:

let promise = new Promise((resolve,reject)=>{
   resolve(1);
});
promise.then((value) => { // pending
   console.log(value)
}, (reason) => {
  console.log(reason);
    });
console.log(2);
复制代码

你会发现咱们的执行结果是1,2,可是在本文的最开始就已经提到promise是一个处理异步的函数,执行结果应该为2,1才对,那是由于咱们如今的promise的执行环境仍是当前的上下文,也就是同步。作一下小小的改动,他就是异步了:

由于刚才分析获得then方法中两个回调函数能够是可选参数,因此咱们也要处理一下:

扩展方法实现

由于在咱们的分析中还有一个catch方法,那咱们也来实现一下吧。既然是能够链式调用的方法,那咱们也必须写在原型链上面:

Promise.prototype.catch = function (onrejected) {
  return this.then(null, onrejected)
}
复制代码

固然promise还能够直接使用resolve()和reject()直接调用,是一种简便写法:

Promise.reject = function (reason) {
  return new Promise((resolve, reject) => {
    reject(reason)
  })
}
Promise.resolve = function (value) {
  return new Promise((resolve, reject) => {
    resolve(value);
  })
}
复制代码

写在最后

至此,咱们全部的promise特性就已经一一实现了,你是否已经看明白了,固然做为一个小白选手,我还有不少的不足,欢迎你们的指正,你也能够去参考promiseA+规范中的文档去看看我写的还有什么须要补充的,欢迎交流。

PS:为何要结合promiseA+的规范?由于咱们不能写一个玩具代码来应付面试考官和本身,你须要让本身的代码更具体有可读性和实用性,须要去规避可能遇到的各类由于调用而产生的问题,让你本身的代码更加无懈可击,在使用场景上也会更加丰富