JavaScript事件循环机制讲解

基础

回调函数: 把一个函数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)机制。

消息队列和事件循环

下图描述的是一个消息队列和事件循环系统,描述:

  • 渲染主线程从队里中循环取任务
  • 其他进程如网络进程、浏览器进程通过进程间通信(IPC)和IO线程通信,将准备好的任务发给IO线程,然后IO线程将将任务加入消息队列的尾部
    在这里插入图片描述
    JavaScript就是通过这样的事件循环和消息队列机制保证了任务的有序执行。但是其中还涉及一些概念,比如宏任务、微任务、异步任务等。

模拟代码:

//消息队列
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;
		}
	}
}

也就是当普通消息队列中的一个任务执行结束后,就会去延迟执行队列中执行以及到期的任务,等这些任务执行结束之后,再继续整个事件循环系统。

微任务

渲染进程内部会维护多个延迟执行任务队列和普通消息队列,通过消息队列和事件循环机制从这些队列中取出任务并执行,这些队列中的任务称为宏任务
生产微任务有两种方式:

  • 使用MutationObserver监控某个DOM节点,然后再通过JavaScript来修改这个节点,当DOM节点发生变化时,就会产生DOM变化记录的微任务
  • 使用Promise,当调用Promise.resolve()或者Promise.reject()的时候,会产生微任务

每个宏任务都维护了一个微任务队列,在宏任务执行的任何时候都可以往微任务队列中加入微任务,这样不会影响到宏任务继续往下执行;微任务在执行的过程中还可以加入微任务,而不是放到下一个宏任务的微任务中。

XMLHttpRequest

XMLHttpRequest同setTimeout一样,也是一个异步事件,不同的是,XMLHttpRequest不是延迟执行,而是当网络进程请求完成之后,就会将返回的状态封装成任务添加到消息队列中。

小结

渲染引擎通过事件循环和消息队列机制使渲染进程中的任务有序调度,渲染进程维护了多个普通消息队列和延迟执行任务队列,这两个队列中的任务称为宏任务。每个宏任务中包含一个微任务队列,存放了执行该宏任务过程中创建的微任务。

像“解析DOM”、“页面布局”这样的任务放在普通消息队列中,setTimeout这样的延迟任务放在延迟执行消息队列中,像Promise.resolve()或者Promise.reject()这样的任务,放在宏任务维护的微任务队列中。

宏任务执行完后,执行当前宏任务维护的微任务队列中的所有微任务,然后再去取下一个宏任务。

setTimeout
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("结束");

结果为:
在这里插入图片描述
实例2:

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

异步操作的优缺点

  1. 异步操作无须额外的线程负担,并且使用回调的方式进行处理,处理函数可以减少或者避免共享变量的使用进而减少了死锁的可能。
  2. 编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,所以难以调试。

异步操作的本质

所有的程序最终都会由计算机硬件来执行,这就用到DMA(Direct Memory Access,直接内存存取) ,硬盘、光驱的技术规格中都有明确DMA的模式指标,网卡、声卡、显卡也有DMA功能。拥有DMA功能的硬件在和内存进行数据交换的时候可以不消耗CPU资源,只要CPU在发起数据传输时发送一个指令,硬件就开始自己和内存交换数据,在传输完成之后硬件会触发一个中断来通知操作完成。这些无须消耗CPU时间的I/O操作正是异步操作的硬件基础。所以即使在DOS 这样的单进程(而且无线程概念)系统中也同样可以发起异步的DMA操作。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值