从简单案例中理解ES6新规范——回调地狱、Promise对象、async await函数

971 阅读11分钟

1 单线程

Javascript语言的执行环境是"单线程"(single thread),一个任务完成之后才能执行另一个任务。

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

为了解决这个问题,Javascript语言将任务的执行模式分成两种:

  • 同步(Synchronous)
  • 异步(Asynchronous)

2 同步与异步

2.1 同步与异步的区别

2.1.1 同步

  • 同步:同步程序从上到下按顺序执行。
    console.log(1);
    console.log(2);
    console.log(3);
    //output:1 2 3
    

2.1.2 异步

  • 异步:先执行一部分,等拿到结果/到时间了再执行后续代码。

    异步指两个或两个以上的对象或事件同时存在或发生。
    电子邮件就是一种异步通信方式;发送者发送了一封邮件,接收者会在方便时读取和回复该邮件,而不是马上这样做。双方可以继续随时发送和接收信息,而无需双方安排何时进行操作。

    setTimeout(() => {console.log(1)}, 1000)
    setTimeout(() => {console.log(1)}, 100)
    setTimeout(() => {console.log(1)}, 10)
    //output:10 100 1000
    
  • 常见的异步程序:
    • 计时器(SetTimeout, SetInterval
    • ajax
      • 在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。
      • 在软件进行异步通信时,一个程序可能会向另一软件(如服务器)请求信息,并在等待回复的同时继续执行其他操作。例如,AJAX(Asynchronous JavaScript and XML)编程技术(现在的应用不常用XML,而是用JSON)。就是这样一种机制,它通过HTTP从服务器请求较少的数据,当结果可被返回时才返回结果,而非立即返回。
    • 读取文件 在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。例如读取文件。

2.1.3 setTimeout

setTimeout的第三个参数:定时器启动时候,第三个以后的参数作为第一个func()的参数传进去。例: www.runoob.com/try/try.php…

2.2 同步异步先执行哪个?

同步程序执行完成后,执行异步程序。

console.log(1);
for (let i=0; i<2000; i++) {
    console.log(1);
}
setTimeout(() => {console.log(2)}, 0)
setTimeout(() => {console.log(3)}, 0)
console.log(4);
//output:[1]*2000 4 2 3
//需要等到2000个1全都输出之后,4也输出了,才能输出2,3。

3 process.nextTick与setImmediate方法

setImmdiate(() => {
    console.log(1)
})
process.nextTick(() => {
    console.log(2)
})
console.log(3);
setTimeout(() => {console.log(4)},0)
setTimeout(() => {console.log(5)},1000)
setTimeout(() => {console.log(6)},0)
console.log(7);
//output: 3 7 2 4 6 1 5

执行顺序: 同步nextTick异步setImmediate当前事件循环结束,则执行)

4 浏览器的事件循环机制

JavaScript 是单线程的,但在实际开发中确实需处理一些异步的问题,那就要求 JavaScript 的运行环境来提供一套方案让我们更好的处理一些异步问题,在前端层面,浏览器是 JavaScript 唯一的运行环境,这就有了浏览器的事件循环机制。

4.1 宏任务与微任务

异步的程序可以分为宏任务和微任务

4.1.1 宏任务

  • 宏任务:JavaScript 是单线程,但浏览器是多线程的,JavaScript 执行在浏览器中,在 V8 里跑着的一直是一个一个的宏任务,就相当于排队打饭一样,一个人相当于一个宏任务。
    • 宏任务包括:script 整体代码setTimeoutsetIntervalsetImmediateAjaxDOM事件
    • 案例: image.png浏览器在执行上面代码时会先执行主线程代码(宏任务 1)然后再执行setTimeout 里面的代码。虽然 setTimeout 的定时时间为 0,但是浏览器在处理的时候会把它当做下一个宏任务进行处理,定时器也是宏任务的典型代表。

4.1.2 微任务

  • 微任务:当浏览器执行完一个宏任务后,就会看看有没有微任务执行,如果有微任务执行就会先把当前的微任务执行完,再去执行下一个宏任务。
    • process.nextTickMutationObserverPromise.then catch finally image.png

执行顺序:
同步-> process.nextTick -> 微任务 -> 宏任务 -> setImmidiate 虽然JS是单线程的,但是可以通过事件循环异步,解决并发问题。

4.2 运行栈与任务队列

image.png

  • 同步的程序会放到运行栈里执行。
  • 异步的程序放到任务队列里。异步的程序等时间到了的时候放进任务队列里(不是放到队里就立刻执行,要等到运行栈里的程序执行完了,再执行任务队列里的程序)。
  • 事件循环不断循环检测任务队列里是否有任务,若有任务就(按照顺序)执行,没有任务,就一直循环。

主线程从 "任务队列" 中读取事件,这个过程是循环不断的,所以整个过程的这种运行机制又称为 Event Loop(事件循环)。案例:

console.log(1);
setTimeout(() => {
  console.log(2);
});

new Promise((res, req) => {
  console.log(3);
  res();
}).then(() => {
  console.log(4);
});

console.log(5);
//1 3 5 4 2

执行顺序:

代码从上到下执行➡打印 1➡遇到 setTimeout 是下一个宏任务,目前先不处理➡遇到 Promise 打印出 3 then 回调函数是微任务,先不处理➡打印 5 且第一个宏任务执行完毕➡开始执行微任务队列➡打印 4 微任务队列执行完毕➡开始执行下一个宏任务➡打印 2➡程序结束

:只有 Promise then 或者 catch 里面的方法是微任务,Promise 里面的回调是当作主程序的宏任务进行处理的。Promise 新建后就会立即执行。

5 Promise对象、回调地狱、async、await函数

5.1 Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

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

  • 优点:
    • 有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
    • Promise对象提供统一的接口,使得控制异步操作更加容易。
  • 缺点:
    • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
    • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
    • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

5.1.1 Promise用法模板

Promise对象是一个构造函数,用来生成Promise实例。

const promise = new Promise(function(resolve,reject) { 
    if(/*异步程序成功*/){ 
        resolve(res) 
    } else { 
        reject(error) 
    } 
}) 
  • resolvereject是两个函数,由 JavaScript 引擎提供,不用自己部署。
    • resolve:将Promise对象的状态从从 pending 变为 resolved,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
    • reject:将Promise对象的状态从从 pending 变为 rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可用then方法分别指定resolved状态和rejected状态的回调函数:

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

then方法可以接受两个回调函数作为参数:

  • Promise对象的状态变为resolved时调用第一个回调函数
  • Promise对象的状态变为rejected时调用第二个回调函数 这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。

5.1.2 什么时候执行then

let p = new Promise(() => {
    console.log(1)
})
p.then(()=> {
    console.log(2)
})
//output:1 没有2

上面的代码没有输出2,then方法没有执行。那么,什么时候执行then方法?

调用resolve的时候才会执行then。resolve传递出来的值,是then的形参。resolve可以将异步数据传递出来。通过then方法可以拿到异步数据。

以下代码执行了then方法:

let p = newe Promise((resolve) => {
    resolve("1");
})
p.then((data)=> {
    console.log(data)
})
//output: 1

resolve函数的作用是,将Promise对象的状态从 pending 变为 resolved,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。💞💞💞

5.1.3 Promise 的方法

Promise 有几个比较重要的方法,如下所示:

方法
Promise.prototype.then() Promise 实例添加状态改变时的回调函数,then方法返回的是一个新的Promise实例
Promise.prototype.catch()发生错误时的回调函数
Promise.all()Promise.all 可以将多个 Promise 实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被 reject 失败状态的值
Promise.race()可以将多个 Promise 实例包装成一个新的 Promise 实例,哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态
// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

上面代码中,第二种写法要好于第一种写法,第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。

5.2 回调地狱与Promise对象

5.2.1 异步编程的解决方案

Promise 是异步编程的一种解决方案,在没有 Promise 之前,只能通过回调的方式实现异步编程:

function fn(name, fn) {
  const nameVal = "我是" + name;
  //   用定时器模拟异步执行
  setTimeout(function () {
    fn(nameVal);
  }, 1000);
}

fn("张三", function (val) {
  console.log(val); // 我是张三
});

但是如果回调函数比较多的话,就会陷入回调地狱,大大降低了代码的可读性。而 Promise 就是来解决这个问题的。

5.2.2 什么是回调地狱

//获取奶茶的方法
function getTea(fn){
    setTimeout(() => {
        fn("奶茶")
    },1000)
}
//获取火锅的方法
function getHotpot(fn){
    setTimeout(() => {
        fn("火锅")
    },2000)
}
//调用获取奶茶的方法
getTea(function(data)){
    console.log(data)
}
//调用获取火锅的方法
getHotpot(function(data)){
    console.log(data)
}

先输出奶茶还是火锅,要看settime的时间哪个更短。 如果想吃火锅→喝奶茶这种先后顺序,该怎么办?

//调用获取火锅的方法 外层吃火锅
getHotpot(function(data)){
    console.log(data)
    //调用获取奶茶的方法 内层喝奶茶
    getTea(function(data)){
        console.log(data)
    }
}

获取异步程序的数据,不能用return,要用回调函数取数据,而要想按照某个顺序执行,势必就要一层一层的嵌套,而如果调用的方法很多,嵌套的层数很多,代码维护起来就很困难,这就叫回调地狱

5.2.3 Promise 解决回调地狱

通过Promise改造代码,不会出现回调地狱。先定义两个函数,返回的都是一个Promise对象。

function getTea(){
   return new Promise(function(resolve){
       setTimeout(() => {
           resolve("奶茶")
       },1000)
   })
}
function getHotpot(){
       return new Promise(function(resolve){
       setTimeout(() => {
           resolve("火锅")
       },2000)
   })
}

链式操作:

//先吃火锅 再喝奶茶
getHotpot().then(function(data){
    console.log(data);
    return getTea();
}).then(function(data){//第二个then()调的第二个then的返回值
    console.log(data);
})

利用async await 更简洁:

async function getData(){
    let hotPot = await getHotpot();
    console.log(hotPot);
    let tea = await getTea();
    console.log(tea)
}
getData();

5.3 async 与 await

5.3.1 async

Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了:

image.png

async相当于一个Promise对象的简写:

async function fun() {
    return 1
}
//等效于:
function fun() {
    return new Promise((resolve) => {
        resolve(1);
    })
}

async函数的返回值是Promise对象:

async function fun() {
    return 1
}
let a = fun();
console.log(a);
//output: Promise {1} 

fun().then((data) => {
    console.log(data);
})
//output: 1

5.3.2 await

await后跟Promise对象,能直接获取resolve传递出来的异步数据,让异步的代码,写起来更像是同步的代码:

let p = new Promise((resolve => {
    resolve(1)
})
let p = new Promise((resolve => {
    resolve(2)
})

async function fun() {
    let a = await p1;
    let b  = await p2;
    console.log(a);
    console.log(b);
}

fun();
//output: 1 2

5.3.3 练习

练习1:

async function fun1() {
    let data = await fun2();//await等待then拿到结果后 赋值给data  
    console.log(data);//看作 then(微任务)中执行的代码
}
async function fun2() {
    console.log(200);//同步
    return 100
}

fun1();
//output:200 100

练习2:

console.log(1)
aysnc function async1(){
    await async2()
    console.log(2)
}
async function async2(){
    console.log(3)
}
async1()
setTimeout(function () {
    console.log(4)
},0)
new Promise(resolve => {
    console.log(5)
    resolve()
}).then(function () {
    console.log(6)
    })
    .then(function () {
        console.log(7)
    })
console.log(8)
//output: 1 3 5 8 2 6 7 4 

参考

developer.mozilla.org/zh-CN/docs/… www.ruanyifeng.com/blog/2012/1… es6.ruanyifeng.com/#docs/promi…