当前位置: 首页 > article >正文

前端 | 深入理解Promise

1. 引言

JavaScript 是一种单线程语言,这意味着它一次仅能执行一个任务。为了处理异步操作,JavaScript 提供了回调函数,但是随着项目处理并发任务的增加,回调地狱 (Callback Hell) 使异步代码很难维护。为此,ES6带来了Promise给了一种更清晰的异步操作模型。

2. 对Promise的理解

Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

(1)Promise的实例有三个状态:
  • Pending(进行中)
  • Resolved(已完成)
  • Rejected(已拒绝)
    当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。
(2)Promise的实例有两个过程:
  • pending -> fulfilled : Resolved(已完成)
  • pending -> rejected:Rejected(已拒绝)

注意:一旦从进行状态变成为其他状态就永远不能更改状态了。

2.1 Promise的特点

  • 对象的状态不受外界影响。
    • promise对象代表一个异步操作,有三种状态,​pending​(进行中)、fulfilled​(已成功)、rejected​(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——“承诺”;
  • 一旦状态改变就不会再变,任何时候都可以得到这个结果。
    • promise对象的状态改变,只有两种可能:从 pending变为 fulfilled​,从 pending变为 rejected。这时就称为 resolved​(已定型)。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。

2.2 Promise的缺点

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

3. Promise的基本用法

3.1 创建Promise对象

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve和 reject。

const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

使用resolve和reject方法

function testPromise(ready) {
  return new Promise(function(resolve,reject){
    if(ready) {
      resolve("hello world");
    }else {
      reject("No thanks");
    }
  });
};
// 方法调用
testPromise(true).then(function(msg){
  console.log(msg);
},function(error){
  console.log(error);
});

上面的代码的含义是给 testPromise方法传递一个参数,返回一个promise对象,如果为 true的话,那么调用promise对象中的 resolve()方法,并且把其中的参数传递给后面的 then第一个函数内,因此打印出 “hello world”, 如果为 false的话,会调用promise对象中的 reject()方法,则会进入 then的第二个函数内,会打印 No thanks;

3.2 Promise方法

(1)then()

第一个回调函数是Promise对象的状态变为 resolved时调用,第二个回调函数是Promise对象的状态变为 rejected时调用。其中第二个参数可以省略。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

当要写有顺序的异步事件时,需要串行写:

let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
        resolve(res);
    })
})
promise.then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
  
})

事件没有顺序或者关系时,还如何写呢?可以使用 all 方法来解决。

(2)catch()

该方法相当于 then方法的第二个参数,指向 reject的回调函数。

p.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
     }
); 
// 或:
p.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err);
});
(3)all()

all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个 promise对象。当数组中所有的 promise的状态都达到 resolved的时候,all方法的状态就会变成 resolved,如果有一个状态变成了 rejected,那么 all方法的状态就会变成 rejected。

let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(1);
    },2000)
});
let promise2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(2);
    },1000)
});
let promise3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(3);
    },3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);
    //结果为:[1,2,3] 
})
(4)race()

race方法和 all一样,接受的参数是一个每项都是 promise的数组,但是与 all不同的是,当最先执行完的事件执行完之后,就直接返回该 promise对象的值。如果第一个 promise对象状态变成 resolved,那自身的状态变成了 resolved;反之第一个 promise变成 rejected,那自身状态就会变成 rejected。

let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       reject(1);
    },2000)
});
let promise2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(2);
    },1000)
});
let promise3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(3);
    },3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
    console.log(res);
    //结果:2
},rej=>{
    console.log(rej)};
)

race方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
(5)finally()

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

4. 使用async/await代替Promise

4.1 对async/await的理解

async function testAsy(){
   return 'hello world';
}
let result = testAsy(); 
console.log(result)

在这里插入图片描述
async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:

async function testAsy(){
   return 'hello world'
}
let result = testAsy() 
console.log(result)
result.then(v=>{
    console.log(v)   // hello world
})

那如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)。

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

注意:Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

4.2 async/await的优势

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。仍然用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}
function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}
function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}
function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

如果用 async/await 来实现呢,会是这样:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}
doIt();

4.3 async/await对比Promise的优势

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
  • Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
  • 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

4.4 async/await如何捕获异常

使用 try…catch…

async function fn(){
    try{
        let a = await Promise.reject('error')
    }catch(error){
        console.log(error)
    }
}

5. 补充:Promise及其方法的手写

5.1 手写Promise

  • Promise 具有 pending(进行中)、fulfilled(已成功)和 rejected(已失败)三种状态,且状态不可逆。
  • new Promise(executor) 立即执行 executor 函数,并传入 resolve 和 reject,以控制 Promise 状态。
class MyPromise{
  constructor(executor){
    this.state = 'pending';
    this.value = undefined;  // 失败的值
    this.error = undefined;  // 成功的值
    this.onFulfilledCallbacks = [];  // 存储成功回调
    this.onRejectedCallbacks = [];  // 存储失败回调

    const resolve = (value)=>{
      if(this.state==='pending'){
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn=>fn(value));  // 执行所有成功回调
      }
    };
    const reject = (error)=>{
      if(this.state==='pending'){
        this.state = 'rejected';
        this.error = error;
        this.onRejectedCallbacks.forEach(fn=>fn(error));
      }
    };

    try{
      executor(resolve,reject);
    }catch(error){
      reject(error);
    }
  }
}

5.2. 手写Promise.then

  • 在 Promise 变为 fulfilled 时调用 onFulfilled,并传入结果。
  • 在 Promise 变为 rejected 时调用 onRejected,并传入错误。
  • then 必须返回一个新的 Promise,支持链式调用。
Promise.prototype.then = function(onFulfilled,onRejected){
  return new Promise((resolve,reject)=>{
    if(this.state==='fulfilled'){
      try {
        const result = onFulfilled?onFulfilled(this.value):this.value;
        resolve(result);
      } catch (error) {
        reject(error);
      }
    }else if(this.state==='rejected'){
      try {
        const result = onRejected?onRejected(this.error):this.error;
        reject(result);
      } catch (error) {
        reject(error);
      }
    }else{
      this.onFulfilledCallbacks.push((value)=>{
        try {
          const result = onFulfilled ? onFulfilled(value) : value;
          resolve(result);
        } catch (error) {
          reject(error);
        }
      })
      this.onRejectedCallbacks.push((error) => {
        try {
          const result = onRejected ? onRejected(error) : error;
          reject(result);
        } catch (error) {
          reject(error);
        }
      });
    }
  })
}

注意:

  • Promise.then是实例方法,实例方法是挂载在prototype上的只能通过实例去调用,不能直接挂载在Promise上
  • 而all和race是静态方法,可以直接挂载在Promise上

5.3 手写Promise.all

1)核心思路
  1. 接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数
  2. 这个方法返回一个新的 promise 对象,
  3. 遍历传入的参数,用Promise.resolve()将参数"包一层",使其变成一个promise对象
  4. 参数所有回调成功才是成功,返回值数组与参数顺序一致
  5. 参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。
2)代码实现

一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了

function promiseAll(promises) {
  return new Promise(function(resolve, reject) {
    if(!Array.isArray(promises)){
        throw new TypeError(`argument must be a array`)
    }
    var resolvedCounter = 0;
    var promiseNum = promises.length;
    var resolvedResult = [];
    for (let i = 0; i < promiseNum; i++) {
      Promise.resolve(promises[i]).then(value=>{
        resolvedCounter++;
        resolvedResult[i] = value;
        if (resolvedCounter == promiseNum) {
            return resolve(resolvedResult)
          }
      },error=>{
        return reject(error)
      })
    }
  })
}
// test
let p1 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(1)
    }, 1000)
})
let p2 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(2)
    }, 2000)
})
let p3 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(3)
    }, 3000)
})
promiseAll([p3, p1, p2]).then(res => {
    console.log(res) // [3, 1, 2]
})

5.3 手写Promise.race

该方法的参数是 Promise 实例数组, 然后其 then 注册的回调方法是数组中的某一个 Promise 的状态变为 fulfilled 的时候就执行. 因为 Promise 的状态只能改变一次, 那么我们只需要把 Promise.race 中产生的 Promise 对象的 resolve 方法, 注入到数组中的每一个 Promise 实例中的回调函数中即可。

Promise.race = function (args) {
  return new Promise((resolve, reject)=>{
    for(let i = 0; i < args.length; i++){
      Promise.resolve(args[i]).then(resolve,reject)
    }
  })
}

http://www.kler.cn/a/528585.html

相关文章:

  • 边缘检测算法(candy)
  • lstm代码解析1.2
  • 第5章 公共事件
  • (即插即用模块-特征处理部分) 十九、(NeurIPS 2023) Prompt Block 提示生成 / 交互模块
  • Vue 3 30天精进之旅:Day 10 - Vue Router
  • RRT_STAR路径规划代码
  • 蓝桥杯备赛经验帖
  • 图书管理系统 Axios 源码 __删除图书功能
  • Linux命令(144)之diff
  • [CVPR 2022]Cross-view Transformers for real-time Map-view Semantic Segmentation
  • Spring Boot项目如何使用MyBatis实现分页查询
  • 90,【6】攻防世界 WEB Web_php_unserialize
  • python-leetcode-完全二叉树的节点个数
  • webrtc协议详细解释
  • 完美还是完成?把握好度,辨证看待
  • 洛谷 P10289 [GESP样题 八级] 小杨的旅游 C++ 完整题解
  • 开发指南093-平台底层技术网站
  • DeepSeek本地部署详细指南
  • 跨域问题解决实践
  • 电路研究9.2.7——合宙Air780EP中嵌入式 TCPIP 相关命令使用方法研究
  • G. XOUR
  • pytorch实现文本摘要
  • [LeetCode]day9 203.移除链表元素
  • ASP.NET Core 中使用依赖注入 (DI) 容器获取并执行自定义服务
  • w179基于Java Web的流浪宠物管理系统的设计与实现
  • 使用pandas的read_excel()报错: