一、同步与异步的概念
- 单线程:在JS引擎中负责解释和执行JavaScript代码的线程只有一个,一般称它为主线程。但是实际上还存在其他的线程,可以称之为工作线程。JS的单线程能够提高工作效率。JavaScript的主要用途是与用户互动,以及操作DOM,这就决定了它只能是单线程。单线程意味着前一个任务结束,才会执行后一个任务。
- 同步:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。如果在函数A返回的时候,调用者就能够得到预期的结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。
- 异步:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。如果在函数A返回的时候,调用者还不能马上得到预期的结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的 。
- 异步的过程:主线程发一起一个异步请求,相应的工作线程接收请求并告知主线程已收到通知(异步函数返回);主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,通知主线程;主线程收到通知后,执行一定的动作(调用回调函数)。
- 在异步过程中有两个重要的要素,发起函数和回调函数,这两个函数是分离的。
- 工作线程在异步操作完成后需要通知主线程,通知机制是工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。
- 消息队列:消息队列是一个先进先出的队列,它里面存放这各种消息。
- 事件循环:事件循环是指主线程重复从消息队列中取消息,执行的过程。
- 同步与异步的区别:同步可以保证顺序一致,但是容易导致阻塞;异步可以解决阻塞问题,但是会改变顺序性。
- 任务队列:是一个事件的队列或者消息的队列,所有任务都是在主线程上执行,形成一个执行栈,执行完js主线程的代码才会去看浏览器任务队列中的事件,再执行js代码中该事件对应的代码。任务队列也是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
- 异步的执行机制:
1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
2)主线程之外,还存在一个"任务队列"(task queue),只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
4)主线程不断重复上面的第三步
二、同步与异步的实例
- 同步的实例
1)代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>同步测试</title>
</head>
<body>
</body>
<script>
// 同步 执行顺序符合代码的顺序
console.log("我是第一个div的盒子");
console.log("我是第二个div的盒子");
console.log("我是第三个div的盒子");
console.log("我是第四个div的盒子");
console.log("我是第五个div的盒子");
</script>
</html>
说明:同步相当于是执行顺序符合代码的顺序,前一个任务执行完毕后才会执行下一个任务。这段程序在控制台中会依次输出 我是第一个div的盒子 我是第一个div的盒子 我是第二个div的盒子
我是第三个div的盒子 我是第四个div的盒子 我是第五个div的盒子 。
- 异步的实例
1)在js当中最常见的异步操作就是setTimeOut()以及setTimeInterval(),它们可以控制js的执行顺序,被压入了称之为Event Loop的队列。
2)setTimeOut()方法用于在指定的毫秒数后执行某些操作,可以使用clearTimeOut()方法来阻止函数的执行。在指定的毫秒数后,将定时任务处理的函数添加到执行队列的队尾,所以有时候也可以使用setTimeout解决异步带来的问题。
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>异步测试</title>
</head>
<body>
</body>
<script>
// 异步测试 setTimeout() 在指定的毫秒数后执行某些操作
console.log("我是第一个div盒子");
setTimeout(function(){
console.log("我是第二个div盒子");
},3000);
console.log("我是第三个div盒子");
</script>
</html>
说明:按照代码的执行顺序应该是 我是第一个div盒子 我是第二个div盒子 我是第三个div盒子,但是实际上的输出是 我是第一个div盒子 我是第三个div盒子 我是第二个div盒子。在这里实际上是发生了异步的操作,通过 setTimeout() 函数,设置 我是第二个div盒子 在3秒后执行,添加到执行队列的队尾,让 我是第三个div盒子 先执行,发生异步。
3)setInterval() 方法用于每隔多少时间执行某些操作,可以使用clearInterval()方法来清除定时。按照指定的周期(以毫秒数计时),将定时任务处理函数添加到执行队列的队尾。
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>异步测试</title>
</head>
<body>
</body>
<script>
// 异步测试 setInterval() 每隔多少时间执行某操作
function f3(){
var test2 = setInterval(f4,5000);
}
f3();
function f4(){
console.log("我是测试的setInterval()函数");
}
</script>
</html>
说明:在这里也同样是发生异步操作,本来应该是直接输出 我是测试的setInterval()函数 ,但是实际上是过了5秒后才在控制台中输出。通过setInterval() 函数,将f4函数添加到执行队列中,设置在5秒后执行,发生异步操作。
4)setTimeOut()以及setTimeInterval()的区别:
a. setTimeout多少时间之后执行某操作,只执行一次,而setInterval每隔多少时间之后执行某操作,如果不用clearInterval清除的话,将会一直执行下去
b. 两个方法都返回一个id值,用于清除定时器,分别是clearTimeout和clearInterval
c. setTimeout的性能是要优于setInterval的
d. setTimeout和setInterval都不能保证到了时间点一定会执行,这得取决于当前js线程队列里面还有没有其他待处理队列,如果刚好没有的话,那么就能刚好执行,如果当前线程里面已经有了其它待处理队列正在执行,那么需要排队,等到javascript线程空闲的时候才会执行定时器
e. setTimeout是等待循环的操作执行完成之后,才继续在间隔时间之后再把循环操作添加到javascript的线程里面,而setInterval是不等待的,它从来不管放在线程里面循环操作有没有执行完成,反正到点就会把循环操作添加到javascript线程队列里面
f. 能用setInterval实现的操作,一定能用setTimeout来实现
5)Event Loop:一个回调函数队列,当异步函数执行时,回调函数会被压入这个队列。JavaScript引擎直到异步函数执行完成后,才会开始处理事件循环。这意味着JavaScript代码不是多线程的,即使表现的行为相似。事件循环是一个先进先出(FIFO)队列,这说明回调是按照它们被加入队列的顺序执行的。
三、js的异步情况以及异步解决
- js的异步情况:
1)异步函数,比如 setTimeout和setInterval
2)ajax
3)node.js中的许多函数也是异步的 - 异步问题的解决
1)命名函数
清除嵌套回调的一个便捷的解决方案是简单的避免双层以上的嵌套,传递一个命名函数给作为回调参数,而不是传递匿名函数
代码如下:
var fromLatLng, toLatLng;
var routeDone = function( e ){
console.log( "ANNNND FINALLY here's the directions..." );
// do something with e
};
var toAddressDone = function( results, status ) {
if ( status == "OK" ) {
toLatLng = results[0].geometry.location;
map.getRoutes({
origin: [ fromLatLng.lat(), fromLatLng.lng() ],
destination: [ toLatLng.lat(), toLatLng.lng() ],
travelMode: "driving",
unitSystem: "imperial",
callback: routeDone
});
}
};
var fromAddressDone = function( results, status ) {
if ( status == "OK" ) {
fromLatLng = results[0].geometry.location;
GMaps.geocode({
address: toAddress,
callback: toAddressDone
});
}
};
GMaps.geocode({
address: fromAddress,
callback: fromAddressDone
});
2)使用promise
promise在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了
promise还可以做若干个异步的任务
代码如下:
'use strict';
var logging = document.getElementById('test-promise2-log');
while (logging.children.length > 1) {
logging.removeChild(logging.children[logging.children.length - 1]);
}
function log(s) {
var p = document.createElement('p');
p.innerHTML = s;
logging.appendChild(p);
}
// 0.5秒后返回input*input的计算结果:
function multiply(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' x ' + input + '...');
setTimeout(resolve, 500, input * input);
});
}
// 0.5秒后返回input+input的计算结果:
function add(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' + ' + input + '...');
setTimeout(resolve, 500, input + input);
});
}
var p = new Promise(function (resolve, reject) {
log('start new Promise...');
resolve(123);
});
p.then(multiply)
.then(add)
.then(multiply)
.then(add)
.then(function (result) {
log('Got value: ' + result);
});