回调函数: 把一个函数A作为参数传递给另一个函数B,在函数B中调用A,则A称为回调函数。
//方式一
function B(fun) {
fun();
}
var A = function() {console.log("A");}
//方式二
setTimeout(A, 100);
同步回调: 执行回调函数的时候,要等到回调函数执行完毕,才能接着执行后面的代码。
异步回调: 像在setTimeout(A, 100)中的回调函数,程序执行到这里的时候,不用等100ms之后A函数执行结束才接着向下执行,而是先向下执行,setTimeout的时间到了才去执行A函数。
JS是单线程的原因: 假如现在有两个进程process1,process2操作同一个dom,JS是多线程的,process1正在删除这个dom,process2正在修改这个dom,那浏览器该如何执行?
JS需要异步的原因: 假如不存在异步,那程序只能自上而下顺序执行,并且阻塞等待上一条命令的结果,这种执行效率之低可想而知。
JS实现异步的方法: JS是单线程的,意味着程序在一条线程上执行,实现异步是通过事件循环(event loop)机制。
下图描述的是一个消息队列和事件循环系统,描述:
模拟代码:
//消息队列
class TaskQueue {
public Task takeTask(); //从队列头部取一个任务
public void pushTask(); //将任务放入队列尾部
}
TaskQueue task_queue;
void ProcessTask();
//渲染进程中的渲染主线程
void MainThread() {
while(true) {
Task task = task_queue.taskTask(); //从队列头部取一个任务
ProcessTask(task); //执行普通消息队列中的任务
if(! exit) { //判断是否有退出标志
break;
}
}
}
//渲染进程中的IO线程(用于接收其他进程传递的的消息)
Task newTask;
task_queue.pushTask(newTask); //将任务放入队列尾部
如果渲染进程收到HTML数据,就会将“解析DOM”事件添加到消息队列中;当用户改变Web页面的窗口大小,渲染引擎会将“重新布局”事件添加到消息队列中。这个消息队列称为普通消息队列。
对于像setTimeout这样的异步函数,不能直接将其回调函数加入消息队列中。设想此时消息队列中还有大概需要500毫秒的任务待处理,setTimeout函数是这样的:setTimeout(callback,0),如果将callback任务加入消息队列,则并不是用于期待的0ms之后执行,而是至少要等到500ms之后才能执行。
在Chrome浏览器中,渲染进程不仅维护了多个普通的消息队列,还有多个延迟执行队列,存放定时器这样的需要延迟执行的任务。因此刚才提到的setTimeout(callback,0),渲染进程会将回调任务加入延迟执行队列中。
延迟执行的回调任务包括任务id、回调函数、发起时间、延迟执行时间,其中,任务id用于标记任务以便取消这个定时任务。
在上述的消息队列循环系统的模拟实现代码中,做如下修改:
//渲染进程中的渲染主线程
void MainThread() {
while(true) {
Task task = task_queue.taskTask(); //从队列头部取一个任务
ProcessTask(task); //执行普通消息队列中的任务
ProcessDelayTask(); //执行延迟队列中的任务
if(! exit) { //判断是否有退出标志
break;
}
}
}
也就是当普通消息队列中的一个任务执行结束后,就会去延迟执行队列中执行以及到期的任务,等这些任务执行结束之后,再继续整个事件循环系统。
渲染进程内部会维护多个延迟执行任务队列和普通消息队列,通过消息队列和事件循环机制从这些队列中取出任务并执行,这些队列中的任务称为宏任务。
生产微任务有两种方式:
每个宏任务都维护了一个微任务队列,在宏任务执行的任何时候都可以往微任务队列中加入微任务,这样不会影响到宏任务继续往下执行;微任务在执行的过程中还可以加入微任务,而不是放到下一个宏任务的微任务中。
XMLHttpRequest同setTimeout一样,也是一个异步事件,不同的是,XMLHttpRequest不是延迟执行,而是当网络进程请求完成之后,就会将返回的状态封装成任务添加到消息队列中。
渲染引擎通过事件循环和消息队列机制使渲染进程中的任务有序调度,渲染进程维护了多个普通消息队列和延迟执行任务队列,这两个队列中的任务称为宏任务。每个宏任务中包含一个微任务队列,存放了执行该宏任务过程中创建的微任务。
像“解析DOM”、“页面布局”这样的任务放在普通消息队列中,setTimeout这样的延迟任务放在延迟执行消息队列中,像Promise.resolve()或者Promise.reject()这样的任务,放在宏任务维护的微任务队列中。
宏任务执行完后,执行当前宏任务维护的微任务队列中的所有微任务,然后再去取下一个宏任务。
setTimeout(function(){
console.log('执行了')
},3000)
上面一段代码表示3秒后打印结果,但实际上按照同步机制和异步机制、宏观任务和微观任务机制解释,3秒后,setTimeout里的函数被会推入event queue,而event queue(事件队列)里的任务,只有在主线程空闲时才会执行。所以只有满足3秒后 并且主线程空闲,才会在3秒后的某个时间执行该函数。
实例1:
setTimeout(function(){
console.log('定时器开始..')
});
new Promise(function(resolve){
console.log('马上执行for循环..');
for(var i = 0; i < 5; i++){
}
resolve();
}).then(function (r) {
console.log("成功");
}).catch(function (reason) {
console.log("失败");
});
console.log("结束");
setTimeout(() => {
//执行后 回调一个宏事件
console.log('内层宏事件3')
}, 0)
console.log('外层宏事件1');
new Promise((resolve) => {
console.log('外层宏事件2');
resolve()
}).then(() => {
console.log('微事件1');
}).then(()=>{
console.log('微事件2')
})
结果:
外层宏事件1
外层宏事件2
微事件1
微事件2
内层宏事件3
实例3:
var fetch = fetch('http://localhost:8081/www5.php')
.then(function(response) {
console.log(1);
return response.json();
})
.then(function(myJson) {
console.log(2);
});
console.log(3);
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log(4);
}, 1000);
});
console.log(5);
结果:3 5 1 2 4
所有的程序最终都会由计算机硬件来执行,这就用到DMA(Direct Memory Access,直接内存存取) ,硬盘、光驱的技术规格中都有明确DMA的模式指标,网卡、声卡、显卡也有DMA功能。拥有DMA功能的硬件在和内存进行数据交换的时候可以不消耗CPU资源,只要CPU在发起数据传输时发送一个指令,硬件就开始自己和内存交换数据,在传输完成之后硬件会触发一个中断来通知操作完成。这些无须消耗CPU时间的I/O操作正是异步操作的硬件基础。所以即使在DOS 这样的单进程(而且无线程概念)系统中也同样可以发起异步的DMA操作。