资讯专栏INFORMATION COLUMN

啥?喝着阔落吃着西瓜就把Promise手写出来了???

idisfkj / 3645人阅读

摘要:嗝首先,我们通过字面可以看出来是一种解决方案,而且还有两种传统的解决方案回调函数和事件,,那么我们就来先聊聊这两种方案。

前言

虽然今年已经18年,但是今天还是要继续聊聊ES6的东西,ES6已经过去几年,可是我们对于ES6的语法究竟是掌握了什么程度,是了解?会用?还是精通?相信大家和我一样都对自己有着一个提升的心,对于新玩具可不能仅仅了解,对于其中的思想才是最吸引人的,所以接下来会通过一篇文章,来让大家对于Promise这个玩具做到精通的程度!!!

打开一瓶冰阔落~~~

Promise

</>复制代码

  1. Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。

~

首先,我们通过字面可以看出来Pormise是一种解决方案,而且还有两种传统的解决方案·回调函数事件,ok,那么我们就来先聊聊这两种方案。

回调函数 Callback

回调函数想必大家都不陌生,就是我们常见的把一个函数当做参数传递给另外一个函数,在满足了一定的条件之后再去执行回调,比如我们想要实现一个在三秒后去计算1到5的和,那么:

</>复制代码

  1. // 求和函数
  2. function sum () {
  3. return eval([...arguments].join("+"))
  4. }
  5. // 三秒后执行函数
  6. function asycnGetSum (callback) {
  7. setTimeout(function(){
  8. var result = callback(1,2,3,4,5);
  9. console.log(result)
  10. },3000)
  11. }
  12. asyncGetSum(sum);

这样的实现就是回调函数,但是如果我要实现在一段动画,动画的执行过程是小球先向右移动100px,然后再向下移动100px,在向左移动100px,每段动画持续时间都是3s.

</>复制代码

  1. dom.animate({left:"100px"},3000,"linear",function(){
  2. dom.animate({top:"100px"},3000,"linear",function(){
  3. dom.animate({left:"0px"},3000,"linear",function(){
  4. console.log("动画 done")
  5. })
  6. })
  7. })

这样就会看到形成了一个回调嵌套,也就是我们常说的回调地狱,导致代码可读性十分差。

事件

事件处理就是jQuery中的on绑定事件和trigger触发事件,其实就是我们常见的发布订阅模式,当我订阅了一个事件,那么我就是订阅者,如果发布者发布了数据之后,那么我就要收到相应的通知。

</>复制代码

  1. // 定义一个发布中心
  2. let publishCenter = {
  3. subscribeArrays:{}, // 定义一个订阅者回调函数callback
  4. subscribe:function(key,callback){
  5. // 增加订阅者
  6. if(!this.subscribeArrays[key]){
  7. this.subscribeArrays[key] = [];
  8. }
  9. this.subscribeArrays[key].push(callback)
  10. },
  11. publish:function(){
  12. //发布 第一个参数是key
  13. let params = [...arguments];
  14. let key = params.shift();
  15. let callbacks = this.subscribeArrays[key];
  16. if(!callbacks || callbacks.length === 0){
  17. // 如果没人订阅 那么就返回
  18. return false
  19. }
  20. for( let i = 0 ; i < callbacks.length; i++ ){
  21. callbacks[i].apply( this, params );
  22. }
  23. }
  24. };
  25. // 订阅 一个wantWatermelon事件
  26. publishCenter.subscribe("wantWatermelon",function(){console.log("恰西瓜咯~~")})
  27. //触发wantWatermelon事件 好咯 可以看到 恰西瓜咯
  28. publishCenter.publish("wantWatermelon")

恰西瓜中~~~

Promise A+

嗝~ok,吃完我们进入正题,看到上面异步编程如此如此如此麻烦,对于我这种头大用户,当然是拒绝的啊,还好我们有PormisePormise大法好),下面我们就来通过实现一个Promise去更深的了解Promise的原理,首先我们了解一下PromiseA+,它是一种规范,用来约束大家写的Promise方法的,为了让大家写的Promise杜绝一些错误,按照我们所期望的流程来走,因此就出现了PromiseA+规范。

Promise特点

我们根据PromiseA+文档来一步一步的看Promise有什么特点。

首先我们看文档的2.1节,题目是Promise states,也就是说讲的是Promise的状态,那么都说了些什么呢,我们来看一哈:

</>复制代码

  1. 一个promise只有三种状态,pending态,fulfilled态(完成态),rejected(拒绝态)

  2. 当promise处于pending态时,可能转化成fulfilled或者rejected

  3. 一旦promise的状态改成了fulfilled后,状态就不能再改变了,并且需要提供一个不可变的value

  4. 一旦promise的状态改成了rejected后,状态就不能再改变了,并且需要提供一个不可变的reason

ok,那么我们就开始写我们自己的Promise,我们先看看一段正常Promise的写法

</>复制代码

  1. // 成功或者失败是需要提供一个value或者reason
  2. let promise1 = new Promise((resolve,rejected)=>{
  3. // 可以发现 当我们new Promise的时候这句话是同步执行的 也就是说当我们初始化一个promise的时候 内部的回调函数(通常我们叫做执行器executor)会立即执行
  4. console.log("hahahha");
  5. // promise内部支持异步
  6. setTimeout(function(){
  7. resolve(123);
  8. },100)
  9. // throw new Error("error") 我们也可以在执行器内部直接抛出一个错误 这时promise会直接变成rejected态
  10. })

根据我们上面的代码还有PromiseA+规范中的状态说明,我们可以知道Promise已经有了下面几个特点

promise有三种状态 默认pendingpending可以变成fulfilled(成功态)或者rejected(失败态),而一旦转变之后就不能在变成其他值了

promise内部有一个value 用来存储成功态的结果

promise内部有一个reason 用来存储失败态的原因

promise接受一个executor函数,这个函数有两个参数,一个是resolve方法,一个是reject方法,当执行resolve时,promise状态改变为fulfilled,执行reject时,promise状态改变为rejected

默认 new Promise 执行的时候内部的executor函数执行

promise内部支持异步改变状态

promise内部支持抛出异常,那么该promise的状态直接改成rejected

我们接下来继续看PromiseA+文档:

</>复制代码

  1. promise必须要有一个then方法,用来访问它当前的value或者是reason
  2. 该方法接受两个参数onFulfilled(成功回掉函数),onRejected(失败回调函数) promise.then(onFulfilled, onRejected)

  3. 这两个参数都是可选参数,如果发现这两个参数不是函数类型的话,那么就忽略 比如 promise.then().then(data=>console.log(data),err=>console.log(err)) 就可以形成一个值穿透

  4. onFulfilled必须在promise状态改成fulfilled之后改成调用,并且呢promise内部的value值是这个函数的参数,而且这个函数不能重复调用

  5. onRejected必须在promise状态改成rejected之后改成调用,并且呢promise内部的reason值是这个函数的参数,而且这个函数不能重复调用

  6. onFulfilledonRejected这两个方法必须要在当前执行栈的上下文执行完毕后再调用,其实就是事件循环中的微任务(setTimeout是宏任务,有一定的差异)

  7. onFulfilledonRejected这两个方法必须通过函数调用,也就是说 他们俩不是通过this.onFulfilled()或者this.onRejected()调用,直接onFulfilled()或者onRejected()
  8. then方法可以在一个promise上多次调用,也就是我们常见的链式调用

  9. 如果当前promise的状态改成了fulfilled那么就要按照顺序依次执行then方法中的onFulfilled回调

  10. 如果当前promise的状态改成了rejected那么就要按照顺序依次执行then方法中的onRejected回调

  11. then方法必须返回一个promise(接下来我们会把这个promise称做promise2),类似于 promise2 = promise1.then(onFulfilled, onRejected);
  12. 如果呢onFulfilled()或者onRejected()任一一个返回一个值x,那么就要去执行resolvePromise这个函数中去(这个函数是用来处理返回值x遇到的各种值,然后根据这些值去决定我们刚刚then方法中onFulfilled()或者onRejected()这两个回调返回的promise2的状态)

  13. 如果我们在then中执行onFulfilled()或者onRejected()方法时产生了异常,那么就将promise2用异常的原因ereject

  14. 如果onFulfilled或者onRejected不是函数,并且promise的状态已经改成了fulfilled或者rejected,那么就用同样的value或者reason去更新promise2的状态(其实这一条和第三条一个道理,也就是值得穿透问题)

好吧,我们总结了这么多规范特点,那么我们就用这些先来练练手

</>复制代码

  1. /**
  2. * 实现一个PromiseA+
  3. * @description 实现一个简要的promise
  4. * @param {Function} executor 执行器
  5. * @author Leslie
  6. */
  7. function Promise(executor){
  8. let self = this;
  9. self.status = "pending"; // 存储promise状态 pending fulfilled rejected.
  10. self.value = undefined; // 存储成功后的值
  11. self.reason = undefined; // 记录失败的原因
  12. self.onfulfilledCallbacks = []; // 异步时候收集成功回调
  13. self.onrejectedCallbacks = []; // 异步时候收集失败回调
  14. function resolve(value){
  15. if(self.status === "pending"){
  16. self.status = "fulfilled";// resolve的时候改变promise的状态
  17. self.value = value;//修改成功的值
  18. // 异步执行后 调用resolve 再把存储的then中的成功回调函数执行一遍
  19. self.onfulfilledCallbacks.forEach(element => {
  20. element()
  21. });
  22. }
  23. }
  24. function reject(reason){
  25. if(self.status === "pending"){
  26. self.status = "rejected";// reject的时候改变promise的状态
  27. self.reason = reason; // 修改失败的原因
  28. // 异步执行后 调用reject 再把存储的then中的失败回调函数执行一遍
  29. self.onrejectedCallbacks.forEach(element => {
  30. element()
  31. });
  32. }
  33. }
  34. // 如果执行器中抛出异常 那么就把promise的状态用这个异常reject掉
  35. try {
  36. //执行 执行器
  37. executor(resolve,reject);
  38. } catch (error) {
  39. reject(error)
  40. }
  41. }
  42. Promise.prototype.then = function(onfulfilled,onrejected){
  43. // onfulfilled then方法中的成功回调
  44. // onrejected then方法中的失败回调
  45. let self = this;
  46. // 如果onfulfilled不是函数 那么就用默认的函数替代 以便达到值穿透
  47. onfulfilled = typeof onfulfilled === "function"?onfulfilled:val=>val;
  48. // 如果onrejected不是函数 那么就用默认的函数替代 以便达到值穿透
  49. onrejected = typeof onrejected === "function"?onrejected: err=>{throw err}
  50. let promise2 = new Promise((resolve,reject)=>{
  51. if(self.status === "fulfilled"){
  52. // 加入setTimeout 模拟异步
  53. // 如果调用then的时候promise 的状态已经变成了fulfilled 那么就调用成功回调 并且传递参数为 成功的value
  54. setTimeout(function(){
  55. // 如果执行回调发生了异常 那么就用这个异常作为promise2的失败原因
  56. try {
  57. // x 是执行成功回调的结果
  58. let x = onfulfilled(self.value);
  59. // 调用resolvePromise函数 根据x的值 来决定promise2的状态
  60. resolvePromise(promise2,x,resolve,reject);
  61. } catch (error) {
  62. reject(error)
  63. }
  64. },0)
  65. }
  66. if(self.status === "rejected"){
  67. // 加入setTimeout 模拟异步
  68. // 如果调用then的时候promise 的状态已经变成了rejected 那么就调用失败回调 并且传递参数为 失败的reason
  69. setTimeout(function(){
  70. // 如果执行回调发生了异常 那么就用这个异常作为promise2的失败原因
  71. try {
  72. // x 是执行失败回调的结果
  73. let x = onrejected(self.reason);
  74. // 调用resolvePromise函数 根据x的值 来决定promise2的状态
  75. resolvePromise(promise2,x,resolve,reject);
  76. } catch (error) {
  77. reject(error)
  78. }
  79. },0)
  80. }
  81. if(self.status === "pending"){
  82. //如果调用then的时候promise的状态还是pending,说明promsie执行器内部的resolve或者reject是异步执行的,那么就需要先把then方法中的成功回调和失败回调存储袭来,等待promise的状态改成fulfilled或者rejected时候再按顺序执行相关回调
  83. self.onfulfilledCallbacks.push(()=>{
  84. //setTimeout模拟异步
  85. setTimeout(function(){
  86. // 如果执行回调发生了异常 那么就用这个异常作为promise2的失败原因
  87. try {
  88. // x 是执行成功回调的结果
  89. let x = onfulfilled(self.value)
  90. // 调用resolvePromise函数 根据x的值 来决定promise2的状态
  91. resolvePromise(promise2,x,resolve,reject);
  92. } catch (error) {
  93. reject(error)
  94. }
  95. },0)
  96. })
  97. self.onrejectedCallbacks.push(()=>{
  98. //setTimeout模拟异步
  99. setTimeout(function(){
  100. // 如果执行回调发生了异常 那么就用这个异常作为promise2的失败原因
  101. try {
  102. // x 是执行失败回调的结果
  103. let x = onrejected(self.reason)
  104. // 调用resolvePromise函数 根据x的值 来决定promise2的状态
  105. resolvePromise(promise2,x,resolve,reject);
  106. } catch (error) {
  107. reject(error)
  108. }
  109. },0)
  110. })
  111. }
  112. })
  113. return promise2;
  114. }

一气呵成,是不是觉得之前总结出的特点十分有效,对着特点十分顺畅的就撸完了代码~

那么就让我们接着来看看promiseA+文档里还有些什么内容吧

</>复制代码

  1. resolvePromise这个函数呢会决定promise2用什么样的状态,如果x是一个普通值,那么就直接采用x,如果x是一个promise那么就将这个promise的状态当成是promise2的状态

  2. 判断如果xpromise2是一个对象,即promise2 === x,那么就陷入了循环调用,这时候promise2就会以一个TypeErrorreason转化为rejected

  3. 如果x是一个promise,那么promise2就采用x的状态,用和x相同的valueresolve,或者用和x相同的reasonreject

  4. 如果x是一个对象或者是函数 那么就先执行let then = x.then

  5. 如果x不是一个对象或者函数 那么就resolve 这个x

  6. 如果在执行上面的语句中报错了,那么就用这个错误原因去reject promise2

  7. 如果then是一个函数,那么就执行then.call(x,resolveCallback,rejectCallback)

  8. 如果then不是一个函数,那么就resolve这个x

  9. 如果xfulfilled态 那么就会走resolveCallback这个函数,这时候就默认把成功的value作为参数y传递给resolveCallback,即y=>resolvePromise(promise2,y),继续调用resolvePromise这个函数 确保 返回值是一个普通值而不是promise

  10. 如果xrejected态 那么就把这个失败的原因reason作为promise2的失败原因reject出去

  11. 如果resolveCallback,rejectCallback这两个函数已经被调用了,或者多次被相同的参数调用,那么就确保只调第一次,剩下的都忽略掉

  12. 如果调用then抛出异常了,并且如果resolveCallback,rejectCallback这两个函数已经被调用了,那么就忽略这个异常,否则就用这个异常作为promise2reject原因

我们又又又又又又总结了这么多,好吧不说了总结多少就开撸吧。

</>复制代码

  1. /**
  2. * 用来处理then方法返回结果包装成promise 方便链式调用
  3. * @param {*} promise2 then方法执行产生的promise 方便链式调用
  4. * @param {*} x then方法执行完成功回调或者失败回调后的result
  5. * @param {*} resolve 返回的promiseresolve方法 用来更改promise最后的状态
  6. * @param {*} reject 返回的promisereject方法 用来更改promise最后的状态
  7. */
  8. function resolvePromise(promise2,x,resolve,reject){
  9. // 首先判断x和promise2是否是同一引用 如果是 那么就用一个类型错误作为Promise2的失败原因reject
  10. if( promise2 === x) return reject(new TypeError("typeError:大佬,你循环引用了!"));
  11. // called 用来记录promise2的状态改变,一旦发生改变了 就不允许 再改成其他状态
  12. let called;
  13. if( x !== null && ( typeof x === "object" || typeof x === "function")){
  14. // 如果x是一个对象或者函数 那么他就有可能是promise 需要注意 null typeof也是 object 所以需要排除掉
  15. //先获得x中的then 如果这一步发生异常了,那么就直接把异常原因reject掉
  16. try {
  17. let then = x.then;//防止别人瞎写报错
  18. if(typeof then === "function"){
  19. //如果then是个函数 那么就调用then 并且把成功回调和失败回调传进去,如果x是一个promise 并且最终状态时成功,那么就会执行成功的回调,如果失败就会执行失败的回调如果失败了,就把失败的原因reject出去,做为promise2的失败原因,如果成功了那么成功的value时y,这个y有可能仍然是promise,所以需要递归调用resolvePromise这个方法 直达返回值不是一个promise
  20. then.call(x,y => {
  21. if(called) return;
  22. called = true;
  23. resolvePromise(promise2,y,resolve,reject)
  24. }, error=>{
  25. if(called) return
  26. called = true;
  27. reject(error)
  28. })
  29. }else{
  30. resolve(x)
  31. }
  32. } catch (error) {
  33. if(called) return
  34. called = true;
  35. reject(error)
  36. }
  37. }else{
  38. // 如果是一个普通值 那么就直接把x作为promise2的成功value resolve掉
  39. resolve(x)
  40. }
  41. }

finnnnnnnnnally,我们终于通过我们的不懈努力实现了一个基于PromiseA+规范的Promise

最后呢为了完美,我们还要在这个promise上实现Promise.resolve,Promise.reject,以及catchPromise.allPromise.race这些方法。

Promise的一些方法

</>复制代码

  1. Promise.resolve = function(value){
  2. return new Promise((resolve,reject)=>{
  3. resolve(value)
  4. })
  5. }
  6. Promise.reject = function(reason){
  7. return new Promise((resolve,reject)=>{
  8. reject(reason)
  9. })
  10. }
  11. Promise.prototype.catch = function(onRejected){
  12. return this.then(null,onRejected)
  13. }
  14. Promise.all = function(promises){
  15. return new Promise((resolve,reject)=>{
  16. let arr = [];
  17. let i = 0;
  18. function getResult(index,value){
  19. arr[index] = value;
  20. if(++i == promises.length) {
  21. resolve(arr)
  22. }
  23. }
  24. for(let i = 0;i{
  25. getResult(i,data)
  26. },reject)
  27. }
  28. })
  29. }
  30. Promise.race = function(promises){
  31. return new Promise((resolve,reject)=>{
  32. for(let i = 0 ; i < promises.length ; i++){
  33. promises[i].then(resolve,reject)
  34. }
  35. })
  36. }
Promise 语法糖

恰完西瓜来口糖,语法糖是为了让我们书写promise的时候能够更加的快速,所以做了一层改变,我们来看一个例子,比如当我们封装一个异步读取图片的宽高函数

</>复制代码

  1. // 原来的方式
  2. let getImgWidthHeight = function(imgUrl){
  3. return new Promise((resolve,reject)=>{
  4. let img = new Image();
  5. img.onload = function(){
  6. resolve(img.width+"-"+img.height)
  7. }
  8. img.onerror = function(e){
  9. reject(e)
  10. }
  11. img.src = imgUrl;
  12. })
  13. }

是不是觉得怎么写起来有点舒服但又有点不舒服,好像我每次都要去写执行器啊!为什么!好的,没有为什么,既然不舒服 我们就改!

</>复制代码

  1. // 实现一个promise的语法糖
  2. Promise.defer = Promise.deferred = function (){
  3. let dfd = {};
  4. dfd.promise = new Promise((resolve,reject)=>{
  5. dfd.resolve = resolve;
  6. dfd.reject = reject;
  7. })
  8. return dfd
  9. }

有了上面的语法糖我们再看一下那个图片的函数怎么写

</>复制代码

  1. let newGetImgWidthHeight = function(imgUrl){
  2. let dfd = Promise.defer();
  3. let img = new Image();
  4. img.onload = function(){
  5. dfd.resolve(img.width+"-"+img.height)
  6. }
  7. img.onerror = function(e){
  8. dfd.reject(e)
  9. }
  10. img.url = imgUrl;
  11. return dfd.promise
  12. }

是不是发现我们少了一层函数嵌套,呼 得劲

最终检测

</>复制代码

  1. npm install promises-aplus-tests -g

既然我们都说了我们是遵循promiseA+规范的,那至少要拿出点证据来是不是,不然是不是说服不了大家,那么我们就用promises-aplus-tests这个包来检测我们写的promise究竟怎么样呢!安装完成之后来跑一下我们的promise

最终跑出来我们全部通过测试!酷!晚餐再加个鸡腿~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96324.html

相关文章

  • Promise快速入门

    摘要:周五就想写这篇文章,但是无奈花花世界的诱惑太多就一直拖到了今天,自责遍进入正题对象用于表示一个异步操作的最终状态完成或失败,以及其返回的值。 周五就想写这篇文章,但是无奈花花世界的诱惑太多……就一直拖到了今天,自责1e4遍;进入正题Promise: Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及其返回的值。 上为MDNPromise的定义;ES6规定Promis...

    bergwhite 评论0 收藏0
  • JS手写bind之处理new的情况详解

      你有遇见过给bind返回的函数做new操作的场景,本篇主要讲述的就是实现一下兼容new操作的bind写法,顺便学习一下new操作符,为大家提供下参考。  大家可以去看下关于 JS 中 bind 方法的实现的文章,并给出了实现:  Function.prototype.myBind=function(thisArg,...prefixArgs){   constfn=this;   return...

    3403771864 评论0 收藏0
  • 只会用就out手写一个符合规范的Promise

    摘要:传入的回调函数也不是一个函数类型,那怎么办规范中说忽略它就好了。因此需要判断一下回调函数的类型,如果明确是个函数再执行它。 Promise是什么 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处...

    muzhuyu 评论0 收藏0
  • 2019-我的前端面试题

    摘要:先说下我面试情况,我一共面试了家公司。篇在我面试的众多公司里,只有同城的面问到相关问题,其他公司压根没问。我自己回答的是自己开发组件面临的问题。完全不用担心对方到时候打电话核对的问题。 2019的5月9号,离发工资还有1天的时候,我的领导亲切把我叫到办公室跟我说:阿郭,我们公司要倒闭了,钱是没有的啦,为了不耽误你,你赶紧出去找工作吧。听到这话,我虎躯一震,这已经是第2个月没工资了。 公...

    iKcamp 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<