前言
JS异步执行机制具有非常重要的地位,尤其体现在回调函数和事件等方面。本文将针对JS异步执行机制进行一个简单的分析。
从一份代码讲起
下面是两个经典的JS定时执行函数,这两个函数的区别相信对JS有一定基础的同学是十分清楚的。timeout仅仅只会执行一次,而interval则会执行多次。
setTimeout(function (args) {
console.log('timeout')
}, 1000);
setInterval(function (args) {
console.log('interval')
}, 1000);
那么再看一份代码
setTimeout(function (args) {
console.log('timeout');
setTimeout(arguments.callee, 1000);
}, 1000);
setInterval(function (args) {
console.log('interval')
}, 1000);
这两份代码是否存在区别呢?在setTimeout中递归调用貌似和setInterval一样,但是实际上由于JS异步执行机制的问题,导致这两个函数存在着一定的差异。
如何理解JS异步执行机制
JS是单线程程序,从而避免了并发访问的一系列问题。但也正是由于单线程这样一个机制,导致JS的异步执行并不能按照传统的多线程方式进行异步执行,所有的异步时间要插入到同一个队列中,依次在主线程中执行。
这里有一张图片,可以比较好的解释JS的异步执行机制。
在浏览器中,一般情况下会存在三个线程,JS执行引擎,HTTP线程,事件触发线程。但是需要注意的是,所有的JS核心逻辑都需要在JS执行引擎线程中执行。
例如我们可以使用下面这样一段代码发送AJAX请求
var xmlReq = createXMLHTTP();//创建一个xmlhttprequest对象
function testAsynRequest() {
var url = "http://127.0.0.1:5000/";
xmlReq.open("post", url, true);
xmlReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlReq.onreadystatechange = function () {
if (xmlReq.readyState == 4) {
if (xmlReq.status == 200) {
var jsonData = eval('(' + xmlReq.responseText + ')');
alert(jsonData.message);
}
else if (xmlReq.status == 404) {
alert("Requested URL is not found.");
} else if (xmlReq.status == 403) {
alert("Access denied.");
} else {
alert("status is " + xmlReq.status);
}
}
};
xmlReq.send(null);
}
testAsynRequest();//1秒后调用回调函数
while (true) {
}
服务端代码
from flask import Flask
app = Flask(__name__)
@app.route('/', methods=['POST', 'GET'])
def print():
return 'hello world'
if __name__ == '__main__':
app.run()
这段代码是否会输出hello world呢?经过测验,发现并不会输出HelloWorld,浏览器会进入假死状态。造成这种情况的原因正是JS异步回调单线程的运行机制。在发送HTTP请求以后,HTTP请求会启动一个线程进行发送,收到响应以后会,事件触发线程会将响应事件加入到等待队列中,等待JS引擎空闲后执行。
但是由于while(true)导致JS引擎永远不存在空闲,从而导致响应事件一致无法触发。
重新思考
通过一个简单的AJAX DEMO,可以简单了解了JS时间执行的一个流程。那么针对上面的那张图片,和最开始提出的settimeout的问题,JS又是如何调度和处理的呢?
JS在定时器函数初始化以后就会开始执行定时任务,到达时间之后如果此时JS引擎空闲,则会直接执行定时任务,否则会将定时任务加入到等待队列中。
对于加入到等待队列中的任务来说,会在JS引擎空闲的时候再不断进行执行。因此如果此时引擎并非空闲,那么setTimeout会等待一段时间后才能执行。
对于setInterval来说,也是需要加入到等待队列中的,但是setInterval并不会因为加入到等待队列中而停止计时,此时如果到了第二个Interval,而第一个Interval还没有开始执行,那么此时队列中旧有存在两个Interval可能,如果这样累加下去,那么就可能会陷入大量Interval的累加,造成线程严重阻塞的问题,因此JS引擎做了一个轻度的优化,如果队列中有Interval,那么这个Interval不会加入队列。但是如果Interval已经pop出队列开始执行,那么Interval将会加入队列。
针对上面的分析,我们可以得出一个结论,相比于setTimeout函数递归调用,在JS中由于单线程的异步执行机制,setInterval执行的频率会更高。因为setTimeout在执行完成以后才会开始下一轮定时任务,但是setInterval是持续执行定时任务,尤其是在setTimeout里的任务执行时间较长的时候,setInterval和setTimeout会有比较明显的频率差异。