JS是单线程语言, JS的 Event Loop - 事件循环 是JS执行机制。
一、js线程:
JS是单线程的,但onclick回调,setTimeout,Ajax这些都是多线程的,原因是浏览器或node(宿主环境)是多线程的。 浏览器有很多线程:
- GUI 渲染线程
- JS 引擎线程
- 定时器触发线程(setTimeout)
- 浏览器事件线程(onclick)
- http 异步线程
- EventLoop轮询处理线程 ... 其中:1、2、4为常驻线程。
二、进程:
用浏览器打开一个网页,这就是开启了一个进程。但是比如打开3个网页,那么就开启了3个进程。 一个 进程 的运行,当然需要很多个 线程 互相配合,比如打开QQ的这个进程,可能同时有接收消息线程、传输文件线程、检测安全线程...... 所以一个网页能够正常的运行并和用户交互,也需要很多个线程之间相互配合。
线程分类:
类别A:GUI 渲染线程
- 解析html文档生成DOM;
- css代码转换为cssom(css object model);
- 结合DOM和CSSOM生成渲染树;
- 生成布局(layout);
- 将布局绘制在屏幕上;
注意:类别A和类别B是互斥的,js语言设定js引擎与GUI引擎是互斥的,也就是说GUI引擎在渲染时会阻塞js引擎计算。原因很简单,如果在GUI渲染的时候,js改变了dom,那么就会造成渲染不同步。
类别B:JS 引擎线程
即 “主线程”:运行JS代码的那个线程(不包括类别C:其他线程); 主线程运行JS代码时,会生成个 执行栈 ,可以处理函数的嵌套,通过出栈进栈来确定代码执行顺序;
消息队列(任务队列):可以理解为一个静态的队列存储结构,非线程,只做存储,里面存的是一堆异步执行完毕后的回调函数,肯定是先执行完毕的异步的回调函数在队列的前面,后执行完毕的在后面。
注意:是异步执行完毕后,才把其回调函数扔进队列中,而不是一开始就把所有异步的回调函数扔进队列。比如setTimeout 3秒后执行一个函数,那么这个函数是在3秒后才进队列的。
类别C:其他线程(定时器触发线程-setTimeout、http 异步线程、浏览器事件线程-onclick、...)
var a = 2;
setTimeout(fun A)
ajax(fun B)
console.log()
dom.onclick(func C)
主线程在运行这段代码时:
碰到 setTimeout(fun A),把这行代码交给 定时器触发线程 去执行;
碰到 ajax(fun B),把这行代码交给 http 异步线程 去执行;
碰到 dom.onclick(func C) ,把这行代码交给 浏览器事件线程 去执行;
注意:这几个异步代码的回调函数fun A,fun B,fun C,各自的线程都会保存着的,因为需要在未来执行。
这几个线程主要干两件事:
- 执行主线程扔过来的异步代码,并执行代码;
- 保存着回调函数,异步代码执行成功后,通知 “EventLoop轮询处理线程” 过来取相应的回调函数。
小区别:主线程把setTimeout、ajax、dom.onclick分别给三个线程,他们之间有些不同
- 对于setTimeout代码,定时器触发线程在接收到代码时就开始计时,时间到了将回调函数扔进队列。
- 对于ajax代码,http 异步线程立即发起http请求,请求成功后将回调函数扔进队列。
- 对于dom.onclick,浏览器事件线程会先监听dom,直到dom被点击了,才将回调函数扔进队列。
注意:对于setTimeout,setInterval的定时,不一定完全按照设想的时间的,因为主线程里的代码可能复杂到执行很久,所以会发生你定时3秒后执行,实际上是3.5秒后执行(主线程花费了0.5秒)
类别D:EventLoop轮询处理线程
用来处理以上三种线程交流的中介,是不断的循环的去交流和沟通的;
注意:整个的流程是循环往复的,只有主线程的同步代码都执行完了,才会去队列里看看还有啥要执行的没。
消息队列,存储着异步成功后的回调函数,一个静态存储结构,其作用就是存放着未来要执行的回调函数:
setTimeout(() => { console.log(1) }, 2000); setTimeout(() => { console.log(2) }, 3000);在一开始,消息队列是空的,在2秒后,一个 () => { console.log(1) } 的函数进入队列,在3秒后,一个 () => { console.log(2) }的函数进入队列,此时队列里有两个元素,主线程从队列头中挨个取出并执行。
三、同步和异步:
同步:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步:不进入主线程、而进入 任务队列(task queue) 的任务,只有 任务队列 通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
任务进入执行栈 → 同步 → 主线程 → 执行完毕 → 读取Event Queue的回调函数
↓
异步 ——> Event Table ——> 注册回调函数到Event Queue
四、宏任务和微任务:
先执行主线程里的宏任务,执行完之后,去查找是否有微任务,如果有则去执行其对应的回调函数,没有则执行下一个宏任务;
宏任务(macro-task):包括整体代码script、setTimeout、setInterval...
微任务(micro-task):Promise.then、process.nextTick...
不同类型的任务会进入对应的Event Queue
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
宏任务 → 执行完毕 → 是否有微任务 → 是 → 执行所有微任务 → 开始新的宏任务 ↓
否 → 开始新的宏任务
检测宏任务和微任务执行:
console.log(1);
setTimeout(function() {
console.log(2)
},1000);
new Promise(function(resolve) {
console.log(3);
resolve();
}
).then(function() {
console.log(4)
});
console.log(5);
//1 3 5 4 2
/* 说明:在一个事件循环中,执行第一个宏任务(整体代码script ==> 1、3、5),
宏任务执行结束,执行当前事件循环中的微任务(Promise的then ==> 4),
执行完毕之后进入下一个事件循环中,或者说执行下一个宏任务(setTimeout ==> 2)*/
console.log(1);
setTimeout(function() {
console.log(2);
},1000)
setTimeout(function() {
console.log(3);
},0)
console.log(4);
//1 4 3 2
/* 说明:当指定事件完成了才从Event table中注册到Event Queue(事件队列) */
setTimeout(function() {
console.log(1);
},0);
new Promise(function(resolve) {
console.log(2);
resolve();
console.log(3);
}).then(function() {
console.log(4);
setTimeout(function() {
console.log(5);
},0);
});
console.log(6);
//2 3 6 4 1 5
/* 说明:1和5:微任务遇到setTimeout加入宏任务队列,执行宏任务setTimeout的(!!!延时相同)
则是按顺序执行先执行setTimeout,先1后5 */
console.log('1');
new Promise(resolve => {
console.log('2')
resolve()
}).then(() => {
console.log('3')
setTimeout(() => {
console.log('4')
}, 0)
});
setTimeout(() => {
console.log('5')
new Promise(resolve => {
console.log('6')
resolve()
}).then(() => {
console.log('7')
})
}, 100);
setTimeout(() => {
console.log('8')
new Promise(resolve => {
resolve()
}).then(() => {
console.log('9')
})
console.log('10')
}, 0);
//1 2 3 8 10 9 4 5 6 7
/* 说明:执行完区域里的宏任务看是否有微任务,有就执行 */
console.log('start')
a().then((res) => {
console.log('res: '+res);
console.log('a_then');
})
console.log('end');
function a() {
console.log('a_function')
return b().then((res) => {
console.log('res: '+res);
console.log('b_then');
return Promise.resolve('a方法的返回值');
});
};
function b() {
console.log('b_function');
return Promise.resolve('b方法的返回值');
}
//start a_function b_function end res: b方法的返回值 b_then res:a方法的返回值 a_then
function Counter() {
var start = Date.now();
this.num = 0;
this.timer1 = setInterval(function() {
console.log(this); //window
this.num++;
var gap = Date.now() - start;
Promise.resolve(2020).then(res => {
this.num++;
gap -= res;
})
console.log('timer1', this.num, gap);
}, 996);
this.timer2 = setTimeout(() => {
this.num++;
var gap = Date.now() - start;
Promise.resolve(2020).then(function (res) {
this.num++;
gap -= res;
})
console.log('timer2', this.num, gap);
}, 0);
}
new Counter();
//timer2 1 2
//timer1 NaN 997
//timer1 NaN 1993
//timer1 NaN 2989
/* 注意:!!!箭头函数的this指向 */
五、同步任务和异步任务、宏任务和微任务:
同步任务:即刻执行的代码,Promise...
异步任务:setTimeout、setInterval、Promise.then()或catch()、process.nextTick(node)、Async/Await、Object.observe...
宏任务:script、setTimeout、setInterval...
微任务:Promise.then()或catch()、process.nextTick(node)、Async/Await、Object.observe...
也可分为:
同步任务:即刻执行的代码,Promise...
异步任务-微任务:Promise.then()或catch()、process.nextTick(node)、Async/Await、Object.observe...
异步任务-宏任务:setTimeout、setInterval...
总结:当事件要执行的时候才放入任务队列,先执行完同步任务,再去执行异步任务-微任务队列,最后执行异步任务-宏任务队列;