事件循环(Event Loop)之setTimout与Promise

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012194956/article/details/82740607

这是我今年秋招笔试面试被考频率最高的一个知识点,没有之一!在连续摔了两跤之后,觉得真的有必要把这个知识点整理一下。

首先了解一下事件循环(event loop)。【参考了阮老师的日志http://www.ruanyifeng.com/blog/2014/10/event-loop.html

1.JavaScript是单线程

JavaScript语言的一大特点就是单线程,也就是说在同一时间只能做一件事。为了利用多核CPU的计算能力,HTML5提出了Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM,本质上来说并没有改变JavaScript单线程的特性。

2.任务队列(task queue)

JavaScript单线程就意味着所有任务需要排队,前一个任务结束之后,才能执行下一个任务。

任务可以分为两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务;异步任务指的是进入“任务队列”的任务。只有“任务队列”通知主线程某个异步任务可以执行了,该任务才会进入主线程执行。

异步执行机制:

(1)所有同步任务都在主线程上执行,形成一个“执行栈”(execution context stack)

(2)主线程之外,还存在一个“任务队列”。只要异步任务有了运行结果,就在“任务队列”中放一个事件

(3)一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”。找到对应该执行的异步任务,结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

3.事件循环(event loop)

“任务队列”是一个先进先出的数据结构,主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop,即事件循环。

补充:回调函数(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

4.定时器

“任务队列”中除了放置异步任务事件以外,还可以放置定时事件,即指定某些代码在多少时间之后执行,也称为“定时器”(timer)功能。定时器主要由setTimeout()和setInterval()这两个函数来完成,前者指定的代码是一次性执行,后者则为反复执行。

setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,它在“任务队列”的尾部添加一个事件,因此要等到同步任务和“任务队列”现有的时间都处理完,才会得到执行。要是当前代码耗时很长,有可能会等很久,所以并没有办法保证回调函数一定会在setTimeout()指定的时间执行。

5.本轮循环和次轮循环

异步任务可以分为两种:(1)追加在本轮循环的异步任务;(2)追加在次轮循环的异步任务。

上述的循环即事件循环,Node规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完毕,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。

6.微任务(Micro-task)

在一步任务重,process.nextTick执行最快,Promise对象的回调函数,会进入异步任务里面的“微任务”队列,微任务队列追加在process.nextTick队列的后面,也属于本轮循环。

下面来分析一些代码。

1.setTimeout

console.log("a");
setTimeout(function(){
	console.log("b");
},0);
console.log("c");
//依次打印 a -> c -> b

解析:执行到setTimeout时,会将其回调函数放入任务队列中等候,待主线程分别打印a,b后,从任务队列中读取并放入主线程中执行,因此最后才打印b。

var i=0;
function create(){
	var i=1;
	return function(){
		setTimeout(function(){
			i++;
		},0);
		console.log(i);
	}
}
var print1=create();
print1();
print1();
var print2=create();
print2();
//三个print1()的结果均打印为 1

解析:这段代码比较刁钻,但本质也是考察的异步执行机制。首先将create()函数赋值给变量print1,然后连续两次调用。create函数的返回值是一个函数,该函数首先设置一个定时器,回调使得i自增1,会被放入任务队列中等待,先执行后面的打印语句,即打印局部变量,也就是create函数中的i,为1。第二次调用相当于又一次将i赋值为1,依然是将setTimeout放入任务队列,先执行打印,也是打印1。单就调用print函数来说,setTimeout并没有起到修改变量的作用,因为每次调用,都会重新初始化i的值为1,然后先打印i,再执行定时器事件。

2. Promise

console.log("a");
new Promise(function(resolve){
	console.log("b");
	resolve();
}).then(function(){
	console.log("c");
});
console.log("d");
//依次打印 a -> b -> d -> c
console.log("a");
var p=new Promise(function(resolve){
	console.log("b");
	resolve();
});
console.log("c");
p.then(function(){
	console.log("d");
});
console.log("e");
//依次打印 a -> b -> c -> e -> d

解析:首先会执行同步任务,即打印a,需要注意的是Promise的实例化也属于同步任务,Promise的异步体现在then()和catch()中,因此接下来会打印b,Promise调用then()方法,其回调函数会放入任务队列中等候,待主线程的任务执行完毕,再执行then中的回调函数。

3.setTimeout && Promise

setTimeout(function(){
	console.log("a");
},0);
var p=new Promise(function(resolve,reject){
	console.log("b");
	resolve("c");
});
console.log("d");
p.then(function(value){
	console.log(value);
});
console.log("e");
//按顺序依次打印:b -> d -> e -> c -> a

解析:首先依然是先执行同步任务,依次打印b,d,e,然后主线程会读取异步任务队列中的事件,因为setTimeout回调会放到次轮循环,而Promise异步事件会放到本轮循环,属于微任务,优先级更高,故先执行Promise的异步,打印c,然后执行setTimeout,打印a。

setTimeout(function(){
	console.log("a");
},0);
new Promise(function(resolve,reject){
	console.log("b");
	resolve();
}).then(function(){
	console.log("c");
});
//依次打印 b -> c -> a
(function test(){
	setTimeout(function(){
		console.log("a");
	},0);
	new Promise(function(resolve){
		console.log("b");
		for(var i=0;i<10000;i++){
			i==9999 && resolve();
		}
		console.log("c");
	}).then(function(){
		console.log("d");
	});
	console.log("e");
})();
//依次打印 b -> c -> e -> d -> a

解析:首先这是一个即时函数,一旦声明则立即执行。然后需要理解的是Promise内部的逻辑,关于逻辑与的计算规则中有一条是:当第一个运算数为一个布尔值true,另一个运算数是对象时,返回这个对象。因此Promise的实例化中的for循环,只有当i=9999时才会执行resolve(),且这个回调会放在then中执行,因此打印b之后会接着打印c,然后继续同步执行e,最后执行异步任务,先Promise后setTimeout。

async function async1(){
	console.log("a");
	await async2();
	console.log("b");
}
async function async2(){
	console.log("c");
}
console.log("d");
setTimeout(function(){
	console.log("e");
},0);
async1();
new Promise(function(resolve){
	console.log("f");
	resolve();
}).then(function(){
	console.log("g");
});
console.log("h");
//依次打印 d -> a -> c -> f -> h -> g -> b -> e

解析:这段代码中异步事件不止setTimeout与Promise,还加了一个async。首先需要知道async函数的哪一部分是放在主线程中执行,哪一部分放在任务队列中等候,以及其任务的优先级。async函数从声明到执行完await语句的这段代码都属于同步任务,await执行完以后的事件需要放到任务队列中等候,其优先级低于Promise,高于setTimeout,也属于次轮循环。因此先执行主线程的同步任务,依次打印d,a,c,f,h,之后从任务队列找到排在前面的任务,即Promise的resolve()回调,打印g,再执行async1中await后面的语句,打印b,最后执行setTimeout,打印e。

猜你喜欢

转载自blog.csdn.net/u012194956/article/details/82740607