写在前面:
在目前的前端分开中,我们对于异步方法的使用越来越频繁,那么如果处理异步方法的返回结果,如果优雅的进行异步处理对于一个合格的前端开发者而言就显得尤为重要,其中在面试中被问道最多的就是对Promise方法的掌握情况,本章将和大家一起分析和完成一个Promise方法,希望对你的学习有一定的帮助。
了解Promise
既然我们是要模仿ES6的Promise,那我们必然要知道这个方法主要都是用来干什么的,有哪些参数,有什么特性,为什么要使用Promise及如何使用等等。
为什么要使用它?
1.先统一执行AJAX逻辑,不关心如何处理结果,然后,在需要的时候处理AJAX结果
不知道大家有没有思考过下面的问题,JavaScript的运行都是单线程的,但是如果我们要处理类似于网络请求(ajax),浏览器的一些事件等就要用到异步执行,,大多都是下面这个样子:
function callback() { console.log('我是一个回调函数');}console.log('异步方法之前');setTimeout(callback, 1000); // 1秒钟后调用callback函数console.log('异步方法之后');复制代码
然后得到下面的结果:
异步操作会在将来的某个时间点触发一个函数调用,AJAX就是典型的异步操作。以jq代码为例:
$.ajax({ type: "POST", url: "some.php", data: "name=John&location=Boston", success: function(msg){ alert( "Data Saved: " + msg ); }});复制代码
在上面的代码中我们虽然能够得到ajax的操作结果,但是这种写法不利于我们复用,说白了异步的处理和返回结果在同一个块内,很不美观和优雅,下面来看看Promise是怎么处理这样的情况的:
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最强大的地方就在于then方法,所以不管是成功还是失败我们最终都要将成功和失败的值传递给then,为了方便调用,我们用两个变量来接收各自的值
看下面的代码我们继续分析接下来promise是进行怎么操作的:
let p = new Promise((resolve,reject)=>{ resolve(111);})p.then((value) => { console.log(value)}, (reason) => { console.log('err', reason);})复制代码
上面的代码最终打印结果为111,这时候我们分析在promise中如果成功了,那么then方法中的成功回调就会立即执行,如果失败了,失败的回调也会立即执行,所以我们可以继续完善我们的代码:
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的状态一开始并不是成功或者失败,而是先将成功和失败的回调函数保存起来,等待异步完成之后在执行相对应的成功或者失败的回调,所以接下来我们代码可以这样写:
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方法中的两个参数都是可选参数:
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的结果,这样一层层的状态等待就会形成回调地狱。
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的执行环境还是当前的上下文,也就是同步。做一下小小的改动,他就是异步了:
扩展方法实现
因为在我们的分析中还有一个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+的规范?因为我们不能写一个玩具代码来应付面试考官和自己,你需要让自己的代码更具体有可读性和实用性,需要去规避可能遇到的各种因为调用而产生的问题,让你自己的代码更加无懈可击,在使用场景上也会更加丰富