javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的"多线程"都是用单线程模拟出来的。
1.js是一门单线程语言
2.Event Loop是javaScript的执行机制
1js是一门单线程语言意思就是说,js的执行顺序是从代码上往下执行的,下面看一段代码
setTimeout(function(){
console.log('定时器开始')
});
new Promise(function(resolve){
console.log('Promise开始');
resolve();
}).then(function(){
console.log('执行then函数')
});
console.log('代码执行结束');
运行结果:
javscript的同步和异步
单线程意味着,所有的任务都需要排队,只有等前面的任务结束,才能进行下一个任务,如果前面一个任务耗时太长,后面一个任务就绪等待很久。
如果排队是因为计算量大,cpu忙不过来也就算了,但是很多cpu是闲着的,因为IO设备(比如ajax请求),不得不等结果出来,再往下执行。
于是所有任务分为两种 1.同步任务(synchronous),2.异步任务(asynchronous)。
同步任务是指在主线程上执行的任务,只有当前面一个任务执行结束再执行后面的任务,存在一个先后执行的问题。异步任务指的是不进入主线程,而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
JS的事件循环(eventloop)是怎么运作的
事件循环、eventloop\运行机制 这三个术语其实说的是同一个东西,
运行过程
1.首先判断是同步还是异步,同步进入主线程,异步进入event table.
2.异步任务在 任务表(event table )注册后,当条件满足后(可能是延迟,也可能是ajax回调),被推入 任务列队( event queue)
3.同步任务进入主线程后一直执行,当主线程任务全部执行完成后,才会去eventqueue中寻找可以执行的任务,如果有就推入主线程。
什么时候知道主线程为空呢?js引擎存在monitoring process进程,会持续不断的检查 主线程 执行栈是否为空,一旦为空,就会去event queue那里检查是否有等待被调用的函数。
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
1.ajax进入event table;
2.执行代码console.log('代码执行结束');
3.ajax完成,将回调函数success进入event queue;
4.js引擎存在monitoring process进程检测到主线程空闲,将success回调推入主线程并且执行。
setTimeout
setTimeout(() => {
console.log('2秒到了')
}, 2000)
settimeout 首先进入event table,注册的时间就是它的回调,2秒后执行回调,将时间推入event queue,当主线程空闲的时候回去event queue中寻找是否有可执行的任务。
console.log(1) // 同步任务进入主线程 1
setTimeout(fun(console.log(3)),0) // 异步任务,被放入event table, 0秒之后被推入event queue里 2
console.log(2) // 同步任务进入主线程 3
运行结果:
运行过程分析:
1.将console.log推入主线程 //1
2.遇到settimeout将settimeout推入event table,0秒后将回调推入event queue;
3.将console.log(2)推入主线程 //2
4.主线程执行完毕,处于空闲状态检测event tale是否有需要执行任务推入主线程 //3
setTimeout延时的时间有时候并不是那么准确
setTimeout(() => {
console.log('2秒到了')
}, 2000)
wait(9999999999)
分析运行过程:
1.将setTimeout进入event table注册,并开始计时
2.执行sleep函数,sleep方法虽然是同步任务但sleep方法进行了大量的逻辑运算,耗时超过了2秒;
3.2000毫秒后将setTimeout回调推入event queue中,但是sleep还没执行完,主线程还被占用,只能等着。
4.sleep终于执行完了, console终于从event queue进入了主线程执行,这个时候已经远远超过了2秒;
注:其实延迟2秒,只是将settimeout的返回函数两秒后从evnet table中推入event queue中,只有当主线程空闲才会去执行 时间队列 (event queue)里的任务。
setIntval
以setIntval(fn,ms)为例,setIntval是循环执行的,setIntval会每隔指定的时间将注册的函数置入event queue,不是每过ms会执行一次fn,而是每过ms秒,会有fn进入event queue。需要注意一点的是,一单setIntval的回调函数fn执行时间超过了延迟事件ms,那么就完成看不出来有时间间隔了。
除了广义的同步任务和异步任务之分,我们对任务还有更精致的定义
宏任务
包含整个script代码块,setTimeout, setIntval
微任务
Promise , process.nextTick
注:不同类型的任务会进入不通的event queue,比如setTimeout, setIntval会进入宏任务的event queue,Promise , process.nextTick会进入微任务的event queue。
Promise与事件循环
注:promise在初始化的时候先会执行里面的函数,然后注册then回调。注册完成后,then不会执行,只有等同步代码执行完成后,再会到微任务的event queue里去查询是否有可用的promise回调,如果有,那么执行,如果没有,继续下一个事件循环。
宏任务执行图:
1.把一段代码看作是一个宏任务
2.在这个代码中遇到setTimeout, setIntval会进入宏任务的event queue,Promise , process.nextTick会进入微任务的event queue.
3.当这段代码的宏任务执行结束后,先查询微任务的event queue,如果存在微任务,执行完毕,在查询宏任务的event queue。如果没有存在的微任务。直接查询宏任务的event queue。
4继续执行下一个宏任务。形成一个循环就是事件循环。
下面用代码来深入理解上面的机制:
setTimeout(function () {
console.log('4')
})
new Promise(function (resolve) {
console.log('1') // 同步任务
resolve()
}).then(function () {
console.log('3')
})
console.log('2')
运行结果:1>2>3>4
1.这段代码作为宏任务,进入主线程
2.先遇到setTimeout 将其注册后,然后将其分发到宏任务的event queue;
3.遇到Promise, new Promise 直接执行,将其then回调分发到微任务的event queue;
4.遇到console.log(), 立即执行;
5.整体script作为一个宏任务执行完毕,查看当前有没有可执行的微任务,发现then回调立即执行;(第一轮事件循环结束了,我们开始第二轮循环)
6.从宏任务的event queue开始,我们发现了宏任务event queue中setTimeout对应的回调函数,立即执行。 执行结果: 1-2-3-4
console.log('1')
setTimeout(function() {
console.log('2')
process.nextTick(function() {
console.log('3')
})
new Promise(function(resolve) {
console.log('4')
resolve()
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6')
})
new Promise(function(resolve) {
console.log('7')
resolve()
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9')
process.nextTick(function() {
console.log('10')
})
new Promise(function(resolve) {
console.log('11')
resolve()
}).then(function() {
console.log('12')
})
})
执行结果:1>7>6>8>2>4>3>5>9>11>10>12
分析:
1.整体script作为第一个宏任务进入主线程,遇到console.log(1)输出1;
2.遇到process.nextTick,其回调函数被分发到微任务event queue中,我们记为process1
3.遇到setTimeout,其回调分发到宏任务event queue;我们暂且记为setTimeout1
4.遇到promies 先执行new promise 遇到 console.log('7') 输出7,then回调分发到微任务event queue;我们记为Promise1
5.遇到settimeout ,其回调分发到宏任务event queue 我们记为setTimeout2.
6.主线程执行完毕,处于空闲状态查找微任务event queue是否有任务,发现process1,Promise1,分别执行;输出6 8;(第一轮循环结束,第二轮循环开始)
8.首先输出2, 接下来遇到了process.nextTick(),统一被分发到微任务event queue,记为process2
9.new promise 立即执行,输出7. then分发到微任务event queue, 记为then2
10.现在开始执行微任务process2,和then2两个微任务可以执行分别输出 3 5(第二轮循环结束,第三轮循环set提settime2开始)
11.settime2输出9, process.nextTick分发进入微任务队列;我们记为process3
12new promise立即执行 ,输出11,then分发到微任务event queue 记为then3;
13现在开始执行微任务process3和then3 分别输出 10 12;查找宏任务event queue,里面不存在任务(三轮循环结束)
讲到这里那么有人会问settiemeout里面嵌套settimeout,那么嵌套的setTimeout的宏任务要在外面的宏任务排序的后面,往后排。看个例子
new Promise(function (resolve) {
console.log('1')// 宏任务一
resolve()
}).then(function () {
console.log('3') // 宏任务一的微任务
})
setTimeout(function () { // 宏任务二
console.log('4')
setTimeout(function () { // 宏任务五
console.log('7')
new Promise(function (resolve) {
console.log('8')
resolve()
}).then(function () {
console.log('10')
setTimeout(function () { // 宏任务七
console.log('12')
})
})
console.log('9')
})
})
setTimeout(function () { // 宏任务三
console.log('5')
})
setTimeout(function () { // 宏任务四
console.log('6')
setTimeout(function () { // 宏任务六
console.log('11')
})
})
console.log('2') // 宏任务一
初步总结:
宏任务是一个栈按先入先执行的原则,微任务也是一个栈也是先入先执行。但是每个宏任务都对应会有一个微任务栈,宏任务在执行过程中会先执行同步代码再执行微任务栈。
一次事件循环就是一次宏任务+微任务执行完毕。
[所见即所得,更直观的的操作,简单明了](http://latentflip.com/loupe/?code=JC5vbignYnV0dG9uJywgJ2NsaWNrJywgZnVuY3Rpb24gb25DbGljaygpIHsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZXIoKSB7CiAgICAgICAgY29uc29sZS5sb2coJ1lvdSBjbGlja2VkIHRoZSBidXR0b24hJyk7ICAgIAogICAgfSwgMjAwMCk7Cn0pOwoKY29uc29sZS5sb2coIkhpISIpOwoKc2V0VGltZW91dChmdW5jdGlvbiB0aW1lb3V0KCkgewogICAgY29uc29sZS5sb2coIkNsaWNrIHRoZSBidXR0b24hIik7Cn0sIDUwMDApOwoKY29uc29sZS5sb2coIldlbGNvbWUgdG8gbG91cGUuIik7!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D
)