JavaScript异步——callback、promise、async/await

背景

JavaScript是单线程工作,这意味着两段脚本不能同时运行,而且必须一个接一个的运行。

其实JavaScript的单线程与它的用途有很大的关系,JavaScript作为浏览器脚本语言,主要实现与用户的交互。利用JavaScript可以对DOM做各种各样的操作。若JavaScript是多线程的话,一个线程在一个DOM节点中增加内容,另一个线程要删除这个DOM节点。那么这个DOM节点就很纠结,这个DOM节点到底要增加内容还是要删除呢?因此JavaScript是单线程的。

同步任务与异步任务

由于JavaScript的单线程特性,因此同一时间只能处理同一个任务,所有任务都需要排队,前一个任务执行完,才可以执行下一个任务。

但是如果前一个任务的执行时间很长,比如说是文件的读取操作或Ajax操作,后一个任务就不得不等待。就比如是Ajax,当用户向后台获取大量数据时,必须等到所有的数据都获取完才能进行下一步的操作,用户就只能等待,严重影响用户体验。

在JavaScript的设计之初就考虑到了这个问题。主线程可以完全不管设备I/O这种耗时的任务,会挂起处于等待任务;先运行排在后面的任务。等到挂起的任务返回了接轨后,再对挂起的任务进行后续处理。因此任务可以分为同步任务和异步任务。

  • 同步任务:同步任务指在主线程上排队的任务,只有前一个任务执行完毕,才能继续执行下一个任务。例如WEB页面的渲染过程就是一个同步任务。
  • 异步任务:异步任务是指不进入主线程,而进入任务队列的任务。只有任务队列通知主线程,某个异步任务可以执行了。该任务才进入主线程执行。例如图片、音乐资源的加载都是一个异步任务。

具体来说JavaScript任务的执行机制如下:

1.  所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2.  主线程之外,还存在一个“任务队列”(task queue),只要异步任务有了运行结果,就在“任务队列”中放置一个事件。
3.  一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”,看看里边有哪些事件。哪些对应的异步任务,于是结束等待状态,进入“执行栈”开始执行。
4.  主线程不断重复上边的第三步。

JavaScript的主线程和任务队列示意图:

3310_1.png

JavaScript异步编程

在JavaScript中通常使用回调函数、Promise以及async/await的方式实现异步。

回调函数

回调函数是实现异步编程最简单的方式。具体的做法是定义一个函数,将这个函数绑定到事件上,当触发事件后会自动调用这个函数,不需要主动去调用这个函数,称这个函数为回调函数。
例如:

var req = new XMLHttpReauest()
req.open("GET", url)
req.send(null)
req.onreadystatechange=function() {}

onreadystatechange函数上绑定的这个函数就称为是回调函数。

回调具体可以分为具名回调、匿名回调和回调地狱

具名回调

function getUserInfo(fn) {
    fn.call(undefined, 'name: xxx')
}

function userinfo(info) {
    console.log(info)
}

getUserInfo.call(undefined, userinfo)

// name: xxx

匿名回调

function getUserInfo(fn) {
    fn.call(undefined, 'name: xxx')
}

getUserInfo.call(undefined, function(info) {
    console.log(info)
})

多层嵌套的匿名回调(回调地狱)

function getUserInfo(fn) {
    fn.call(undefined, 'name: xxx')
}

function saveUserInfo(fn) {
    fn.call(undefined, 'name: xxx')
}

function getOtherUserInfo(fn) {
    fn.call(undefined, 'name: xxx')
}

getUserInfo.call(undefined, function(info) {
    console.log(info)
    saveUserInfo.call(undefined, function() {
        getOtherUserInfo.call(undefined, function() {
            saveUserInfo.call(undefined, function() {
                ......
            })
        })
    })
})

向上面这种多层匿名回调嵌套就很难读懂和维护,这种代码就称为回调地狱。

回调函数的优点是写法简单,但是容易出现回调地狱。

Promise

Promise对象是CommonJS定义的一种规范,目的为异步编程提供统一的接口。

Promise包括以下几个规范:

  • 一个promise可能有三种状态:等待( pending )、已完成( fulfilled )和已拒绝( rejected )
  • 一个promise的状态只可能从等待转到完成拒绝,不能逆向转换,同时完成拒绝不能相互转换。
  • promise必须实现一个then方法,而且then方法必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致。
  • then方法接受两个参数,第一个参数是成功时的回调,在promise由等待转换为完成时调用;另一个参数是失败时的回调,在promise由等待转换为拒绝时调用。同时then可以接受另一个promise传入,也接受一个类then的对象或方法。
function wait(time) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, time)
    })
}

wait(1000).then(function() {
    console.log(1)
})

async / await

从字面是理解async异步的意思,而await是等待的意思。所以async用于声明一个异步function,而await用于等待一个异步任务执行完成的结果。

其中await只能出现在async函数中,async函数的返回值是一个promise对象。

function test() {
    return new Promise(reslove => {
        setTimeout(() => reslove("test"), 2000)
    })
}

async function test2() {
    const result = await test()
    console.log(result)
}

test2()
console.log('end')

async的作用

通常情况下使用async命令是因为函数内部有await命令,因为await命令只能出现在async函数里面,否则会报语法,这就是为什么async/await成对出现的原因,但是如果对一个普通函数单独加个async会是什么结果呢?来看个例子:

async function test () {
    let a = 2
    return a
}

const res = test()
console.log(res)
3312_1.png

可以看到async函数的返回是一个promise对象。如果函数有返回值,async会把这个返回值通过promise.resole()封装成promise对象。通过then就可以将这个返回值取出来。

res.then(a => {
    console.log(a)      // 2
})

在没有await的情况下async函数会立即执行,并返回一个promise,那么加上await会有什么变化呢?

await的作用

一般情况下await命令后面接的是一个promise对象,等待promise对象状态发生变化,得到返回值,但是也可以接任意表达式的返回结果,例如:

function a () {
    return 'a'
}
async function b () {
    return 'b'
}

const c = await a()
const d = await b()
console.log(c, d)

可以看到await后面不管接什么表达式,都可以等到结果的返回。当等到的不是promise对象时,就将等到结果返回,当等到的是一个promise对象时,会阻塞后面的代码,等待promiset对象状态变化,得到对应的值作为await等待的结果,这里的阻塞是指async内部的阻塞,async函数的调用不会阻塞。

解决了什么问题

promise对象已经解决了回调地狱的问题,那么为什么还要async/await呢?看下面一段代码:

function login () {
    return new Promise(resolve => {
        resolve('aaa')
    })
}

function getUserInfo (token) {
    return new Promise(resolve => {
        if (token) {
            resolve({
                isVip: true
            })
        }
    })
}

function getVipGoods (userInfo) {
    return new Promise(resolve => {
        if (userInfo.isVip) {
            resolve({
                id: 'xxx',
                price: 'xxx'
            })
        }
    })
}

function showVipGoods (vipGoods) {
    console.log(vipGoods.id + '----' + vipGoods.price)
}

login()
    .then(token => getUserInfo(token))
    .then(userInfo => getVipGoods(userInfo))
    .then(vipGoods => showVipGoods(vipGoods))

上面的例子中,每一个promise都相对于是一个异步的网络请求,通常一个业务流对应了对个网络请求,上面的例子描述了每个网络请求都依赖前一个请求的结果的场景,下面采用async/awite重写。

async function call() {
    const token = await login()
    const userInfo = await getUserInfo(token)
    const vipGoods = await getVipGoods(userInfo)
    showVipGoods(vipGoods)
}

call()

相比于promise/then语法结构,使用async/await的调用更加清晰,和同步代码一样。

带来的问题

使用async/await会因为同步执行造成时间的积累,导致程序变慢。本质上async/await将并发执行的任务变为了继发。

在多个任务不关心执行顺序的情况下,继发会浪费很多的执行时间。

你可能感兴趣的:(JavaScript异步——callback、promise、async/await)