JS 探索异步Promise

Promise是异步编程的一种解决方案

学习promise前,我们先来了解下什么是异步

首先我们要知道js是单线程语言,也就是说,它并不能像JAVA语言那样,多个线程并发执行。

先讲下什么是同步。同步指的是代码一行一行依次执行,下面的代码必须等待上面代码的执行完成。当遇到一些耗时比较长的任务时,比如网络请求就容易发生阻塞,必须等数据请求过来后才能执行后面的操作。那么这里异步执行是在请求数据的同时还能执行后面的任务。

拿现实生活来举例,比如你又要煮饭又要炒菜。如果是同步执行,那么你必须先等饭煮完才能炒菜,或者是先炒完菜才能再去煮饭,这显然是非常耽搁时间的。如果是异步执行的话,你可以在遇到煮饭任务时将任务交给电饭煲,然后再去炒菜,炒完菜后再将饭取出来,这样的过程我们就可以称之为一个异步操作。

其中取饭的操作相当于执行了回调函数(异步执行的函数就是回调函数,因为这个函数最终会调入主线程执行),这里我已经简单解释了什么是同步执行与异步执行还有回调函数。

如果没有理解请再看一遍,或看这里异步执行的解析。

异步任务的影响

所以从上可以看出异步的执行是依赖于回调函数,那么在进行异步操作时回调函数会带来什么影响呢?那就是回调地狱。

回调地狱指的是:回调函数嵌套回调函数,形成的多层嵌套。

我们来看下如下例子,这里当你需要1s后打印3条hello,再过1秒后打印3条vue.js 再过1秒后打印3条node.js

首先你要知道setTimeout就是一个异步操作,其中传入的第一个参数就是回调函数,到了规定的时间就会回调执行。

setTimeout(() => {
  console.log("hello");
  console.log("hello");
  console.log("hello");
  setTimeout(() => {
    console.log("vue.js");
    console.log("vue.js");
    console.log("vue.js");
    setTimeout(() => {
      console.log("node.js");
      console.log("node.js");
      console.log("node.js");
    }, 1000);
  }, 1000);
}, 1000);

使用Promise解决回调地狱

如上代码就是产生了回调地狱,当代码过多会非常复杂。如下就是使用一种优雅的方式(promise)来解决如上的问题

这里我不打算特别详细的讲解Promise的用法,你可以根据下面的注释来分析代码。

//这里注意Promise()不用调用会自动执行,Promise括号中接收一个函数为参数,而函数中有两个参数resolve,reject
//当然resolve与reject参数名不是固定的,但是为了语义化我们通常这样写更加的通俗易懂
new Promise((resolve, reject) => {//resolve表示异步操作成功时的回调。reject表示失败时的回调(暂时不谈)。
  setTimeout(() => {
    resolve();    //因为resolve与reject是函数,所以加上括号调用
  }, 1000);
}).then(() => {
  //then表示然后,一旦执行了resolve()就会执行then()这个方法
  //之后的所有的回调代码就能then()函数中处理,这样会使得代码变得更加清晰
  console.log("hello");
  console.log("hello");
  console.log("hello");
  setTimeout(() => {
    console.log("vue.js");
    console.log("vue.js");
    console.log("vue.js");
    setTimeout(() => {
      console.log("node.js");
      console.log("node.js");
      console.log("node.js");
    }, 1000);
  }, 1000);
})

这样看起来then中还是有回调函数的嵌套,那么我们可以在then()方法中返回一个Promise实例,这样返回一个Promise就能继续的使用then()来解决嵌套,形成链式调用。那么要如何操作呢?如下:

new Promise((resolve, reject) => {
  //第一次回调执行的函数
  setTimeout(() => {
    resolve();
  }, 1000);
}).then(() => {
  //第一次回调函数处理的代码
  console.log("hello");
  console.log("hello");
  console.log("hello");
  return new Promise((resolve, reject) => {
    //第二次回调执行的函数
    setTimeout(() => {
      resolve();
    }, 1000);
  })
}).then(() => {
  //第二次回调函数处理的代码
  console.log("vue.js");
  console.log("vue.js");
  console.log("vue.js");
  setTimeout(() => {
    console.log("node.js");
    console.log("node.js");
    console.log("node.js");
  }, 1000);
})
//最后还有一层嵌套那么交给你自己来解绝。

虽然代码变得复杂了,但是逻辑清晰了。即使嵌套再多层,代码依然很清晰。只要是类似于如上的回调函数嵌套(比如网络请求)就必须使用Promise包裹。

再来举个例子,每隔1s将数值加1,且打印加1后的结果,并将结果传入下一个回调,再进行加1操作并打印,持续两次。

new Promise(resolve => {//因为我并不打算调用reject,这个参数可以省略
  setTimeout(() => {
    resolve(0);//resolve中传入的参数在then中接收
  }, 1000)
}).then(data => {
  data++;
  console.log(data);//1
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(data);
    }, 1000)
  })
}).then(data => {
  data++;
  console.log(data);//2
})

仔细观察setTimeout()中的内容,你会发现里面只有一行代码,加1还有输出的操作都是放在then()中执行,这就是Promise的操作思想,将回调函数中需要执行的所有代码放入then()中执行,再由resolve()调用。这里你可能还是会觉得麻烦,多此一举,但实际编码中回调函数的代码量是非常的多,使用这样的结构代码会更加清晰。

说了这么多我们来讲讲reject()这个函数。上面有注释有提到reject()是回调失败时的调用,那么当我们回调失败时,如果也把代码写入then()中再做判断,那么Promise()显得好像并不是很优雅,因为then既有成功的回调代码,也会有失败的回调代码。

当然Promise考虑到了这点,所以我们失败时的回调代码是写在catch()中的,catch就是“捕获”的意思,一旦你调用reject()就会来到catch()这个方法中。

举个简单的例子:

new Promise((resolve, reject) => {
  setTimeout(() => {
      let num = Math.floor(Math.random() * 11);//0-10的随机数
        if (num >= 5) {
          resolve(num);
        } else {
          reject(num);
        }
      },1000)
}).then(data => {
  console.log("执行了成功时的回调,数值为:"+data);
}).catch(reason => {
  console.log("执行了失败时的回调,数值为:"+reason);
})

异步获取一个随机数(0-10),1秒后执行。大于等于5我们认为是成功,所以调用resolve()修改Promimse的状态,否则是失败,调用reject()修改Promise的状态,reject()中的参数我们可以认为传入的是失败的原因,所以一般使用reason作为参数名。

那么现在你是不是又困惑了,什么是Promise的状态?

promise有三种状态:pending / fulfilled / rejected

  • pending:等待状态,当异步任务的回调函数还没有执行Promise就处于pending状态,比如网络请求,定时器时间
  • fulfilled:完成状态,当我们调用了resolve()方法,promise的状态就会从 pending转变为fulfilled,且会调用then()方法
  • rejected:拒绝状态,当我们调用了reject()方法,promise的状态就会从 pending转变为rejected,且会调用catch()方法

有基础的小伙伴又会疑惑了then()中不是也可以写失败的回调代码吗?没错,下面来看下Promise的另外一种写法

还是以上面代码为例子。

new Promise((resolve, reject) => {
  setTimeout(() => {
      let num = Math.floor(Math.random() * 11);//0-10的随机数
        if (num >= 5) {
          resolve(num);
        } else {
          reject(num);
        }
      },1000)
}).then(data => {
  console.log("执行了成功时的回调,数值为:"+data);
},reason => {
  console.log("执行了失败时的回调,数值为:"+reason);
})

其实then()中有两个参数,第一个参数的函数是用于处理成功时的回调代码,而第二个参数的函数就是处理失败时的回调代码。

Promise的链式调用与简写方法

看如下代码:

new Promise(resolve => {
  //这里1s后回调,执行then()中的代码
  setTimeout(() => {
    resolve('aaa');
  }, 1000)
}).then(data => {
  console.log(data);//aaa
  return new Promise(resolve => {
    resolve(data + 'bbb');
  })
}).then(data => {
  console.log(data);//aaabbb
  return new Promise(resolve => {
    resolve(data + 'ccc');
  })
}).then(data => {
  console.log(data);//aaabbbccc
})

你会发现上面代码在后面的then中并没有进行异步操作,所以我们没有必要使用Promise包裹没有异步操作的代码,可以简写成如下代码:

new Promise(resolve => {
  setTimeout(() => {
    resolve('aaa');
  }, 1000)
}).then(data => {
  console.log(data);
  return Promise.resolve(data + 'bbb');
}).then(data => {
  console.log(data);
  return Promise.resolve(data + 'ccc');
}).then(data => {
  console.log(data);
})

没想到吧除了上面还有更简单的方法:

new Promise(resolve => {
  setTimeout(() => {
    resolve('aaa');
  }, 1000)
}).then(data => {
  console.log(data);
  return data + 'bbb';
}).then(data => {
  console.log(data);
  return data + 'ccc';
}).then(data => {
  console.log(data);
})

直接返回结果,实际内部会对返回的结果进行封装。

对于如上的操作,我们肯定不会只有成功的回调,当然也会有失败时的回调。

new Promise(resolve => {
  setTimeout(() => {
    resolve('aaa');
  }, 1000)
}).then(data => {
  console.log(data);
  //想必也猜到了失败的简写
  return Promise.reject('error info');
  //当然我们如果抛出一个异常也会执行catch中的代码
  //throw 'error info';
}).then(data => {
  console.log(data);
  return Promise.resolve(data + 'ccc');
}).then(data => {
  console.log(data);
}).catch(reason => {
    //如果执行失败时的回调catch()那么中间的then()都不会执行
  console.log('执行了失败时的回调',reason);
})

Promise.all()方法

Promise.all()主要用于同时处理多个异步任务。当我们需要完成一个业务,就是当两个网络请求都成功时,就执行回调函数中的代码。这个时候就需要使用到Promise.all()。下面我就使用setTimeout()来模拟网络请求(这里你需要知道网络请求是一个异步操作),第一个网络请求(p1)的时间是1s,第二个请求(p2)的时间是2s。

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('结果1');
  }, 1000);
})
let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('结果2');
  }, 2000);
})
//Promise.all([])接收多个异步任务,放入数组中
Promise.all([p1, p2]).then(results => {//results接收多个参数,所以是数组
  console.log(results);//["结果1", "结果2"]
})

当你执行代码后你会发现两秒后才执行完成,也就是等待所有网络请求成功时,回调代码才会执行。

Promise.race()方法

Promise.all()是等待每个异步任务都完成就执行,以最慢的异步任务为准。而Promise.race()就是谁先完成就执行谁,以最快的任务为准。所以你可以看到then中我都是写的result而不是复数results。如下你可以看作是两个网络请求,你可以理解为同时请求了两个服务器,但是p1请求的服务器最先响应,所以我们就可以从响应快的服务器中拿数据。

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('结果1');
  }, 1000);
})
let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('结果2');
  }, 2000);
})
//Promise.race([])中接收多个异步任务,放入数组中
Promise.race([p1, p2]).then(result => {//result只会接收一个参数,谁先完成接收谁
  console.log(result);
})

总结

Promise就是对异步操作进行封装,其中的思想就是不希望你在回调函数中处理需要执行的代码,而是把回调执行的代码放入对应的区域也就是then()或catch()方法中处理,然后通过resolve()或reject()来调用。将代码分离后做的时链式的调用,你可以这样理解一个then或一个catch就是一个链条的连接点。一般有异步操作我们都要使用promise对异步操作进行封装,养成良好的习惯。

 

posted @ 2022-03-20 23:47  失恋的蔷薇  阅读(452)  评论(1编辑  收藏  举报