异步编程总结 | 青训营

84 阅读6分钟

前言🐱

异步编程 的语法目标,就是怎样让它更像同步编程

我们知道Javascript语言的执行环境是"单线程"。也就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。

这种模式虽然实现起来比较简单,执行环境相对单纯,但是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

常见异步形式🐟

一,回调函数🦈

1.定时器回调🐬

"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,回调函数是异步操作最基本的方法。以下的定时器就是一个异步函数简单例子:

setTimeout(
function(){
// 处理逻辑
},2000)//在2s后执行处理逻辑里的代码
//假设有四个人,分别叫达姆,灿灿,佳🐟姐,还有小龙一起抢三张凳子
//先让我们用同步的方式展现  console . log ( "灿灿发现凳子" ); console . log ( "灿灿抢到凳子" ); console . log ( "佳🐟姐发现凳子" ); console . log ( "佳🐟姐抢到凳子" ); console . log ( "小龙发现凳子" ); setTimeout ( function (){  console . log ( "小龙抢到凳子" ); }, 20 ); //定时器在20ms后执行回调函数,也就是小龙会在20ms后抢到凳子  console . log ( "达姆发现凳子" ); console . log ( "达姆抢到凳子" );  可以看出以上就是一种同步队列中夹杂着异步,
console . log ()为同步输出语句,当队列执行到有定时器时,
单线程的 Js 并不会等待异步编程的结束,而是让后面同步队列先执行完后再去执行异步操作。 

2.事件回调🐏


我们之前学习的事件监听回调也是一个最基本的异步函数。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <button class="button">点我回调</button>
  </body>

  <script>
    console.log('我在回调前面执行');
    document.querySelector('.button').addEventListener('click', () => {
      console.log('点我触发回调函数');
    });
    console.log('我在回调后面执行');
  </script>
</html>

3.网络资源请求*

下节课自平学长会向大伙细🔒

我就举个小🌰

回调地狱问题🐖

回调函数固然好,可以处理js浏览器中很多需要等待的任务,增加浏览器执行代码的效率,提高用户的使用试验。但是当有一个函数中,嵌套了一个回调函数然后在里面又嵌套了一个,无穷的嵌套也就造成了回调地狱问题。

举一个例子,定时器嵌套

function callback() {
  (function () {
    setTimeout(function () {
      console.log(1);
      setTimeout(function () {
        console.log(2);
        setTimeout(function () {
          console.log(3);
          setTimeout(function () {
            console.log(4);
            setTimeout(function () {
              console.log(5);
            }, 1000);
          }, 1000);
        }, 1000);
      }, 1000);
    }, 1000);
  })();
}
callback();

这时候原本让人赏析悦目的代码有序执行的过程俨然变得更加难理解。

回调的嵌套会使得代码的可读性下降,对开发者项目后期改bug调试和维护造成很大的困难!

所以以下就是JavaScript的一些新特性可以解决以上回调地狱问题。

二,Promise🐫

1.Promise构造函数

Promise本质上就是一种构造函数,他能够像模板一样创建带有Promise功能的实例,其中参数会立即执行。

2.Promise的三种状态

  • Pending----Promise对象实例创建时候的初始状态

  • Fulfilled----可以理解为成功的状态

  • Rejected----可以理解为失败的状态

3.Promise使用

当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})
console.log('end')
// new Promise => end

执行一个new Promise构造函数

我们可以利用Promise构造函数的特性进行实例化

let p = new Promise((resolve, reject) => {
    //做一些异步操作
    setTimeout(() => {
        console.log('执行完成');
        resolve('我是成功!!');
    }, 2000);
    p.then(()=>{})
});

也可以使用返回实例函数的方式接收(推荐)

let step = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("在蓝山学习真的太有趣了!");
    }, 1000);
  });
};
let p = step();
p.then((res)=>{
        console.log(res);
})

Promise的构造函数接收一个参数:函数,并且这个函数需要传入两个参数:

  • resolve :异步操作执行成功后的回调函数

  • reject:异步操作执行失败后的回调函数

而resolve和reject则和下面的then链式回调的状态息息相关

4.then链式调用

所以,从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递回调函数要简单、灵活的多。而且then函数本身为promise构造函数的实例,故接受链式调用,所以使用Promise的正确场景是这样的:

链式调用then 的几个规则

1.返回非promise数据的时候 执行成功回调

2.返回promise 数据的时候 根据promise 状态 执行对应回调

3.抛出错误执行失败回调

let p = new Promise((resolve, reject) => {
  //做一些异步操作
  setTimeout(() => {
    console.log("执行完成");
    resolve("好耶");
  }, 2000);
})
  .then(
    (data) => {  
      console.log(data);
      return data;
      //此时输出data为resolve传入的参数
    },
    (error) => {
      console.log(error);
      //此时输出error为reject传入的参数
    }
  )
  .then((data) => {
    console.log(data);
    return data;
    //好耶
  })
  .then((data) => {
    console.log(data);
    //好耶
  })
  .then((data) => {
    console.log(data);
    //undefined
  })
  .catch((error) => {
    console.log(data);
  });     

then链式调用

在结尾加上catch进行错误捕获,用来中断链条,并且捕获错误原因。

我们可以看出,用then执行的函数每一步的执行都会去等待上一步的结果,在视觉上通过then来维系,可读性好,同时还能解决令人眼花缭乱的回调地狱问题,可以说是很优美的代码流程了!

5.catch的用法

我们知道Promise对象除了then方法,还有一个catch方法,它是做什么用的呢?其实它和then的第二个参数一样,用来指定reject的回调。用法是这样:

p.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err);
});

效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。请看下面的代码:

p.then((data) => {
    console.log('resolved',data);
    console.log(somedata); //此处的somedata未定义
})
.catch((err) => {
    console.log('rejected',err);
});

在resolve的回调中,我们console.log(somedata);而somedata这个变量是没有被定义的。如果我们不用Promise,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,会得到这样的结果:

也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能

6.all的用法:谁跑的慢,以谁为准执行回调。

all接收一个数组参数,里面的值最终都算返回Promise对象

Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。看下面的例子:

let Promise1 = new Promise(function(resolve, reject){})
let Promise2 = new Promise(function(resolve, reject){})
let Promise3 = new Promise(function(resolve, reject){})

let p = Promise.all([Promise1, Promise2, Promise3])

p.then(funciton(){
  // 三个都成功则成功  
}, function(){
  // 只要有失败,则失败 
})