前言
异步一直是前端开发里最让人头疼的一个难点,接下来的几篇文章,将围绕这个话题展开。
1. 单线程的语言-JavaScript
众所周知,JS最初的目的是用于处理浏览器的用户交互和操作DOM,因此,如果JS设计成允许同时存在2个以上的线程,就会出现以下这种问题:
2个线程同时操作了同一个DOM节点(a线程要编辑该节点,而b线程删除该节点),那么此时浏览器将无法处理,因为无法判断以哪个线程为基准。因此,JS只能是单线程。(Web Worker API虽然提供了多线程,但只是纯粹基于使用多核cpu的计算能力,其创建的子线程严格受控,不影响JS单线程的设计实质)
,单线程的设计就意味着,任务以排队的方式依此执行。
基于单线程设计,不可避免的遇到一个情形:某些任务需要的时间很长,但不是因为任务本身太过复杂,难以处理,而是输入输出太慢(例如Ajax获取数据)。而在等待输入输出的过程中,CPU是闲置的,为了充分利用资源,这一类任务被设计成允许暂时挂起,等到有了结果再执行的任务。
现在有两种任务了:同步任务和异步任务
接下来介绍JS的处理机制。
2. Event Loop
理论基础
首先看来自MDN的一张图:
看这个例子:
function a(){ console.log('a') } function b(){ console.log('from') a() // 这里调用了函数a } b()
在Chrome中运行,并且单步调试,可以看到以下步骤:
- 执行
b()
时,函数b进栈(如图1) - 在
b
中调用函数a
时,a
继续进栈(如图2) - 函数a执行完毕,出栈(如图1)
- 执行
(这部分内容实际上对应着之前介绍闭包时,函数作用域链的生成部分,传送门)
常见示例:
- 让页面中的某个按钮,点击时触发
handleClick
函数,那么,当用户触发点击按钮的动作时,会有一个待处理消息进入queue,关联的函数为handleClick
。 - 发起一个ajax请求,当请求有结果之后,会有一个待处理消息进入queue,关联的函数为所指定的回调函数
- 让页面中的某个按钮,点击时触发
整体运行过程
整体的执行过程如下(如图):
- 主线程执行同步代码,执行过程会产生对应的函数调用栈stack,如果碰到有异步事件,如发起ajax请求,则提交给对应的异步模块处理,当异步任务有结果时,异步模块负责在消息队列中添加待处理的消息;
- 当同步任务处理完成,函数调用栈清空时,主线程检查消息队列queue:如果消息队列不为空,那么从消息队列头部取出一个待处理的消息,进入主线程;
- 主线程重复以上过程
上述过程循环执行,所以称为事件循环(Event Loop)
// 简单的例子
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){}; //指定回调函数, 这是一个异步任务,会被先提交到异步处理的api,等有了结果才会添加到消息队列
req.send();
*任务队列类型
补充说明以下,任务队列分成2类:
- microtask queue:ES6 的 promise产生的任务队列
- macrotask queue:除microtask queue以外的任务产生的任务队列,如(事件触发 setTimeout Ajax请求)
他们的区别下次讲解Promise时再说明(挖个坑)
3.定时器
上述Event Loop模型中,消息队列的新消息来源,除了有dom事件操作,ajax请求等,也可能是定时任务,也就是由setTimeout
创建的任务。这个函数大家肯定不陌生,但是也可能未必真的足够熟悉~。
setTimeout
接受两个参数:
- 回调函数
- 延迟执行的毫秒数。(严格来说,应该是实际加入到主线程的最小延迟时间,为什么呢,往下看)
现在看下以下2个例子:
//示例1
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
// 输出结果 1 3 2 ,因为setTimeout指定了里面的函数要推迟1000毫秒才会执行
这个例子说明了setTimeout的基本作用,比较简单不多说。
//示例2
const s = new Date().getSeconds(); //获取当前的秒数
setTimeout(function() {
// 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);
while(true) {//这个循环含义就是,至少要过2s,当前主线程任务才执行完毕
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
//实际输出
Good, looped for 2 seconds
eventloop.html:15 Ran after 2 seconds
这个例子,首先使用setTimeout
指定了一个500毫秒后执行的回调函数,然后使用while
循环故意让当前运行超过2秒钟,根据上文的流程图可知:
其实在第500毫秒时,这个消息已经被添加到消息队列,但是由于当前的主线程并没有执行完,调用栈尚未清空,所以在500毫秒不会执行setTimeout
指定的回调函数。实际上,即使把上述代码中的500
改成0
,结果也是一样的。
简而言之,setTimeout(fn,x毫秒)
的x只是指定了fn被执行的最小等待时间,息具体能在多少时间之后执行,取决于现有调用栈函数的执行进度,以及消息队列中前面的任务执行进度。
小结
本文介绍了Event Loop模型过程以及常见的任务队列的几种任务队列消息来源,这是JS异步话题的基础篇。
参考文献:
MDN-EventLoop
JavaScript 运行机制详解:再谈Event Loop
惯例:如果内容有错误的地方欢迎指出(觉得看着不理解不舒服想吐槽也完全没问题);如果有帮助,欢迎点赞和收藏,转载请征得同意后著明出处,如果有问题也欢迎私信交流,主页有邮箱地址