前排占楼

个人开源项目 — Vchat 正式上线了,欢迎各位小哥哥小姐姐体验。如果觉得还行的话,记得给个star哟 ^_^。

  • 线上预览
  • Vchat源码
  • Vchat原文地址
  • 本文源码
  • 知乎

言归正传,你经历过绝望吗?

众所周知,js是单线程异步机制的。这样就会导致很多异步处理会嵌套很多的回调函数,最为常见的就是ajax请求,我们需要等请求结果返回后再进行某些操作。如:

    function success(data, status) {console.log(data)}function fail(err, status) {console.log(err)}ajax({url: myUrl,type: 'get',dataType: 'json',timeout: 1000,success: success(data, status),fail: fail(err, status)})

乍一看还行啊,不够绝望啊,让绝望来的更猛烈一些吧!那么试想一下,如果还有多个请求依赖于上一个请求的返回值呢?五个?六个?代码就会变得非常冗余和不易维护。这种现象,我们一般亲切地称它为‘回调地狱’。现在解决回调地狱的手段有很多,比如非常方便的async/await、Promise等。

我们现在要讲的是Promise。在如今的前端面试中,Promise简直是考点般的存在啊,十个有九个会问。那么我们如何真正的弄懂Promise呢?俗话说的好,‘想要了解它,先要接近它,再慢慢地实现它’。自己实现一个Promise,不就什么都懂了。

其实网络上关于Promise的文章有很多,我也查阅了一些相关文章,文末有给出相关原文链接。所以本文侧重点是我在实现Promise过程中的思路以及个人的一些理解,有感兴趣的小伙伴可以一起交流。

如果用promise实现上面的ajax,大概是这个效果:

    ajax().success().fail();

何为 Promise

那么什么是Promise呢?

  1. Promise是为了解决异步编程的弊端,使你的代码更有条理、更清晰、更易维护。
  2. Promise是一个构造函数(或者类),接受一个函数作为参数,该函数接受resolve,reject两个参数。
  3. 它的内部有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),其中pending可以转化为fulfilled或者和rejected,但是不能逆向转化,成功和失败也不能相互转化。
  4. value、reason成功的参数和失败的错误信息。
  5. then方法,实现链式调用,类似于jq。

基本用法:

    let getInfo = new Promise((resolve, reject) => {setTimeout(_ => {let ran = Math.random();console.log(ran);if (ran > 0.5) {resolve('success');} else {reject('fail');}}, 200);});getInfo.then(r => {return r + ' ----> Vchat';}).then(r => {console.log(r);}).catch(err => {console.log(err);})// ran > 0.5输出 success ----> Vchat// ran <= 0.5输出 fail

先定个小目标,然后一步步实现它。

构建Promise

  • 基础构造

    首先需要了解一下基本原理。我第一次接触Promise的时候,还很懵懂(捂脸)。那会只知道这么写,不知道到底是个什么流程走向。下面,我们来看看最基本的实现:

        function Promise(Fn){let resolveCall = function() {console.log('我是默认的');}; // 定义为函数是为了防止没有then方法时报错this.then = (onFulfilled) => {resolveCall = onFulfilled;};function resolve(v){ // 将resolve的参数传给then中的回调resolveCall(v);}Fn(resolve);}new Promise((resolve, reject) => {setTimeout(_ => {resolve('success');}, 200)}).then(r => {console.log(r);});// success

    这里需要注意的是,当我们new Promise 的时候Promise里的函数会直接执行。所以如果你想定义一个Promise以待后用,比如axios封装,需要用函数包装。比如这样:

        function myPromise() {return new Promise((resolve, reject) => {setTimeout(_ => {resolve('success');}, 200)})}// myPromise().then()

    再回到上面,在new Promise 的时候会立即执行fn,遇到异步方法,于是先执行then中的方法,将 onFulfilled 存储到 resolveCall 中。异步时间到了后,执行 resolve,从而执行 resolveCall即储存的then方法。这是输出的是我们传入的‘success’

    这里会有一个问题,如果 Promise 接受的方法不是异步的,则会导致 resolve 比 then 方法先执行。而此时 resolveCall 还没有被赋值,得不到我们想要的结果。所以要给resolve加上异步操作,从而保证then方法先执行。

        // 直接resolvenew Promise((resolve, reject) => {resolve('success');}).then(r => {console.log(r); // 输出为 ‘我是默认的’,因为此时then方法还没有,then方法的回调没有赋值给resolveCall,执行的是默认定义的function() {}。});// 加上异步处理,保证then方法先执行function resolve(v){setTimeout(_ => {resolveCall(v);})}
  • 增加链式调用

    链式调用是Promise非常重要的一个特征,但是上面写的那个函数显然是不支持链式调用的,所以我们需要进行处理,在每一个then方法中return一下this。

        function Promise(Fn){this.resolves = []; // 方便存储onFulfilledthis.then = (onFulfilled) => {this.resolves.push(onFulfilled);return this;};let resolve = (value) =>{ // 改用箭头函数,这样不用担心this指针问题setTimeout(_ => {this.resolves.forEach(fn => fn(value));});};Fn(resolve);}

    可以看到,这里将接收then回调的方法改为了Promise的属性resolves,而且是数组。这是因为如果有多个then,依次push到数组的方式才能存储,否则后面的then会将之前保存的覆盖掉。这样等到resolve被调用的时候,依次执行resolves中的函数就可以了。这样可以进行简单的链式调用。

        new Promise((resolve, reject) => {resolve('success');}).then(r => {console.log(r); // success}).then(r => {console.log(r); // success});

    但是我们会有这样的需求, 某一个then链想自己return一个参数供后面的then使用,如:

        then(r => {console.log(r);return r + ' ---> Vchat';}).then();

    要做到这一步,需要再加一个处理。

        let resolve = (value) =>{setTimeout(_ => {// 每次执行then的回调时判断一下是否有返回值,有的话更新valuethis.resolves.forEach(fn => value = fn(value) || value);});};
  • 增加状态

    我们在文章开始说了Promise的三种状态以及成功和失败的参数,现在我们需要体现在自己写的实例里面。

        function Promise(Fn){this.resolves = [];this.status = 'PENDING'; // 初始为'PENDING'状态this.value;this.then = (onFulfilled) => {if (this.status === 'PENDING') { // 如果是'PENDING',则储存到数组中this.resolves.push(onFulfilled);} else if (this.status === 'FULFILLED') { // 如果是'FULFILLED',则立即执行回调console.log('isFULFILLED');onFulfilled(this.value);}return this;};let resolve = (value) =>{if (this.status === 'PENDING') { // 'PENDING' 状态才执行resolve操作setTimeout(_ => {//状态转换为FULFILLED//执行then时保存到resolves里的回调//如果回调有返回值,更新当前valuethis.status = 'FULFILLED';this.resolves.forEach(fn => value = fn(value) || value);this.value = value;});}};Fn(resolve);}

    这里可能会有同学觉得困惑,我们通过一个例子来说明增加的这些处理到底有什么用。

        let getInfo = new Promise((resolve, reject) => {resolve('success');}).then(_ => {console.log('hahah');});setTimeout(_ => { getInfor.then(r => {console.log(r); // success})}, 200);

    在resolve函数中,判断了'PENDING' 状态才执行setTimeout方法,并且在执行时更改了状态为'FULFILLED'。这时,如果运行这个例子,只会输出一个‘success’,因为接下来的异步方法调用时状态已经被改为‘FULFILLED’,所以不会再次执行。

    这种情况要想它可以执行,就需要用到then方法里的判断,如果状态是'FULFILLED',则立即执行回调。此时的传参是在resolve执行时保存的this.value。这样就符合Promise的状态原则,PENDING不可逆,FULFILLED和REJECTED不能相互转化。

  • 增加失败处理

    可能有同学发现我一直没有处理reject,不用着急。reject和resolve流程是一样的,需要一个reason做为失败的信息返回。在链式调用中,只要有一处出现了reject,后续的resolve都不应该执行,而是直接返回reject。

        this.reason;this.rejects = [];// 接收失败的onRejected函数if (this.status === 'PENDING') {this.rejects.push(onRejected);}// 如果状态是'REJECTED',则立即执行onRejected。if (this.status === 'REJECTED') {onRejected(this.reason);}// reject方法let reject = (reason) =>{if (this.status === 'PENDING') {setTimeout(_ => {//状态转换为REJECTED//执行then时保存到rejects里的回调//如果回调有返回值,更新当前reasonthis.status = 'REJECTED';this.rejects.forEach(fn => reason = fn(reason) || reason);this.reason = reason;});}};// 执行Fn出错直接rejecttry {Fn(resolve, reject);}catch(err) {reject(err);}

    在执行储存then中的回调函数那一步有一个细节一直没有处理,那就是判断是否有onFulfilled或者onRejected方法,因为是允许不要其中一个的。现在如果then中缺少某个回调,会直接push进undefined,如果执行的话就会出错,所以要先判断一下是否是函数。

        this.then = (onFulfilled, onRejected) => {// 判断是否是函数,是函数则执行function success (value) {return typeof onFulfilled === 'function' && onFulfilled(value) || value;}function erro (reason) {return typeof onRejected === 'function' && onRejected(reason) || reason;}// 下面的处理也要换成新定义的函数if (this.status === 'PENDING') {this.resolves.push(success);this.rejects.push(erro);} else if (this.status === 'FULFILLED') {success(this.value);} else if (this.status === 'REJECTED') {erro(this.reason);}return this;};

    因为reject回调执行时和resolve基本一样,所以稍微优化一下部分代码。

        if(this.status === 'PENDING') {let transition = (status, val) => {setTimeout(_ => {this.status = status;let f = status === 'FULFILLED',queue = this[f ? 'resolves' : 'rejects'];queue.forEach(fn => val = fn(val) || val);this[f ? 'value' : 'reason'] = val;});};function resolve(value) {transition('FULFILLED', value);}function reject(reason) {transition('REJECTED', reason);}}
  • 串行 Promise

    假设有多个ajax请求串联调用,即下一个需要上一个的返回值作为参数,并且要return一个新的Promise捕捉错误。这样我们现在的写法就不能实现了。

    我的理解是之前的then返回的一直是this,但是如果某一个then方法出错了,就无法跳出循环、抛出异常。而且原则上一个Promise,只要状态改变成‘FULFILLED’或者‘REJECTED’就不允许再次改变。

    之前的例子可以执行是因为没有在then中做异常的处理,即没有reject,只是传递了数据。所以如果要做到每一步都可以独立的抛出异常,从而终止后面的方法执行,还需要再次改造,我们需要每个then方法中return一个新的Promise。

        // 把then方法放到原型上,这样在new一个新的Promise时会去引用prototype的then方法,而不是再复制一份。Promise.prototype.then = function(onFulfilled, onRejected) {let promise = this;return new Promise((resolve, reject) => {function success (value) {let val = typeof onFulfilled === 'function' && onFulfilled(value) || value;resolve(val); // 执行完这个then方法的onFulfilled以后,resolve下一个then方法}function erro (reason) {let rea = typeof onRejected === 'function' && onRejected(reason) || reason;reject(rea); // 同resolve}if (promise.status === 'PENDING') {promise.resolves.push(success);promise.rejects.push(erro);} else if (promise.status === 'FULFILLED') {success(promise.value);} else if (promise.status === 'REJECTED') {erro(promise.reason);}});};

    在成功的函数中还需要做一个处理,用以支持在then的回调函数(onFulfilled)中return的Promise。如果onFulfilled方法return的是一个Promise,则直接执行它的then方法。如果成功了,就继续执行后面的then链,失败了直接调用reject。

        function success(value) {let val = typeof onFulfilled === 'function' && onFulfilled(value) || value;if(val && typeof val['then'] === 'function'){ // 判断是否有then方法val.then(function(value){ // 如果返回的是Promise 则直接执行得到结果后再调用后面的then方法resolve(value);},function(reason){reject(reason);});}else{resolve(val);}}

    找个例子测试一下

        function getInfo(success, fail) {return new Promise((resolve, reject) => {setTimeout(_ => {let ran = Math.random();console.log(success, ran);if (ran > 0.5) {resolve(success);} else {reject(fail);}}, 200);})}getInfo('Vchat', 'fail').then(res => {console.log(res);return getInfo('可以线上预览了', 'erro');}, rej => {console.log(rej);}).then(res => {console.log(res);}, rej => {console.log(rej);});// 输出// Vchat 0.8914818954810422// Vchat// 可以线上预览了 0.03702367800412443// erro

总结

到这里,Promise的主要功能基本上都实现了。还有很多实用的扩展,我们也可以添加。
比如 catch可以看做then的一个语法糖,只有onRejected回调的then方法。其它Promise的方法,比如.all、.race 等等,感兴趣的小伙伴可以自己实现一下。另外,文中如有不对之处,还请指出。

    Promise.prototype.catch = function(onRejected){return this.then(null, onRejected);}

相关文章

  • 手把手教你实现一个完整的 Promise
  • 教你一步一步实现一个Promise - 飞魚
  • 阮一峰老师的es6-Promise章节

交流群

本群是Vchat前端交流群,欢迎各种技术交流,期待你的加入

站住,你这个Promise!相关推荐

  1. setTimeout、setInterval、promise、async/await的顺序详解(多种情况,非常详细~)

    本文很长,列举的情况很多. 在阅读本文之前,如果您有充足的时间,请新建一个项目与本文一同实践. 每段代码都有对应的解释,但是自己动手尝试印象才会更深哦~ setInterval:表示多久执行一次,需要 ...

  2. C++多线程:异步操作std::async和std::promise

    文章目录 std::async 简介 使用案例 std::promise 简介 成员函数 总结 之前的文章中提到了C++多线程中的异步操作机制 C++ 多线程:future 异步访问类(线程之间安全便 ...

  3. ES6中的Promise详解

    Promise 在 JavaScript 中很早就有各种的开源实现,ES6 将其纳入了官方标准,提供了原生 api 支持,使用更加便捷. 定义 Promise 是一个对象,它用来标识 JavaScri ...

  4. 关于ES6中Promise的应用-顺序合并Promise,并将返回结果以数组的形式输出

    1.Promise 基础知识梳理 创建一个Promise实例 const promise = new Promise(function(resolve, reject) {if (success){r ...

  5. promise实现多个请求并行串行执行

    早上查资料,偶然发现这个话题,发现自己并不会,于是乎,下来研究了一下. 想想之前我们用jquery写请求的时候,要实现请求的串行执行,我们可能是这么做的. $.ajax({url: '',data: ...

  6. 异步编程之Promise(2):探究原理

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

  7. 自己动手写cpu pdf_自己动手写 Promise

    这段时间在学习Promise,但始终不得要领.为了更好地理解Promise,我决定自己实现一个简易版的Promise,以学习Promise工作原理.该工程名为ToyPromise,仓库地址如下: ht ...

  8. promise 和 async await区别

     什么是Async/Await? async/await是写异步代码的新方式,以前的方法有回调函数和Promise. async/await是基于Promise实现的,它不能用于普通的回调函数. as ...

  9. Promise - js异步控制神器

    微信小程序开发交流qq群   581478349    承接微信小程序开发.扫码加微信. 正文: 首先给来一个简单的demo看看Promise是怎么使用的: <!DOCTYPE html> ...

最新文章

  1. AD5272数字变阻器
  2. 华为harmonyos公测,华为鸿蒙 Harmony OS 2.0 第二轮公测已开启,赶紧申请报名
  3. SAP Cloud Platform 上CPI的初始化工作
  4. sql 中实现打乱数据的排序
  5. div超出不换行_div+CSS设置一行内文字超过宽度不换行且不显示
  6. PyTorch:将模型转换为torch.jit.ScriptModule
  7. 谷歌浏览器添加.crx插件
  8. java reactor例子_ProjectReactor响应式编程入门例子
  9. word打开老是配置进度_word怎么转pdf?两个值得学习的高效转换法
  10. Java 代码块:静态代码块、构造代码块、构造函数块
  11. Django面试题(一)django的中间件最多可以写几个方法?使用中间件做什么?
  12. 定时任务实现方式对比
  13. linux虚拟摄像头 开源,(四) 虚拟摄像头vivi体验
  14. 《经济学通识》八、劳动关系
  15. CUDA出现:无法找到兼容的图形硬件
  16. 超实用的 IPTV 管理工具,xTeVe 助你定制专属电视频道。
  17. Python将url转换作为合法文件名
  18. 怎么找网图本人_如何通过一张照片找到一个人的位置?https://www.zhihu.com/zvideo/1312521748374917120...
  19. yoyo鹿鸣lumi动态壁纸人工桌面(软件篇)
  20. 便携式计算机是笔记本电脑吗,便携式笔记本电脑推荐

热门文章

  1. java调用QQ邮箱发送邮件
  2. 《数据库原理》学生表,课程表,选课表的相关内容
  3. 手机User-Agent
  4. 可裂解组织蛋白酶的ADC偶联物-靶向抗体偶联技术
  5. Sringboot2.x整合Redis缓存,设置过期时间
  6. 团体程序设计天梯赛-练习集 L1-034 点赞
  7. 1079 活字印刷
  8. 微信撤回的消息找不到?你OUT了,看看python程序怎么找回!
  9. 广工计算机学院校区,番禺校区 | 广工最神秘的校区
  10. Oracle启动错误:ORA-00821: Specified value of sga_target 2352M is too small, needs to be at least 4352M