全栈开发Web前端之路让前端飞

从编程小白到全栈开发:理解异步

2017-10-16  本文已影响556人  一斤代码

作为以JavaScript为主要开发语言的JS全栈开发者,是一定会碰上“异步(Asynchronous)”这个重要概念的,尽早的理解这个概念,会对你的JS编程生涯来说会是非常有利的。

所以,今天我们来扯一扯这个所谓的异步吧!

介绍

“异步”这个词汇,它的反义词是“同步”!我这么一说,是不是理解起来就没那么纠结了?其实在我们的生活中,处处充满着异步。比如有这样一个场景:

我在沙发上看电视,突然感觉肚子饿了,于是我去冰箱里找了些食物,并把它们放到微波炉里去加热5分钟,在加热的过程中,我回到了沙发上继续看我的电视,而不会在微波炉边傻站5分钟,当微波炉发出清脆的一声“叮!”,我才跑过去,取出热腾腾的食物开吃起来。

在以上场景中,我在启动微波炉后,并没有在那儿等待微波炉加热完食物,因为这会白白浪费我约5分钟的时间。我的选择是继续回去看我的电视,等收到微波炉的通知,再回去取食物。这里的结果显而易见了:如果我等待了,我就少看了几分钟的电视;而没等,则多看了几分钟的电视。前者的过程就是“同步”,而后者则是“异步”了。

所以,在同步处理情况下:在开始做一件事情之后,当前事情就会阻止其他事情的进展,只有当这件事情完成,才能继续其他事情:

同步处理

而异步处理则是:在一件事情开始后,不会等待它的完成,可以立即去做其他的事情,等之前那事情完成后,会以某种通知方式告知它已完成:

异步处理

从这个例子中,我们也大致可以看出异步处理在效率上会存在一些优势。

JS中典型的异步

那在我们的JS开发中,哪些地方会遇到异步的情况呢?

在前端的浏览器开发中,最大名鼎鼎的可得算AJAX(Asynchronous Javascript And XML,即异步JavaScript和XML)了,它最典型的应用场景就是我们俗称的“页面局部刷新”。很早之前的网页,页面如果要从服务器更新数据的话,都需要重新向服务器请求,然后服务器发回更新后的整个HTML页面,这种情况在页面变动比较少的时候,浪费是很大的,重复传输和加载了很多页面内容。而借助AJAX,我们可以做到只向服务器端请求页面中想要变动部分的少量数据,然后再把返回的数据更新到页面的那个部分去就行了。

但是AJAX解决的问题不只在于“局部刷新”,而是在于“异步”。因为如果是采用同步方式向服务器请求数据然后更新页面的话,当发出请求后,浏览器就会进入等待状态,用户将不能在页面上进行任何操作。如果这个数据请求的过程比较长,那么页面就会产生长时间的卡死现象,这种体验肯定就非常不好。而AJAX采用了异步方式进行服务器端请求,请求发出后,其他部分还是继续该干嘛干嘛,不用等待,丝毫不受影响。

在后端的Node.js中,最典型的异步处理,可能就是对文件读写和对网络请求的异步处理了,这也是Node.js在这方面性能优良的来源。在这方面,Node.js采取了“你先去干别的吧,等我做完了再告诉你”的方式,避免阻塞等待,大大提高了任务吞吐量。

下面,我们写一些最简单的代码,来看一下在JS中是如何做异步处理的。JS中最简单的异步功能,莫过于setTimeout这个定时器函数了,下面我们就开始用它来编写一些异步代码示例。

一个最简单的异步示例

我们首先来写一个最简单的会异步执行的任务:

// 这是一个异步任务
function asyncTask1() {
    // 该定时器2秒钟后执行
    setTimeout(function () {
        var a = 1
        var b = 2
        var c = a + b
        console.log("async task 1 was done.", a, "+", b, "=", c)
    }, 2000)
}

// 这是另一个异步任务
function asyncTask2() {
    // 该定时器1秒钟后执行
    setTimeout(function () {
        console.log(new Date())
    }, 1000)
}

// 这是一个同步任务
function task1() {
    console.log("do something else...")
}

asyncTask1()
asyncTask2()
task1()

上面代码中,有两个异步任务函数asyncTask1asyncTask2,还有一个普通的同步任务task1。其中一个异步任务使用定时器在2秒后执行一个加法运算,另一个异步任务在1秒后打印当前日期。然后我们执行这段代码,会发现在我们的控制台上是以这样的顺序的输出信息:

do something else...
Mon Oct 16 2017 10:35:48 GMT+0800 (CST)
async task 1 was done. 1 + 2 = 3

可以看到,最先打印出的是task1里的内容。这说明了,在异步任务asyncTask1asyncTask2执行后,其后的task1不会对它们进行等待,而是立即就接着执行了,这就是异步的特性。

控制异步流程

我们从上面的例子可以看到,异步任务会根据自己的情况来执行,我们有可能不知道它到底什么时候执行完,有可能是1秒后,2秒后,也可能是几分钟后(实际的例子,比如我们去请求服务端API,服务端执行并返回结果的精确时间,我们是不可预知的)。而相比,同步任务总是一个挨着一个的排队执行,执行的流程总是可预知的。如果我们需要在含有异步的任务间建立起一个可预知的执行流程,该如何来处理?

一般情况下,我们使用回调机制(callback)。所谓回调机制,即把下一步要执行的函数,当做参数传入任务函数,在任务函数中需要的位置进行调用。

很拗口,还是直接来看代码把,我们对上面的示例代码做了一些小小的调整:

// 这是一个异步任务
function asyncTask1(callback) {
    // 该定时器2秒钟后执行
    setTimeout(function () {
        var a = 1
        var b = 2
        var c = a + b
        console.log("async task 1 was done.", a, "+", b, "=", c)

        if (typeof callback === 'function') {
            callback()
        }
    }, 2000)
}

// 这是另一个异步任务
function asyncTask2(callback) {
    // 该定时器1秒钟后执行
    setTimeout(function () {
        console.log(new Date())

        if (typeof callback === 'function') {
            callback()
        }
    }, 1000)
}

// 这是一个同步任务
function task1() {
    console.log("do something else...")
}

// 任务执行流程:asyncTask1 => asyncTask2 => task1
asyncTask1(function () {
    asyncTask2(function () {
        task1()
    })
})

可以看到,我在异步任务的函数参数中,多加了一个callback参数(这个参数名字你可以任意取),在调用这些函数时,可以传入一个函数当做这个callback参数。然后,在定时器中执行主要工作结束后,callback函数会被调用。这样,就实现了异步函数和传入函数之间在调用顺序问题上的保障了。

也因此,为了实现asyncTask1 => asyncTask2 => task1这样一个执行流程,我们的调用代码就变成了像俄罗斯套娃一样一层套一层的结构:

asyncTask1(function () {
    asyncTask2(function () {
        task1()
    })
})

如果你的任务函数有很多,那这个嵌套就会非常的深。这让我想起了一个笑话:一个俄罗斯特工窃取了美国一个重要软件系统的最后几页代码,然后发回总部,总部那个欣喜若狂啊,赶紧打印出来查看,发现代码的内容是这样的:

                                                                                            })
                                                                                        })
                                                                                    })
                                                                                })
                                                                            })
                                                                        })
                                                                    })
                                                                })
                                                            })
                                                        })
                                                    })
                                                })
                                            })
                                        })
                                    })
                                })
                            })
                        })
                    })
                })
            })
        })
    })
})
...

嗯,不错,还挺好看呢-o-

改进这无尽的嵌套

上面那种可怕的嵌套,除了代码结构复杂,不容易读懂外,更有功能性障碍:内层函数向外层函数传递数据将变得困难和不优雅。

好在我们已经有一些方案来比较好的改进这个问题了,最主流的都是基于Promise这个概念的。概念知识自己可以点击链接了解一下,我在这里只想给大家看一下Promise是如何来改进嵌套问题的。

继续改我们上面的示例代码:

// 这是一个异步任务
function asyncTask1() {
    return new Promise(function (resolve, reject) {
        // 该定时器2秒钟后执行
        setTimeout(function () {
            var a = 1
            var b = 2
            var c = a + b

            console.log("async task 1 was done.", a, "+", b, "=", c)

            resolve(c)
        }, 2000)
    })
}

// 这是另一个异步任务
function asyncTask2() {
    return new Promise(function (resolve, reject) {
        // 该定时器1秒钟后执行
        setTimeout(function () {
            var date = new Date()

            console.log(date)

            resolve(date)
        }, 1000)
    })
}

// 这是一个同步任务
function task1() {
    console.log("do something else...")
}

// 任务执行流程:asyncTask1 => asyncTask2 => task1
asyncTask1().then(asyncTask2).then(task1)

可以看到,我们的异步工作都被包装在一个Promise对象中,当工作完成时,可以选择使用resolve(代表成功)或reject(代表失败)函数来进行结束。然后,最立竿见影的,就是在流程控制部分的代码了,嵌套不见了,只留下语义清晰的then...then....

由于我们上面的异步任务中,都会通过 resolve函数返回出一个结果,所以如果我们在任务的调用过程中需要对执行结果进行额外的处理和使用,那么可以这样来写我们的代码:

asyncTask1()
    .then(function (result) {
        console.log('asyncTask1 result:', result)
    })
    .then(asyncTask2)
    .then(function (result) {
        console.log('asyncTask2 result:', result)
    })
    .then(task1)

所以,我们不用再写被套牢的代码啦!

另外,上面的例子里,我们的流程是串行执行三个任务的,后一个任务相当于还是要等待前一个任务的执行结束才能被执行,这样有时候就体现不出异步的优势了。比如在我们的例子中,如果asyncTask1asyncTask2之间没有强制的顺序要求,只需要它们都执行完后再执行task1,这种情况我们怎么来写呢?

可以这样:

Promise.all([
    asyncTask1(),
    asyncTask2()
]).then(function (result) {
    console.log('asyncTask1 result:', result[0])
    console.log('asyncTask2 result:', result[1])
}).then(task1)

这里用到了Promise.all函数来进行了并行执行两个异步的promise。其实Promise还提供了其他一些用来控制promise执行的工具函数,比如Promise.race。一些第三方开源Promise实现库提供了更为丰富的特性,比如bluebird,建议大家可以看一下。

再改进一下?

有了Promise来做异步流程的控制,情况已经发生了明显的改善。不过,then...then...的写法毕竟还是对有些人,特别是以前使用Java之类语言的朋友来说不是很习惯。有没有什么方式,可以让写异步流程像同步流程的方式来写?在JavaScript ES2017规范中,加入了async/await的特性,这个特性就是用来做这样的事情的。

我们来看下经过async/await改造过后的对三个任务的顺序调用:

// await必须在声明为async的函数中使用
async function start() {
    await asyncTask1() 
    await asyncTask2()
    task1()
}
start()

是不是更加直观,更接近于普通同步代码的写法了?

总结

今天初步讲解了一些JavaScript中异步和异步处理的知识,其实这些内容在JS编程中是贯穿始终的,你不可能不会用上。所以,好好理解这些内容,将会对你理解和掌握JS编程非常的有帮助。

拥抱不同的理念,从不同的理念中吸取对自己有用的东西。
欢迎关注一斤代码的系列课程《从编程小白到全栈开发》

【省钱小能手】
1. 阿里云通用代金券:最高1888元,云产品通用红包,可叠加官网常规优惠使用
2. 阿里云基础版服务器优惠套餐:1核1G仅需293元/年
3. 阿里云高性能服务器优惠套餐:首次购买企业级云服务器5折

上一篇下一篇

猜你喜欢

热点阅读