彻底搞懂 javascript的Promise

目录
  • 一、为什么要引入Promise?
    • Promise解决了什么问题?
    • Promise有哪些具体的使用场景?
  • 二、手写Prromise身上的方法
    • 手写Promise.all
    • 手写Promise.race
    • 手写Promise.finally
    • Promise.all和Promise.race的区别
    • Promise.all和Promise.race的应用场景
      • promise.all()的应用场景
      • Promise.race()的应用场景
  • 三、Promise是如何解决串行和并行的?
    • 什么是并行?什么是串行?
    • Promise实现并行请求
    • Promise实现串行请求
  • 四、什么是Promise穿透?
  • 五、使用Promise封装Ajax请求
  • 六、Promise有哪些状态?
    • Promise状态的变化过程
  • 七、将callback改写成Promise
  • 总结

一、为什么要引入Promise?

在介绍本章之前,首先先抛出几个问题:

  • Promise解决了什么问题?
  • Promise有哪些具体的使用场景?

Promise解决了什么问题?

1.回调地狱问题

在没有Promise之前,前端获取数据往往需要通过回调函数层层嵌套的方式来解决异步问题,例如下面这段代码实例:

// 回调地狱实例
// 奶茶函数
function getTea(fn) {
  setTimeout(() => {
    fn('获取到一杯奶茶')
  },2000)
}
// 面包函数
function getBread(fn) {
  setTimeout(() => {
    fn('获取到一个面包')
  },100)
}
// 如果必须按照顺序获取,而不是根据时间,要求是先获取到奶茶后获取到面包。
getTea(function(data) {
  console.log(data);
  getBread(function(data) {
    console.log(data);
  })
})

2.可读性问题

通过Promise我们可以将上面的代码重写为下面的方式,明显这样可读性更高。

// 下面解释下,如何通过Promise来解决回调地狱的问题
function getTea() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('获取到一杯奶茶')
    }, 2000)
  })
}
function getBread() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('获取到一个面包')
    }, 500)
  })
}
getTea()
  .then(res => {
    console.log(res);
    return getBread();
  })
  .then(res => {
    console.log(res);
  })

3.信任问题(也叫回调多次执行问题)

传统的回调函数无法保证只被执行一次,回调函数还要可能被执行其他操作,而Promise调用且仅调用一次resolve,不会产生回调多次执行的问题,所以Promise很好的解决了第三方库多次调用回调的问题。

Promise有哪些具体的使用场景?

  • 场景1:将图片的加载写成一个Promise,图片一旦加载完成,Promise的状态就会发生变化。
  • 场景2:当下一个异步请求需要依赖上一个请求结果的时候,可以通过链式操作解决问题。
  • 场景3:通过all()实现多个请求合并在一起,汇总所有的请求结果,只需设置一个loading即可。
  • 场景4:通过race()可以设置图片请求超时。

二、手写Prromise身上的方法

手写Promise.all

Promise.all的特点是接收的是一个可迭代对象,当这个可迭代对象中的所有元素都执行成功会返回一个数组,一个出错则立即返回错误。

function myPromiseAll(iterable) {
  // 首先明确要返回的对象是一个Promise
  return new Promise((resolve,reject) => {
    // 首先将可迭代对象转换为数组
    const promises = Array.from(iterable);
    let flag = 0;
    const result = [];
    // 开始遍历执行
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i]).then(res => {
        result[i] = res;
        flag++;
        if (flag === promises.length) {
          resolve(result)
        }
      }).catch(err => {
        reject(err)
      })
    }
  })
}

手写Promise.race

Promise.race函数接收的是一个可迭代对象,相当于让这个可迭代对象中的所有promise对象进行赛跑,只要有一个promise对象发生了状态变化,那么直接返回这个promise对象返回的结果。

// 手写promise.race
function myPromiseRace(iterator) {
  // 首先返回的是一个promise对象
  return new Promise((resolve,reject) => {
    for (let item of iterator) {
      Promise.resolve(item).then(res => {
        resolve(item);
      }).catch(err => {
        reject(err);
      })
    }
  })
}
let p1 = new Promise(resolve => {
  setTimeout(resolve, 105, 'p1 done')
})
let p2 = new Promise(resolve => {
  setTimeout(resolve, 100, 'p2 done')
})
myPromiseRace([p1, p2]).then(data => {
  console.log(data); // p2 done
})

手写Promise.finally

Promise.finally的特点

  • 无论成功还是失败,都会执行这个方法
  • 返回的是一个Promise

Promise.finally执行的例子

let p = new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve(111);
  },2000)
})
p.then(res => {
  console.log(res);  // 111
}).finally(() => {
  console.log('无论如何这里都会被执行');  // 无论如何这里都会被执行
})

手写Promise.finally(Promise.finally返回的本质上是一个then方法,需要在then方法中执行我们传入的参数,然后返回形参)

Promise.prototype.finally = function(f) {
  return this.then((value) => {
    return Promise.resolve(f()).then(() => value)
  },(err) => {
    return Promise.resolve(f()).then(() => {
      throw err;
    })
  })
}

Promise.all和Promise.race的区别

Promise.all()成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候返回的是最先被reject的值。当Promise.all()的结果是成功的时候,返回结果的数组里边的数据顺序和Promise.all()接收到的promise顺序是一致的。

promise.race表示多个Promise赛跑的意思,里面哪个结果执行的快就返回哪个结果,不管结果本身是成功还是失败,其他Promise代码还会执行,只是不会返回。

Promise.all和Promise.race的应用场景

promise.all()的应用场景

多个异步任务都得到结果时,进行显示的场景

比如,当用户点击按钮时,会弹出一个对话框,这个对话框中的数据来自两个不同的后端接口获取的数据,当用户刚点击的时候,显示的时数据加载中的状态,当这两部分数据都从接口获取到数据的时候,才让数据加载中的状态消失,此时就可以使用Promise.all方法。

Promise.race()的应用场景

提示用户请求超时

比如,当用户点击按钮发送请求的时候,当后端的接口超过我们设定的时间还没有获取到数据的时候,我们就可以提示用户请求超时。

三、Promise是如何解决串行和并行的?

什么是并行?什么是串行?

并行:指的是多个异步请求同时进行。

串行:一个异步请求完成之后再进行下一个请求。

Promise实现并行请求

Promise实现并行请求主要是依靠Promise.all方法和Promise.race方法,我们可以通过手写Promise.all方法或Promise.race方法来实现这一目标。

Promise实现串行请求

Promise实现串行请求主要是借助reduce函数。可以参考我的这篇文章如何控制Promise的串行执行?

// 借助reduce函数来实现Promise的串行执行
const funcArr = [
  () => {
    return new Promise((resolve) => {
      setTimeout(() => {resolve(1)},2000)
    })
  },
  () => {
    return new Promise((resolve) => {
      setTimeout(() => {resolve(2)},1000)
    })
  },
  () => {
    return new Promise((resolve) => {
      setTimeout(() => {resolve(3)},3000)
    })
  },
];
function inOrder(arr) {
  const res = [];
  return new Promise((resolve) => {
    arr.reduce((pre,cur) => {
      return pre.then(cur).then(data => res.push(data))
    },Promise.resolve()).then(data => resolve(res))
  })
}
inOrder(funcArr).then(data => console.log(data))   // [1,2,3]

四、什么是Promise穿透?

所谓的Promise的值穿透指的是.then或者.catch的参数希望是函数,如果传入的不是函数,则可能会发生值穿透。Promise方法通过return传值,没有return就只是相互独立的任务而已。看看下面这个例子的输出可能会更好的帮助我们理解什么是值穿透?

Promise.resolve(1)
  .then(function(){return 2})
  .then(Promise.resolve(3))
  .then(console.log)   // 2

之所以发生了值穿透就是因为第二个then中传入的不是一个函数的形式。

五、使用Promise封装Ajax请求

使用Promise封装Ajax请求的关键步骤,全部在下面的代码中的注释里,详情请看下面的代码。

// 使用Promise封装Ajax请求
const res = new Promise((resolve,reject) => {
  // 1. 创建一个XMLHttpRequest对象
  const xhr = new XMLHttpRequest();
  // 2. 初始化请求方法和URL
  xhr.open('GET','https://api.apiopen.top/getJoke');
  // 3. 发送请求
  xhr.send();
  // 4. 绑定事件,处理响应结果
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      // 这里4代表的就是说服务端返回了全部的结果
      // 如果服务端返回的状态码是2开头的,我们就resolve这个返回的结果,反之则reject对应的状态码
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response)
      } else {
        reject(xhr.status)
      }
    }
  }
})
res.then(function(value) {
  console.log(value);
},function(err) {
  console.log(err);
})

六、Promise有哪些状态?

Promise主要有以下三种状态:

  • pending状态(初始状态)
  • fulfilled状态(已经成功的状态)
  • rejected状态(已经失败的状态)

Promise状态的变化过程

1.从pending到fulfilled状态的切换

resolve前是pending状态,resolve之后是fulfilled状态

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('resolve前的状态:', p);
    resolve();
    console.log('resolve之后的状态', p);
  })
})

2.从pending状态到rejected状态

reject前是pending状态,reject之后是rejected状态。

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('reject前的状态:', p);
    reject();
    console.log('reject之后的状态', p);
  })
})

七、将callback改写成Promise

1.传统callback的形式

const fs = require('fs');
fs.readFile('./temp.md',(err,data) => {
  console.log(data.toString());
})

2.将callback改为promise的形式

核心就是通过resolve来获取callback的数据。

const fs = require('fs');
async function myReadFile() {
  let result = await new Promise((resolve,reject) => {
    fs.readFile('./temp.md',(err,data) => {
      resolve(data.toString());
    })
  })
  console.log(result);   // xxxxx
  return result;
}
myReadFile()

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • 深入了解JavaScript Promise

    目录 一 什么是 Promise? 二 为什么有 Promise? 三 Promise常用api 四 Promise常用的两个用法 总结 一 什么是 Promise? 一个 Promise 对象就像容器一样,在容器中写着一段执行具体操作的代码,并且在这段代码执行结束后,会执行两个回调函数,一个是操作成功的回调函数(resolve),一个是操作失败的回调函数(reject) 二 为什么有 Promise? Promise 的出现是为了解决异步编程中,主要使用的回调机制的几个问题: Callback

  • JS如何为promise增加abort功能

    目录 概述 promise race方法 重新包装promise AbortController  Axios插件自带取消功能 概述 Promise只有三种状态:pending.resolve.reject,一个异步的承诺一旦发出,经历等待(pending)后,最终只能为成功或者失败,中途无法取消(abort). 为promise提供abort功能的思路有两种: 手动实现abort,触发取消后,异步回来的数据直接丢弃(手动实现,比较稳妥) 使用原生方法AbortController中断请求(实验

  • JS 9个Promise面试题

    1. 多个.catch var p = new Promise((resolve, reject) => { reject(Error('The Fails!')) }) p.catch(error => console.log(error.message)) p.catch(error => console.log(error.message)) 以上代码的输出将会是什么?请选择正确的答案: [ ] 打印一次消息 [x] 打印两次消息 [ ]UnhandledPromiseReject

  • JavaScript中的Promise详解

    目录 Promise的基本用法: 1.创建Promise对象 2.Promise 方法 总结 Promise是异步编程的一种解决方案,是一个对象,可以获取异步操作的消息,大大改善了异步编程的困难,避免了回调地狱,比传统的解决方案回调函数和事件更合理和更强大. 从语法上讲,Promise是一个对象,它可以获取异步操作的消息.提供了一个统一的API,各种异步操作都可以用同样的方法进行处理 1.Promise的实例有三个状态: (1)Pending(进行中) (2)Resolved(已完成) (3)R

  • 前端JavaScript之Promise

    目录 1.什么是Promise 2.基本用法 3.Promise的方法 3.1 Promise.prototype.then() 3.2 Promise.prototype.catch() 3.3 Promise.prototype.finally() 3.4 Promise.all() 3.5 Promise.race() 3.6 Promise.allSettled() 3.7 Promise.any() 3.8 Promise.resolve() 3.9 Promise.reject()

  • 彻底搞懂 javascript的Promise

    目录 一.为什么要引入Promise? Promise解决了什么问题? Promise有哪些具体的使用场景? 二.手写Prromise身上的方法 手写Promise.all 手写Promise.race 手写Promise.finally Promise.all和Promise.race的区别 Promise.all和Promise.race的应用场景 promise.all()的应用场景 Promise.race()的应用场景 三.Promise是如何解决串行和并行的? 什么是并行?什么是串行

  • 彻底搞懂JavaScript中的apply和call方法(必看)

    call和apply都是为了改变某个函数运行的context上下文而存在的,即为了改变函数体内部this的指向.因为JavaScript的函数存在定义上下文和运行时上下文以及上下文是可以改变的概念. 回到目录定义 fun.apply(thisArg, [argsArray]) fun.call(thisArg, arg1,arg2, ...) 其中thisArg可以为null或undefined,此时表示全局对象,更详细见MDN:apply().call() 二者的作用完全一样,只是接受参数的方

  • 一篇文章搞懂JavaScript正则表达式之方法

    咱们来看看JavaScript中都有哪些操作正则的方法. RegExp RegExp 是正则表达式的构造函数. 使用构造函数创建正则表达式有多种写法: new RegExp('abc'); // /abc/ new RegExp('abc', 'gi'); // /abc/gi new RegExp(/abc/gi); // /abc/gi new RegExp(/abc/m, 'gi'); // /abc/gi 它接受两个参数:第一个参数是匹配模式,可以是字符串也可以是正则表达式:第二个参数是

  • 一篇文章让你搞懂JavaScript 原型和原型链

    本文由葡萄城技术团队原创并首发 转载请注明出处:葡萄城官网 与多数面向对象的开发语言有所不同,虽然JavaScript没有引入类似类的概念(ES6已经引入了class语法糖),但它仍然能够大量的使用对象,那么如何将所有对象联系起来就成了问题.于是就有了本文中我们要讲到的原型和原型链的概念. 原型和原型链作为深入学习JavaScript最重要的概念之一,如果掌握它了后,弄清楚例如:JavaScript的继承,new关键字的原来.封装及优化等概念将变得不在话下,那么下面我们开始关于原型和原型链的介绍

  • 一文搞懂JavaScript如何实现图片懒加载

    目录 实现思路 准备知识 data-* getBoundingClientRect() throttle window.innerHeight 完整代码 js部分 CSS部分 运行结果 总结 图片懒加载,往往作为减少首页白屏时间的一个解决方案而出现.直观的来说,就是不要直接加载所有图片,而是满足一定条件后才加载,也就是”惰性加载“.实现图片懒加载的方式有很多,如果要简单点那就直接使用第三方插件:vue-lazyload,如果想探究一下别人的插件是怎么实现图片懒加载的,那么可以看看本文是如何实现的

  • 一文搞懂JavaScript中bind,apply,call的实现

    目录 bind.call和apply的用法 bind call&apply 实现bind 实现call和apply 总结 bind.call和apply都是Function原型链上面的方法,因此不管是使用function声明的函数,还是箭头函数都可以直接调用.这三个函数在使用时都可以改变this指向,本文就带你看看如何实现bind.call和apply. bind.call和apply的用法 bind bind()方法可以被函数对象调用,并返回一个新创建的函数. 语法: function.bin

  • 一文搞懂JavaScript中的内存泄露

    目录 什么是内存泄漏 怎么检测内存泄漏 Performance Memory 内存泄漏的场景 垃圾回收算法 引用计数 循环引用 标记清除 闭包是内存泄漏吗 总结 以前我们说的内存泄漏,通常发生在后端,但是不代表前端就不会有内存泄漏.特别是当前端项目变得越来越复杂后,前端也逐渐称为内存泄漏的高发区.本文就带你认识一下Javascript的内存泄漏. 什么是内存泄漏 什么是内存?内存其实就是程序在运行时,系统为其分配的一块存储空间.每一块内存都有对应的生命周期: 内存分配:在声明变量.函数时,系统分

  • 一文搞懂JavaScript中原型与原型链

    目录 1.构造函数原型prototype 2.对象原型__proto__ 3.constructor构造函数 4.原型链 5.原型对象中的this指向 6.扩展内置对象(原型对象的应用) 在ES6之前,我们面向对象是通过构造函数实现的.我们把对象的公共属性和方法放在构造函数里 像这样: function student(uname,age) { this.uname = uname; this.age = age; this.school = function() { console.log('

  • 一文带你搞懂JavaScript中的进制与进制转换

    目录 进制介绍 进制转换 parseInt(str, radix) Number() +(一元运算符) Number.prototype.toString(radix) 自定义转换 十进制与十六进制转换 十进制和二进制转换 进制介绍 JavaScript 中提供的进制表示方法有四种:十进制.二进制.十六进制.八进制. 对于数值字面量,主要使用不同的前缀来区分: 十进制(Decimal):取值数字 0-9:不用前缀. 二进制(Binary):取值数字 0 和 1 :前缀 0b 或 0B. 十六进制

  • 一篇文章带你搞懂JavaScript的变量与数据类型

    目录 前言: 温馨提示: 变量 1.声明 2.赋值 3.二个语法小细节 变量的命名规范 为什么需要数据类型? 简单数据类型(基本数据类型) 数字型 字符串型 String 什么是数据类型的转换 1.转换为字符串 2.转换为数字型(重点) 转化为布尔型 总结 前言: 我不是搞前端,而是搞后端的.本命编程语言是java.学习js的嘛,因为看到室友能做出动态网页,而我只能做出静态网页,再加上下个学期要学所以提前来学习学习. 温馨提示: java和javsScript没有半毛钱关系,只是javaScri

随机推荐