JavaScript是一门最为流行的编程语言之一,在日常的前端开发中我们难以避免地会用到它。而异步编程则是JavaScript的一个重要特性,之所以如此强调异步编程,因为它可以让我们在面对一些需要处理网络请求、本地硬盘文件读取等等异步操作时,不会像同步编程那样陷入阻塞的状态,大大提高了程序的执行效率。
使用异步编程,我们可以通过回调函数、Promise、async/await等多种方式进行实现。但无论使用哪种方式,其核心原理都始终不变,那就是——JavaScript运行环境中维护了一个消息队列(message queue)和一个调用栈(call stack)。当某个异步事件(比如一个定时器到期、一个HTTP请求结束等等)触发后,该事件所对应的回调函数进入消息队列中等待执行,只有当调用栈中的所有函数执行完成,并且消息队列中没有其他事件(回调函数)等待执行时,该事件所对应的回调函数才能被取出执行。这样一来,程序就不会因为单独一个函数阻塞而陷入等待状态,而是可以继续执行其他回调函数,提升了程序的运行效率。
下面我们来看一个简单例子,以便更加说明异步编程的工作原理。在这个例子中,我们首先使用setTimeOut()函数在浏览器中设置一个定时任务,并在该任务触发后,打印一条日志信息到控制台中:
console.log('Beginning'); setTimeout(() =>{console.log('ending');}, 2000); console.log('after setTimeOut');
以上代码的输出顺序非常设定,我们可以预测其输出结果,代码执行流程如下:
- “Beginning”日志信息直接输出到控制台中
- setTimeOut()函数被加入消息队列中,等待计时器到期触发
- “after setTimeOut”日志信息直接输出到控制台中
- 调用栈中的所有函数执行完毕,消息队列中的回调函数被取出执行
- onsole.log('ending') 日志信息输出到控制台中
因此,以上代码最终输出的结果是:Beginning->after setTimeOut->ending。
在上面这个例子中,我们可以明显看到“ending”这条日志信息是在“after setTimeOut”后输出的,这就是异步编程中的典型的“回调函数”方式。我们使用setTimeOut()函数来设定一个定时器,等待指定的时间后,该函数内部触发了一个回调函数,将该回调函数加入消息队列中等待执行。在等待的时间段内,我们的程序并没有被阻塞着等待“睡眠”时间的结束,而是可以继续执行其他函数,比如“after setTimeOut”这个日志信息的输出,而待回调函数触发后,我们运行时环境就会将它从消息队列中取出运行,输出“ending”日志信息到控制台中。
类似于setTimeout()函数,很多常用的Web API都是基于回调函数的异步编程的方式实现的,比如Ajax请求、fetch()函数、文件读写等等。除了回调函数之外,还有Promise、async/await等新的异步编程方式出现,这些方式都绕不开运用JavaScript底层消息队列+调用栈的异步编程的工作原理。
总结一下,异步编程是JavaScript的重要特性之一,可以有效避免程序在执行异步操作时的阻塞等待状态,提高程序的执行效率。但实现异步编程之前需要充分了解JavaScript的消息队列和调用栈,以便于更好地理解合适使用不同的异步编程方式。