写在前面:

在目前的前端分开中,我们对于异步方法的使用越来越频繁,那么如果处理异步方法的返回结果,如果优雅的进行异步处理对于一个合格的前端开发者而言就显得尤为重要,其中在面试中被问道最多的就是对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有三个状态 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+的规范?因为我们不能写一个玩具代码来应付面试考官和自己,你需要让自己的代码更具体有可读性和实用性,需要去规避可能遇到的各种因为调用而产生的问题,让你自己的代码更加无懈可击,在使用场景上也会更加丰富

手写promise的更多相关文章

  1. 手写Promise A+ 规范

    基于ES6语法手写promise A+ 规范,源码实现 class Promise { constructor(excutorCallBack) { this.status = 'pending'; ...

  2. 手写Promise看着一篇就足够了

    目录 概要 博客思路 API的特性与手写源码 构造函数 then catch Promise.resolved Promise.rejected Promise.all Promise.race 概要 ...

  3. 手写Promise中then方法返回的结果或者规律

    1. Promise中then()方法返回来的结果或者规律 我们知道 promise 的 then 方法返回来的结果值[result]是由: 它指定的回调函数的结果决定的 2.比如说下面这一段代码 l ...

  4. 前端面试题之手写promise

    前端面试题之Promise问题 前言 在我们日常开发中会遇到很多异步的情况,比如涉及到 网络请求(ajax,axios等),定时器这些,对于这些异步操作我们如果需要拿到他们操作后的结果,就需要使用到回 ...

  5. [转]史上最最最详细的手写Promise教程

    我们工作中免不了运用promise用来解决异步回调问题.平时用的很多库或者插件都运用了promise 例如axios.fetch等等.但是你知道promise是咋写出来的呢? 别怕-这里有本promi ...

  6. 手写Promise简易版

    话不多说,直接上代码 通过ES5的模块化封装,向外暴露一个属性 (function(window){ const PENDING = 'pending'; const RESOLVED = 'fulf ...

  7. 史上最简单的手写Promise,仅17行代码即可实现Promise链式调用

    Promise的使用相比大家已经孰能生巧了,我这里就不赘述了 先说说我写的Promise的问题吧,无法实现宏任务和微任务里的正确执行(也就是在Promise里面写setTimeout,setInter ...

  8. js手写'Promise'

    /* * pending:初始化成功 * fulfilled:成功 * rejected:失败 * */ function Promise(executor) {// 执行器 this.status ...

  9. js 手写 Promise

    /* * pending:初始化成功 * fulfilled:成功 * rejected:失败 * */ function Promise(cback){ this.status = 'pending ...

随机推荐

  1. Proxy模式(代理[延迟]模式)

    Proxy?? Proxy是"代理人"的意思,它指的是代替别人进行工作的人.代理实际上就是使用委托的机制,在代理的过程中你可以做点其他的事情,然后再来执行被代理对象的代码. 知识储 ...

  2. Django基础(5) ----基于双下划线的跨表查询,聚合查询,分组查询,F查询,Q查询

    一.基于双下划线的跨表查询 Django 还提供了一种直观而高效的方式在查询(lookups)中表示关联关系,它能自动确认 SQL JOIN 联系.要做跨关系查询,就使用两个下划线来链接模型(mode ...

  3. PIE使IE浏览器支持CSS3属性(圆角、阴影、渐变)

    http://www.360doc.com/content/12/1214/09/11181348_253939277.shtml PIE使IE浏览器支持CSS3属性(圆角.阴影.渐变) 2012-1 ...

  4. @Requestbody@ApiParam @PathVariable @RequestParam三者区别

    一.问题描述 由于项目是前后端分离,因此后台使用的是spring boot,做成微服务,只暴露接口.接口设计风格为restful的风格,在get请求下,后台接收参数的注解为RequestBody时会报 ...

  5. C 碎片八 结构体&枚举&联合

    一.结构体 1, 结构体定义 结构体类型的定义:任意数据类型变量的集合.用于描述一个具体的事物的信息,在C语言中描述一件事物一般都是用结构体 声明结构体类型的格式: struct  结构体名 {成员列 ...

  6. JAVA中数组介绍

    一.数组: 数组指一组数据的集合,数组中的每个数据被称作元素. 二.数组定义: 数组类型[] 数组名 = new 数组类型[元素个数或数组长度]: (注意:等号前面的[]里面不能写任何东西) 也可以以 ...

  7. Java 打印PDF文档的3种情况

    以下内容归纳了通过Java程序打印PDF文档时的3种情形.即: 静默打印 显示打印对话框打印 打印PDF时自定义纸张大小 使用工具:Spire.PDF for Java Jar导入: 方法1:通过官网 ...

  8. 从零开始的全栈工程师——js篇2.5

    数据类型与全局属性 js的本质就是处理数据 数据来自于后台的数据库所以变量就起到一个临时存储数据的这作用ECMAscirpt 制定了js的数据类型 一.数据类型 1.基本数据类型 基本数据类型就是简单 ...

  9. IOS微信6.7.4输入框失去焦点,软键盘关闭后,被撑起的页面无法回退到原来正常的位置

    近期在开发微信H5页面时碰到这个问题,如图,软键盘弹起后,若原输入框被遮挡,页面整体将会上移,然而当输入框失焦,软键盘收起后,页面未恢复,这也是ios的微信版本更新6.7.4之后才遇到的bug. 目前 ...

  10. Google地址

    Google的访问一直很不稳定,经常被墙,无意间发现以下一些地址,特此记录. https://g.ttlsa.com/ https://wen.lu/ http://sinaapp.co https: ...