深圳幻海软件技术有限公司 欢迎您!

一文明白:JavaScript异步编程

2023-03-02

同步和异步#JS是单线程#JavaScript语言的一大特点是单线程,同一时间只能做一件事(单线程的JS就是一个傻子,脑子一根筋,做着当前的这件事情,没有完成之前,绝对不会做下一件事情)当然,这是由其诞生的初衷所决定的——处理页面中用户的交互,以及操作DOM用户不可能同时进行两个操作,边添加边删除当

同步和异步#

JS是单线程#

JavaScript语言的一大特点是单线程,同一时间只能做一件事

(单线程的JS 就是一个傻子,脑子一根筋,做着当前的这件事情,没有完成之前,绝对不会做下一件事情)

当然,这是由其诞生的初衷所决定的——处理页面中用户的交互,以及操作DOM

用户不可能同时进行两个操作,边添加边删除

当然会出现一个问题:所有的任务需要排队,前一个结束,才会执行下一个(要是前面有人很磨蹭,后面的人需要等很久),造成页面渲染的不连贯

console.log(1)

setTimeout(function(){
    console.log(3)
},100000000);

console.log(2)//快点吧,等的我花都谢了

同步和异步#

问题总有解决方案,利用多核CPU的计算能力,HTML5提出了Web Worker标准,允许JS脚本创建多个线程,于是JS出现了——同步和异步

  • 同步:前一个任务结束执行下一个任务,任务的执行顺序和任务的排列顺序是一致的
  • 异步:在执行某一任务(要花费很长时间)的同时,可以执行其他任务

所以上面那个代码结果是什么呢?

JS没有那么傻,不会一直等待,所以打印了:1 2 3

//那这个呢?
console.log(1)

setTimeout(function(){
    console.log(3)
},0);

console.log(2)

知道同步和异步是什么之后,我们要学习——同步任务和异步任务

  • 同步任务(synchronous)(非耗时任务):同步任务都在主线程上执行,形成一个执行栈
  • 异步任务(asynchronous)(耗时任务):JS的异步任务都是通过回调函数实现的,如:
  1. 普通事件:click、resize等
  2. 资源加载:load、error等
  3. 定时器:setInterval、setTimeout等

JS执行机制#

从内存角度(上图)理解不难发现,同步任务和异步任务根本身处两个区域,当执行任务时:

  1. 先执行执行栈中的同步任务
  2. 异步任务(回调函数)放入任务队列中
  3. 执行完所有的同步任务,就会一次读取任务队列中的异步任务,结束等待,进入执行栈开始执行
//通过底下的;例子练习一下吧(结果不唯一哦)
console.log(1);
document.onclick=function(){
    console.log('click');
}
console.log(2);
setTimeout(function(){
    console.log(3);
},3000)

你会发现,当点击鼠标时,click不断被打印,说明主线程不断获取着任务队列中的异步任务,这种主线程不断

的重复获得任务、执行任务,再获得任务、执行任务的这种机制就是——事件循环(Event Loop)!!!!

宏任务和微任务#

JavaScript把异步任务又做了进一步划分——宏任务和微任务

宏任务(macrotask):

  • 异步Ajax请求
  • setTimeout、setInterval
  • 文件操作
  • 其他宏任务

微任务(microtask):

  • Promise.then、.catch和.finally
  • process.nextTick
  • 其他微任务

既然细分,势必涉及到二者的执行顺序:

每一个宏任务执行完成之后,都会检查是否存在待执行的微任务,如果有,则执行完所有的微任务,再继续执行下一个宏任务

哈哈,到这里你是不是以为已经学了很多,课件到此就结束了#

其实才刚刚开始,使用回调函数只是JavaScript 的异步编程发展的第一个阶段,也只是异步解决方案的其中一种

可以说JavaScript 的异步编程发展经过了四个阶段:

  1. 回调函数、发布订阅
  2. Promise
  3. co 自执行的 Generator 函数
  4. async / await

接下来,我们主要讲一下Promiseasync / await

Promise#

Promise本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。 什么时候会用到过一段时间?

答案是异步操作,异步是指可能比较长时间才有结果的才做,例如网络请求、读取本地文件等

回调地狱#

之所以诞生Promise,是因为回调地狱的问题,废话不多说,先看下面代码:

function demo(num) {
  setTimeout(function () {
    console.log("1");
    if (num > 5) {
      setTimeout(function () {
        console.log(num);
      }, 500);
    } else {
      setTimeout(function () {
        console.log("3");
        setTimeout(function () {
          console.log("4");
          setTimeout(function () {
            console.log("5");
          }, 3000);
        }, 500);
      }, 500);
    }
  }, 500);
}
demo(6);

是不是看到想把电脑砸了?😒

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

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

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

所以一些新特性可以解决以上回调地狱问题。

Promise构造函数#

Promise是一个构造函数,我们可以创建Promise的实例对象

const p=new Promise()

new出来的Promise实例对象,代表一个异步操作

Promise的三种状态#

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

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

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

.then方法#

Promise.prototype上包含一个.then()方法,每一次new Promise()构造函数得到的实例对象,都可以通过原型链的方式访问到.then()方

法,.then()方法用来预先指定成功和失败的回调函数

p.then(成功的回调函数,失败的回调函数)
p.then(result=>{},error=>{})
//调用.then()方法时,成功的回调函数必选,失败的回调函数可选

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链式回调的状态息息相关

then链式调用#

所以,从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得

回调函数能够及时调用,它比传递回调函数要简单、灵活的多。而且then函数本身为promise构造函数的实例,故接受链式调用,所以使

用Promise的正确场景是这样的:

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);
  });

当resolve执行后,promise状态指定为resolved,执行成功的回调。每一次then的执行中参数的data都为上一次异步函数执行的返回值。若上一次无返回值,则输出undefined.错误同理,这就是then链式调用

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

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

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语句有相同的功能

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(){
  // 只要有失败,则失败 
})

有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据,是不是很酷?有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。

race的用法:谁跑的快,以谁为准执行回调#

race的使用场景:比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作,代码如下:

 //请求某个图片资源
    function requestImg(){
        var p = new Promise((resolve, reject) => {
            var img = new Image();
            img.onload = function(){
                resolve(img);
            }
            img.src = '图片的路径';
        });
        return p;
    }
    //延时函数,用于给请求计时
    function timeout(){
        var p = new Promise((resolve, reject) => {
            setTimeout(() => {
                reject('图片请求超时');
            }, 5000);
        });
        return p;
    }
    Promise.race([requestImg(), timeout()]).then((data) =>{
        console.log(data);
    }).catch((err) => {
        console.log(err);
    });

requestImg函数会异步请求一张图片,我把地址写为"图片的路径",所以肯定是无法成功请求到的。timeout函数是一个延时5秒的异步操作。我们把这两个返回Promise对象的函数放进race,于是他俩就会赛跑,如果5秒之内图片请求成功了,那么遍进入then方法,执行正常的流程。如果5秒钟图片还未成功返回,那么timeout就跑赢了,则进入catch,报出“图片请求超时”的信息。运行结果如下:

async/await#

接下来要讲的非常重要,真要实现完美的异步还是得看async/await,这也是目前开发环境中最普遍、最好用的方法。

async/await是ES8引入的新语法,用来简化Promise异步操作,在它出现之前,开发者只能通过链式调用处理Promise异步操作。

async和await是基于promise实现的,可以完美的解决回调地狱问题,优秀的封装也让它实现上述功能所用代码更少,并且可读性更强!

let step = (time) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("前端太有趣了!");
    }, time);
  });
};
async function demo() {
  let word = await step(500);
  console.log(word);//前端太有趣了!
}
demo();

注意:

  • 记住await后面一定要接promise风格的函数且有resolve返回值,否则返回undefined
  • function中使用了await,则function必须被async修饰
  • 在async方法中,第一个await之前的代码会同步执行,await之后的代码会异步执行

当我们不断增加这种异步操作时,整个代码得结构反而如同同步一样清晰,使得回调地狱问题得到了完美的解决

async function demo(){
let word1 = await step(500);
let word2 = await step(1000);
let word3 = await step(1500);
}

我们也可以对step进行Promise封装从而达到异步函数也像同步的执行顺序一样执行,非常人性化!

注意:在await之前的代码属于同步调用,在await之后的代码则会进入异步队列,会在前面的await得到返回值以后再执行后续的代码

let step = (time) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("前端太有趣了!");
    }, time);
  });
};
async function demo() {
  let word1 = await step(500);
  console.log(word1); //前端太有趣了!
  let word2 = await step(1000);
  console.log(word2);//前端太有趣了!
  let word3 = await step(1500);
  return "函数执行完成";
}
demo().then((res) => {
  console.log(res);//函数执行完成
});

async也是基于promise封装的函数,可以调用以后返回一个实例可以通过then的方式接受函数的返回值并进行处理!

建议阅读#

https://juejin.cn/post/6844903487805849613

https://juejin.cn/post/6844904094079926286

https://mp.weixin.qq.com/s/wugntKhMZpgr6RtB1AwAmQ

练习#

分析以下代码输出顺序,将原因结果注释到代码代码旁

setTimeout(() => {
  console.log('setTimeout start');
  new Promise((resolve) => {
    console.log('promise1 start');
    resolve();
  }).then(() => {
    console.log('promise1 end');
  })
  console.log('setTimeout end');
}, 0);
function promise2() {
  return new Promise((resolve) => {
    console.log('promise2');
    resolve();
  })
}
async function async1() {
  console.log('async1 start');
  await promise2();
  console.log('async1 end');
}
async1();
console.log('script end');

(练习答案见评论区)