for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 0);
console.log(i);
}
一. 单线程
所谓单线程,是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个。不妨叫它主线程。
二. 同步和异步
A(args...);
同步:如果在函数A返回的时候,调用者就能够得到预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。
Math.sqrt(2);
console.log('Hi');
异步:如果在函数A返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。
fs.readFile('foo.txt', 'utf8', function(err, data) {
console.log(data);
});
正是由于JavaScript是单线程的,而异步容易实现非阻塞,所以在JavaScript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。异步是这篇文章关注的重点。
三. 异步过程的构成要素
从上文可以看出,异步函数
实际上很快就调用完成了。但是后面还有工作线程执行异步任务、通知主线程、主线程调用回调函数等很多步骤。我们把整个过程叫做异步过程
。异步函数的调用在整个异步过程中,只是一小部分。
A(args..., callbackFn)
它可以叫做异步过程的发起函数,或者叫做异步任务注册函数。args
是这个函数需要的参数。callbackFn
也是这个函数的参数,但是它比较特殊所以单独列出来。
它们都是在主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果。
setTimeout(fn, 1000);
其中的setTimeout
就是异步过程的发起函数,fn
是回调函数。
注意:前面说的形式A(args..., callbackFn)
只是一种抽象的表示,并不代表回调函数一定要作为发起函数的参数,例如:
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回调函数
xhr.open('GET', url);
xhr.send(); // 发起函数
四. 消息队列和事件循环
上文讲到,异步过程中,工作线程在异步操作完成后需要通知主线程。那么这个通知机制是怎样实现的呢?答案是利用消息队列和事件循环。
while(true) {
var message = queue.get();
execute(message);
}
那么,消息队列中放的消息具体是什么东西?消息的具体结构当然跟具体的实现有关,但是为了简单起见,我们可以认为:
$.ajax('http://segmentfault.com', function(resp) {
console.log('我是响应:', resp);
});
// 其他代码
...
...
...
主线程在发起AJAX请求后,会继续执行其他代码。AJAX线程负责请求segmentfault.com,拿到响应后,它会把响应封装成一个JavaScript对象,然后构造一条消息:
// 消息队列中的消息就长这个样子
var message = function () {
callbackFn(response);
}
其中的callbackFn
就是前面代码中得到成功响应时的回调函数。
五. 异步与事件
var button = document.getElement('#btn');
button.addEventListener('click', function(e) {
console.log();
});
从事件的角度来看,上述代码表示:在按钮上添加了一个鼠标单击事件的事件监听器;当用户点击按钮时,鼠标单击事件触发,事件监听器函数被调用。
事件的概念实际上并不是必须的,事件机制实际上就是异步过程的通知机制。我觉得它的存在是为了编程接口对开发者更友好。
另一方面,所有的异步过程也都可以用事件来描述。例如:setTimeout
可以看成对应一个时间到了!
的事件。前文的setTimeout(fn, 1000);
可以看成:
timer.addEventListener('timeout', 1000, fn);
六. 生产者与消费者
七. 总结一下
这就是同步和异步的区别。同步可以保证顺序一致,但是容易导致阻塞;异步可以解决阻塞问题,但是会改变顺序性。改变顺序性其实也没有什么大不了的,只不过让程序变得稍微难理解了一些 :)