本文讨论的事件循环均是基于浏览器环境上的,类似nodejs环境下的事件循环与此并不相同。
读者首先要对js单线程事件循环机制以及Promise有基本理解;如果这两个概念不是很清楚,建议先阅读下面两篇文章:
THE JAVASCRIPT EVENT LOOP ; Promise 对象
本文是基于THE JAVASCRIPT EVENT LOOP ,并对其内容的延伸,所以下面提到的概念都按这篇文章的来。首先我会总结一下 THE JAVASCRIPT EVENT LOOP 。OK,让我们开始吧。
1,消息队列(message queue)
我们知道js单线程的实现方式会把异步任务(setTimeout回调函数,事件监听回调函数等)放在一个消息队列中;当主任务队列任务为空时会去message queue查询是否有等待执行的任务,如果有则执行。
例1:
var task_in_message_queue = () => {console.log('task in message queue')}
setTimeout(task_in_message_queue,0);
console.log('main task');
//result:
//main task
//task in message queue
setTimeout函数将task_in_message_queue函数添加到message queue队列中。等到主任务队列执行完成时(此时已打印main task),执行存在message queue队列中的task_in_message_queue函数
2,任务队列(job queue)
ES6中引入了任务队列来执行Promise的回调函数。同message queue一样,job queue中的任务也是在主任务队列为空时才开始执行。
例2:
var promise = new Promise((resolve,reject) => {
resolve('task in job queue');
});
var resolve_callback = (resolve_message) => {console.log(resolve_message)}
promise.then(resolve_callback);
console.log('main task');
//result:
//main task
//task in job queue
/**
这里有一个有趣的现象
在chrome中打印出的结果是
main task
task in job queue
undefined //主任务的函数返回值
在firefox中的结果是
main task
undefined //主任务的函数返回值
task in job queue
感觉v8的实现是把job queue整合到了主任务队列尾部
**/
promise.then 将promise fulfilled状态下的回调函数resolve_callback添加到job queue中。等到主任务队列执行完成时(此时已打印main task),执行存在job queue队列中的resolve_callback函数
这里有一点需要注意的是promise构造函数会在主任务中立即执行,例子如下:
var promise = new Promise((resolve,reject) => {
resolve('task in job queue');
console.log('the promise construction executed');
});
var resolve_callback = (resolve_message) => {console.log(resolve_message)}
promise.then(resolve_callback);
console.log('main task');
//result:
//the promise construction executed
//main task
//task in job queue
3,任务队列(job queue)VS 消息队列(message queue)
通过上面的例子我们知道主任务队列优先级是最高的,那么job queue和message queue哪个优先级更高呢?答案是job queue,js会将job queue中的任务完全执行完之后再执行message queue中的任务。例子如下:
var message_task = () => {console.log('message task');}
setTimeout(message_task,0);
var promise1 = new Promise((resolve,reject) => {
resolve('promise 1 resolved');
});
var promise2 = new Promise((resolve,reject) => {
resolve('promise 2 resolved');
});
var resolve_callback = (resolve_message) => {console.log(resolve_message)}
promise1.then(resolve_callback);
promise2.then(resolve_callback);
console.log('main task');
//result:
//main task
//promise 1 resolved
//promise 2 resolved
//message task
/**
这里chrome和firefox返回undefined的位置同上面的例子一样,也是不同的。有兴趣的话可以试试看一下。
**/
4,每次执行message queue中的任务前都会检查job queue吗?
现在我们知道job queue的优先级高于message queue。那么每次执行message queue中任务前会检查job queue吗?我的意思是如果当前job queue为空,message queue中有多个任务(假设有m_task1和m_task2)。js开始执行message queue中的任务,在执行完m_task1时插入了一个j_task1在job queue中。那么接下来是先执行m_task2呢还是j_task1呢?如果先执行了m_task2的话,就说明js一旦开始执行message queue中的任务就会将所有message queue中任务执行完再检查其它任务队列。如果先执行j_task1的话,那么说明再执行每个message queue中的任务前都会先检查其它任务队列,先执行优先级高的任务队列中的任务。为此我们用如下代码来检验:
var promise_task = new Promise((resolve,reject) => {
resolve('j_task1');
});
var resolve_callback = (resolve_message) => {console.log(resolve_message)}
var message_task1 = () => {
promise_task.then(resolve_callback);
console.log('m_task1');
}
var message_task2 = () => {console.log('m_task2');}
setTimeout(message_task1,0);
setTimeout(message_task2,0);
//result:
//m_task1
//j_task1
//m_task2
事实证明js在每次执行message queue中的任务前都会检查其它任务队列(至少会检查job queue),根据队列优先级决定先执行哪个队列中的任务。
5,主任务队列呢?
上面我们了解了job queue和message queue中任务的执行顺序,简而言之:在每次一个任务结束时,js都会根据任务队列的优先级判断下一个执行任务是哪个。如果job queue中有任务则执行job queue中的第一个任务,否则执行message queue中的第一个任务。那么主任务队列是不是也一样呢?(逻辑上应该是一样的,否则job queue或者message queue中的任务可以递归创建新任务,这样就永远无法回到主任务队列了)。
即每次选择执行任务前(或者每次任务结束后),js会根据主任务队列,job queue,message queue的优先级来挑选将要执行下一个任务是哪个。
为此我们声明一个promise和一个message_task函数。在这个promise的回调函数中使用setTimeout创建一个message_task的message queue任务,同时在message_task中调用promise.then 函数创建一个job queue 任务。这样两个任务会循环创建并循环执行。运行后我们会在console中看到两个任务循环打印,这是我们在console中键入alert('stop')命令。如果页面显示了alert,console停止了打印就说明主任务队列的行为方式和job queue,message queue是一样的。否则的话,在这种情况下我们将永远无法回到主任务队列。验证代码如下:
var promise_task = new Promise((resolve,reject) => {
resolve('j_task');
});
var resolve_callback = (resolve_message) => {
setTimeout(message_task,0);
console.log(resolve_message);
}
var message_task = () => {
promise_task.then(resolve_callback);
console.log('m_task');
}
promise_task.then(resolve_callback);
//result:
//console会循环打印 j_task 和 m_task
//这是在console中键入alert('stop')命令,观察是否弹出alert框,console中打印是否终止
希望大家自行求证一下,当然验证完毕后记得刷新页面,不然可能就崩了。另:最好在chrome下验证,firefox有些卡顿。
总结
js事件循环规律可大致总结为如下:
1,js中有三个任务队列:主任务队列,job queue,message queue;
2,它们的优先级是:主任务队列 > job queue > message queue;
3,每当要执行下一个任务前(或者一个任务完成后),js会根据优先级询问各个任务队列是否为空,一旦遇到非空任务队列时则取其第一个任务执行。