js高级- 异步任务的回调的处理方式

361 阅读14分钟

js 对异步任务的处理方式根据时间顺序主要分为如下几种:

  1. 回调函数处理方式
  2. Promise
  3. Promise + generator
  4. async/await

回调函数处理方式

以发起网络请求为例,在es6之前,当我们发送过网络请求后,如果需要对网络请求的结果进行后续处理,我们一般是通过传递回调函数的方式进行处理。(使用setTTimeout 模拟网络请求)

function requestData(url, successCallBack, failCallBack) {
  setTimeout(() => {
    if (url === "success") {
      successCallBack("success");
    } else {
      failCallBack("fail message");
    }
  }, 1000);
}
//成功的回调
function successCallBack(data) {
  console.log(data);
}
//失败的 回调
function failCallBack(error) {
  console.log(error);
}

requestData("success", successCallBack, failCallBack);
requestData("error", successCallBack, failCallBack);

回调函数存在的问题

这样做可以很好的完成我们的简单需求,但处理复杂的逻辑时,就会产生回调地狱的问题。比如,我们希望在一个请求完成拿到结果后,再发起另外一个网络请求,新的网请求拿到结果后再发起一次网络请求时,使用函数回调就需要复杂的处理逻辑,也不容易理解。这样就会形成回调地狱。请看如下代码

function requestData(url, successCallBack, failCallBack) {
  setTimeout(() => {
    if (url === "requestA" || url === "requestB" || url === "requestC") {
      successCallBack();
    } else {
      failCallBack();
    }
  }, 1000);
}
requestData(
  "requestA",
  () => {
    // A 请求成功
    requestData(
      "requestB",
      () => {
        // B 请求成功回调
        console.log("successA-successB");
        requestData(
          "requestC",
          () => {
            // C 请求成功回调
            console.log("successA-successB-successC");
          },
          () => {
            //C 请求失败回调
            console.log("error C message");
          }
        );
      },
      () => {
        // B 请求失败
        console.log("error B message");
      }
    );
    // A 请求成功
    console.log("successA");
  },
  (error) => {
    // A 请求 失败
    console.log("error A message");
  }
);

Promise 处理回调

Promise 出现的原因及历史流程

Promise 出现的原因

Promise 的出现是为了 解决回调地狱带给编程者的心智上的消耗,在编写类似回调地狱出现的编码场景时,程序员需要花费很大的精力去编写实现代码,即使完成了对应的需求,在代码的理解上和后续代码的维护上都会消耗程序员更大的精力和时间,所以为了节省这些编程中的痛点,才有了Promise 的出现。

Promise/A+ 规范

Promise 早期是没有什么规范的,大家都有自己的编写代码的方式,目标一致就是为了解决回调地狱,也都解决了问题。但这也引来了新的问题,没有统一的规范,其实也就没有彻底的解决问题。于是在社区上就逐渐的出现了Promise/A+ 规范,大家都按照这个规范去编写处理回调地狱问题的代码,这样理解起来就很容易,也更容易推广。

ECMASCript 的实现

在es6,ECMASCript 也注意到了回调地狱的问题,并依照Promise/A+ 规范 ,在js 中正式支持了Promise,我们常说的Promise 通常是指es6中 promise 的实现。需要注意的是es6的实现 并非完全依照Promise/A+ 规范,为了方便前端人员的使用,es6在Promise/A+ 规范的基础上,又新增了如 catch,finally 的对象方法等等一些语法。

Promise 的属性与方法

从使用角度来说,Promise 可以理解为全局对象中的一种,是ECMAScript 为了解决回调地狱问题而实现的一个类,前端人员可以使用这个类的类方法/类属性/对象方法/对象属性来更好的处理回调地狱问题。这个类是内置类,我们可以在全局范围内使用。既然Promise 就是一个类,我们就可以从这个类的方法,属性等方面去学习如何使用这个类,来完成我们的需求。下面是Promise 的类方法与属性,对象方法的截图,熟练掌握这些方法的使用就能更好的掌握Promise 的使用。

截屏2023-02-20 11.16.49.png

Promise 与期约相关的概念

期约是一个概念的产物,简单来说就是指定一个任务,并对这个任务的处理结果进行后续处理的过程进行控制。

期约概念的流程理解

具体来说就是假设我跟Promise 对象对于完成一个任务做了一个约定,我会具体制定这个事情要完成的任务(executor),Promise 对象去执行这个任务。

一旦这个任务有了结果,Promise 对象要告诉我事情完成的结果,我根据Promise 对象完成的情况,来判断Promise 对象是解决了(resolve)任务,还是拒绝了(reject)任务。

然后Promise 对象 根据我的判断,去处理我之后交代给Promise 对象的任务,是去执行解决后的任务(then),还是去执行解决失败的任务(catch)。

Promise 的理解

Promise 抽象了期约的概念,把要完成的任务,抽象成了executor(执行器函数),用暴露的Promise对象内部的reslove 和reject 方法,让我们具备 告知Promise对象 我们对任务完成结果的判断的能力,同时通过暴露then 方法,让我们能够注册 任务resolve时 的回调函数和 任务被reject时的回调函数,另外,还提供了catch 方法让我们能够单独注册 任务reject时 的回调函数,提供 finally 方法 让我们能够 注册 任务完成后 的善后事宜。

我们创建一个Promise 对象,就相当于是在制定一个期约的过程。例如:

function tryFly(animal) {
  if (animal === "鸟") {
    return true;
  } else {
    return false;
  }
}

const promise = new Promise((reslove, reject) => {
  let task = tryFly("鸟");
  if (task) {
    reslove();
  } else {
    reject();
  }
});

这里我和Promise 对象 约定了一个判断一个动物是否能飞的任务,Promise 对象调用函数去完成这个任务,我根据Promise 完成任务的情况,来决定调用relove回调函数,还是reject 回调函数。

如果这个任务解决了,Promise 对象 在收到 resolve 回调后,就会去执行 Promise 对象中,我给它注册的解决后的回调(then 方法)。

如果这人任务reject了,Promise 在收到 reject 回调后,就会去执行 Promise 对象中,我给它注册的解决后的回调(resolve方法)。

Promise 的构造函数

Promise 类的构造函数要求我们要传递一个函数(这个函数被称为executor处理函数),传递给构造函数的这个函数就是我们制定的期约要完成的任务。

构造函数的返回值是一个Promise 对象。

executor处理函数

executor处理函数 就是我们制定的期约要完成的任务。 executor处理函数有两个回调函数参数,一个是在任务解决的时候的回调(resolve),一个是在任务拒绝的时候的回调(rejet)

executor处理函数的返回值没有价值。如果我们需要把任务的处理结果传递给executor的两个回调函数,可以通过给resolve和reject 回调函数传递参数达到目的。

resolve 回调函数传递的参数类型

resolve 函数 是 Promise 对象提供给我们的在任务解决后的回调函数,我们可以通过这个函数,告知Promise 对象,我们对任务的完成结果很满意,Promise 对象可以进行后续的任务了。

在调用resolve 函数时,我们还可以传递给Promise 对象一个参数,使用这个参数,我们可以将任务的执行结果传递给Promise 对象,让Promise 对象保存起来,并在后续的任务中 再提供给我们。

resolve回调函数的参数可以传递的类型的使用方法如下:

resolve(参数)

  • 1> 普通的值或者对象 pending -> fulfilled
  • 2> 传入一个Promise 那么当前的Promise的状态会由传入的Promise来决定, 相当于状态进行了移交
  • 3> 传入一个对象, 并且这个对象有实现then方法(并且这个对象是实现了thenable接口)
    那么也会执行该then方法, 并且由该then方法决定后续状态

1.传入Promise的特殊情况

const newPromise = new Promise((resolve, reject) => {
  resolve("aaaaaa")
//   reject("err message")
})


new Promise((resolve, reject) => {
  // pending -> fulfilled
  resolve(newPromise)
}).then(res => {
  console.log("res:", res)
}, err => {
  console.log("err:", err)
})

2.传入一个对象, 这个兑现有then方法

new Promise((resolve, reject) => {
  // pending -> fulfilled
  const obj = {
    then: function(resolve, reject) {
      // resolve("resolve message")
      reject("reject message")
    }
  }
  resolve(obj)
}).then(res => {
  console.log("res:", res)
}, err => {
  console.log("err:", err)
})

Promise 对象方法一:then方法

then 方法用于注册 在executor 函数中调用了 resolve 时的回调函数。

then 方法的返回值 & then 方法的回调函数的返回值

then 方法的返回值 也是一个Promise,这是在我们调用Promise 的then 方法后,在then 方法中重新创建的新的Promise 对象,已经不再是我们最初创建的那个Promise了。

then 方法的回调函数的返回值 作为我们传递给then 方法的回调函数,由于本身是一个函数,所以它也有返回值,他的返回值类型可以普通值,Promise,或者thenable 对象。then 方法的回调函数的返回值 将会被then 方法创建或者返回的Promise 作为Promise的resolve函数的参数。这样 我们就能在下一次 then 方法的回调函数中中拿到上一次then方法的回调函数的返回值。

在使用then 方法时,我们要注意以下几点:

1.then 方法的几种使用方式

//一个参数
promise.then(
  (res) => {
    // 调用reslove 会执行这里的任务
    console.log("起飞");
  }
);
//2个参数
promise.then(
  (res) => {
    // 调用reslove 会执行这里的任务
    console.log("起飞");
  },
  (error) => {
    // 调用reject 会执行这里的任务
    console.log("改造");
  }
);
//2个参数,只传递任务失败时的回调函数
promise.then(
 undefined,
  (error) => {
    // 调用reject 会执行这里的任务
    console.log("改造");
  }
);

这里 需要注意的是then 方法可以接受一个参数,代表任务解决后的回调函数,也可以传递两个参数,第一个方法代表任务解决后的回调函数,第二个参数代表任务失败后的回调函数。这种传递两个参数的写法 是Promise/A+ 的写法。

如果我们只给then 方法传递了一个参数,后续还想添加任务失败的回调函数,可以通过catch 方法再注册一个函数,用来代表任务失败的回调函数。Promise/A+ 并没有规定catch 方法,这个是es6的特定实现。

promise.catch((error) => {
  // 调用reject 会执行这里的任务
  console.log("改造");
});

2.同一个Promise可以被多次调用then方法。 当我们的resolve方法被回调时, 所有的then方法传入的回调函数都会被调用

promise.then(res => {
  console.log("res1:", res)
})

promise.then(res => {
  console.log("res2:", res)
})

promise.then(res => {
  console.log("res3:", res)
})

3.then方法传入的 "回调函数": 可以有返回值。

then方法本身也是有返回值的, 它的返回值是Promise

需要注意的是 then 方法返回的Promise 和 我们最初创建的Promise 对象 已经不是一个对象了。

then方法的 回调函数返回值 的几种情况的demo

const promise = new Promise((resolve, reject) => {
  console.log("promise");
  resolve();
})
// 1> 如果我们返回的是一个普通值(数值/字符串/普通对象/undefined), 那么这个普通的值被作为一个新的Promise的resolve值
promise.then(res => {
  return "aaaaaa"
}).then(res => {
  console.log("res:", res)
  return "bbbbbb"
})
// 2> 如果我们返回的是一个Promise,将把Promise 中的rsolve 的传递的参数作为返回值,传递给then 函数
promise.then(res => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(111111)
    }, 3000)
  })
}).then(res => {
  console.log("res:", res)
})
// 3> 如果返回的是一个对象, 并且该对象实现了thenable,会直接调用thenable 中的then 方法,并把then 方法中的resolve 方法传递的参数作为返回值,传递给后续then
promise
  .then((res) => {
    return {
      then: function (resolve, reject) {
        resolve(222222);
      },
    };
  })
  .then((res) => {
    console.log("res:", res);
  });

4.then 方法的链式调用

由于then 方法的返回值也是 Promise ,所以我们可以通过这个特性,进行then 方法的链式调用

new Promise((resolve, reject) => {
  console.log("promise");
  resolve();
})
  .then(
    () => {
      console.log("then 处理函数1");
    },
    (error) => {
      console.log(error);
    }
  )
  .then((res) => {
    console.log(res);
    console.log("then 处理函数2");
  })
  .then((res) => {
    console.log("then 处理函数3");
  });
 这里由于每次都没有给then 返回值,所以then 的函数的返回值都是undfined
  
// promise
// then 处理函数1
// undefined
// then 处理函数2
// then 处理函数3

Promise 对象方法二:catch 方法

catch 对象方法用于注册 在executor 函数中调用了 reject 时的回调函数

在使用catch 方法时,需要注意以下几点:

1.当executor抛出异常时, 也是会调用错误(拒绝)捕获的回调函数的

const promise = new Promise((resolve, reject) => {
  //reject("rejected status");
   throw new Error("rejected status");
});

promise.then(undefined, (err) => {
  console.log("err:", err);
  console.log("----------");
});

2.通过catch方法来传入错误(拒绝)捕获的回调函数的使用方法

const promise = new Promise((resolve, reject) => {
 reject("rejected status");
 // throw new Error("rejected status");
});

promise
 .then((res) => {
   // return new Promise((resolve, reject) => {
   //   reject("then rejected status")
   // })
   throw new Error("error message");
 })
 .catch((err) => {
   console.log("err:", err);
 });

3.catch 方法的返回值, 也是一个Promise。 返回值情况同then 方法。

const promise = new Promise((resolve, reject) => {
 reject("111111");
});

promise
 .then((res) => {
   console.log("res:", res);
 })
 .catch((err) => { // executor 中 reject ,会调用这里的任务
   console.log("err:", err);
   return "catch return value";
 })
 .then((res) => { // catch 的返回值也是Promise,如果
   console.log("res result:", res);
 })
 .catch((err) => {
   console.log("err result:", err);
 });
 
// 运行结果
// err: 111111
//res result: catch return value

Promise 对象方法三:finally 方法

es6 为我们提供了 Promise 的finally 对象方法,无论executor 的函数的处理结果是resolve 还是reject 都会执行finally 函数中注册的任务。

const promise = new Promise((resolve, reject) => {
  // resolve("resolve message")
  reject("reject message")
})

promise.then(res => {
  console.log("res:", res)
}).catch(err => {
  console.log("err:", err)
}).finally(() => {
  console.log("finally code execute")
})

finally 的返回值 也是一个Promise.

Promise 类方法一:Promise.resolve()

Promise.resolve 用于实例化一个已解决的期约。

resolve 静态方法的接收一个参数,可以是普通值,也可以是期约,也可以是一个符合thenable 接口的 对象。接收不同的参数对返回值没有任何影响,都是返回一个已解决的期约。

 const promise = Promise.resolve({ name: "why" })
// 相当于
 const promise2 = new Promise((resolve, reject) => {
   resolve({ name: "why" })
 })

对于这个期约静态方法而言,如果传入的参数是一个期约,那它的行为就类似于一个空包装。因此,Promise.resolve()可以说是一个幂等方法。

let p = Promise.resolve(7);
setTimeout(console.log,0,p === Promise.resolve(p));
//true
setTimeout(console.log,0,p === Promise.resolve(Promise.resolve(p)));
//true

Promise 类方法二:Promise.reject()

与Promise.resolve() 类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误,这个错误不能通过try/catch 捕获,而只能通过拒绝处理程序捕获。

//能够捕获
try{
throw new Error('foo');
}catch(e){
console.log(e);
}
//捕获不到
try {
Promise.reject(new Error('bar'));
}catch(e){
console.log(e);
}

Promise.reject()没有照搬Promise.resolve()的幂等逻辑,如果给他传递一个期约对象,则这个期约将会成为他返回的拒绝期约的理由。

setTimeout(console.log,0,Promise.reject(Promise.resolve()));
// Promise <rejected>:Promise <resolved>

Promise 类方法三:Promise.all()

1.Promise.all() 静态方法创建的期约会在一组期约全部解决后再解决。这个静态方法接收一个可迭代对象,返回一个新期约。

2.可迭代对象中的元素会通过Promise.resolve()转换为期约

如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的期约也会拒绝。

如果所有的期约都成功解决,则合成期约的解决值就是所有包含期约的解决值的数组,按照迭代器顺序。

**如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由。之后再拒绝的期约不会影响最终期约的拒绝理由。但是合成期约仍会继续处理所有拒绝期约,只是不会返回拒绝理由而已。 **

Promise.all([p2, p1, p3, "aaaa"])
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log("err:", err);
  });

const promise = Promise.all([
  Promise.reject(3),
  new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(5);
    }, 1000);
  }),
]);
promise.catch((reason) => {
  console.log(reason);
});

Promise 类方法四:Promise.race()

Promise.race()静态方法会返回一个包装期约,是一组期约组合中最新解决或拒绝期约的镜像,这个方法可以接受一个可迭代对象,返回一个新期约。

Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约。

如果有一个期约拒绝,只要它是第一个落定的,就会成为拒绝合成期约的理由。之后再拒绝的期约不会影响最终期约的拒绝理由。不过,这并不影响所有包含期约正常的拒绝操作。

只要有一个解决或拒绝,就结束。

Promise 类方法五:Promise.any()

Promise.any()静态方法会返回一个包装期约,这个方法可以接受一个可迭代对象,返回一个新期约。 只要有一个期约解决了,就解决。

Promise 类方法五:Promise.allSettled()

1.Promise.allSettled() 静态方法创建的期约会在一组期约全部落定后再解决。这个静态方法接收一个可迭代对象,返回一个新期约。

无论期约组合中的期约是否是拒绝,只要能够落定,都会返回新期约。这是与Promise.all()最大的区别。

编写不易,欢迎点赞👍👍👍

水平有限,欢迎评论指出,共同进步🤌🤌🤌