转载别人的。js是一门单线程语言。
首先了解一下
js为什么是单线程的?为什么需要异步? 单线程又是如何实现异步的?
(1).为什么是单线程。
现在有2个进程,process1 process2,由于是多进程的JS,所以他们对同一个dom,同时进行操作
process1 删除了该dom,而process2 编辑了该dom,同时下达2个矛盾的命令,浏览器究竟该如何执行呢?
所以 js是单线程的。
(2).js为什么需要异步?
如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。 对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验
(3)js如何实现异步的呢?
是通过的事件循环(event loop),理解了event loop机制,就理解了JS的执行机制
1 js事件循环
setTimeout(function(){
console.log('定时器开始啦')
});
new Promise(function(resolve){
console.log('马上执行for循环啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then函数啦')
});
console.log('代码执行结束');
console的结果是:
// 马上执行for循环了。
// 代码执行结束
//执行then函数
//定时器开始了
js的事件循环
同步任务和异步任务
当我们打开网站,网页的渲染就是一大堆同步任务,比如页面骨架和页面元素的渲染。
加载音乐图片之类占资源大耗时久的任务,就是异步任务。
上图的解释说明:
- 同步和异步任务分别进入不通过的‘执行场所’。 同步的进入主线程。异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程的任务执行完毕的时候,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程不断重复。也就是事件循环-Event Loop
怎么知道主线程的执行栈是否为空呢? js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
let obj = [];
$.ajax({
url:www.baidu.com,
data:obj,
success: () => {
console.log('发送成功')
}
})
console.log('代码执行结束');
上面是一段ajax请求
- ajax先进入Event Table,然后注册回调函数success。
- 执行console.log(‘代码执行结束’)
- ajax事件完成,回调函数success进入Event Queue、
- 主线程从Event Queue读取回调函数success并执行。
2 setTimeout
我们可以用它来实现异步延时执行。例如
setTimeout(() => {
console.log('延迟3秒')
}, 3000)
但是有时候写的延迟3秒,实际5,6秒才执行函数,这有是何缘故?
先看一个例子
setTimeout(() => {
task();
}, 3000)
console.log('执行console')
因为setTimeout是异步的,他的结果是 1. 执行console .2.task()
如果修改一下前面的代码
setTimeout(() => {
task()
}, 3000)
sleep(1000000)
看起来差不多,但是却发现控制台task()需要的时间远远的超过3秒。为什么这么长时间呢?
这时候就要重新理解定义一下setTimeout。 先说下上述代码的执行顺序
- task()进入Event Table并注册,计时开始。
- 执行sleep函数,十分慢,计时仍在继续。
- 3秒到了,计时事件timeout完成,task()进入Event Queue, 但是sleep还没有执行完。。
- sleep终于执行完了,task()终于从Event Queue进入了主线程执行。
上述的流程走完,我们知道setTimeout这个函数,是经过指定的时间后,吧要执行的任务(比如task() )放到Event Queue中,又因为是单线程任务,要一个一个执行,如果前面的任务需要的时间太久,那么只能等着。导致真正的延迟时间远远大于3秒。
我们还经常遇到setTimeout(fn, 0)这样的代码, 0秒后执行是不是刻意立即执行呢?
答案是不会的,setTimeout(fn, 0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思是不用再等多少秒,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。
举例说明:
// 例子1
console.log('先执行这里');
setTimeout(() => {
console.log('执行啦')
}, 0)
//例子2
console.log('先执行这里');
setTimeout(() => {
console.log('执行啦')
}, 3000)
代码的输出结果分别是:
//例子1
先执行这里
执行啦
//例子2
先执行这里
... 3s later
执行啦
关于setTimeout要补充的是,即使主线程为空,0毫秒实际上也是达不到的。根据html的标准,最低是4毫秒。
为什么是最低是4毫秒? 原因有两个
一是W3C标准规定setTimeout中最小的时间周期是4毫秒,凡是低于4ms的时间间隔都按照4ms来处理
二是述举例,我们可以看出js执行的时候,主线程碰到定时器的时候,是不会直接处理的,应该是先把定时器事件交给定时器线程去处理,这时主线程继续执行下面的代码,同时定时器线程开始计时处理,等到计时完毕,事件循环线程会把定时器要执行的操作放在事件队列末尾,等主线程空闲的时候再来执行事件队列里面的操作。
3 setInterval
与setTimeout类似,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue, 如果前面的任务耗时太久,那么同样失去等待。
应该使用setTimeout还是setInterval?
举例,使用setTimeout模拟setInterval.
vay say = function() {
setTimeout(say, 1000)
console.log('hello, world')
}
setTimeout(say, 1000)
这样js碰到定时器,会交给定时器线程处理,然后等计时完毕,定时器里面的操作添加到事件队列,等主线程空闲去执行。
主线程执行时又会遇到定时器,又开始执行上面的一系列操作。
这样做会在每一次定时器执行完毕才开始下一个定时器,其中的误差只是等待主线程空闲所需要等待的时间。
而setInterval是规定每隔固定的时间就往定时器线程中推入一个时间,这样做有一个问题,就是累积效应。
累积效应:就是如果定时器里面的代码执行所需的时间大于定时器的执行周期,就会出现累计效应。简单来说就是上一次定时器里面的操作还没执行完毕,下一次定时器事件又来了。
累积效应会导致有些事件丢失。为了保险起见,尽量使用setTimeout而不是使用setInterval。
4 Promise与process.nextTick(callback)
promise的学习可以参考阮一峰老师的Promise。
process.nextTick(callback),在事件循环的下一次循环中调用callback回调函数
除了广义的同步任务和异步任务,我们队任务有更精细de 定义:
macro-task(宏任务):包括整体代码script setTimeout setInterval
micro-tack(微任务):Promise, process.nextTick
不同类型的任务会进入对应的Event Queue,比如setTimeout setInterval会进入相同的Event Queue。
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环,接着执行所有的微任务。然后再从宏任务开始,找到其中一个任务队列执行完毕,在执行所有的微任务。
举例说明:
setTimeout(function() {
console.log('setTimeout')
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then')
})
console.log('console')
这段代码作为宏任务,进入主线程。
先遇到setTimeout,将其回调函数注册后分发到宏任务Event Queue。
接下来遇到promise, new Promise立即执行,then函数分发到微任务Event Queue。
遇到console.log(),立即执行。
好啦,整体代码script作为第一个宏任务执行结束。看看有哪些微任务? then在微任务Event Queue里面,执行。
OK,第一轮事件循环结束了,开始第二轮循环。从宏任务Event Queue开始,然后是setTimeout对应的回调函数,立即执行。
事件循环,宏任务,微任务的关系如图所示:
接下来是一段复杂的代码,测试是否真的掌握了js的执行机制。
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')
})
})
第一轮事件循环流程。
整体script作为第一个宏任务进入主线程。遇到console.log。 输出1。
遇到setTimeout,,将其回调函数分发到宏任务Event Queue中,暂且记为setTimeout1。
遇到process.nextTrick(),将其回调函数分发到微任务Event Queue。 暂记process1。
遇到promise。newPromise直接指向。输出7。then函数被分发到微任务Event Queue。暂记then1。
又遇到了setTimeout,回调函数分发到宏任务Event Queue中,暂记setTimeout2。
宏任务 | 微任务 |
setTimeout1 | process1 |
setTimeout2 | then1 |
执行微任务
process1 ,输出6。
then1,输出8。
至此,第一轮循环正式结束。 这一轮的结果是 1,7,6,8。
第二轮从setTimeout1开始。过程同上。输出2,4,3,5。
第三轮从setTimeout2开始。过程同上。输出9,11,10,12,。
所以完成的输出是 1 7 6 8 2 4 3 5 9 11 10 12
(请注意。node坏境下的事件监听依赖与前端环境不完全相同,输出顺序可能有误差。)
总结
1.js的异步。
js是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步。其实都是用同步的方法去模拟的。
2.事件循环 Event loop
事件循环是js实现异步的一种方法,也是js的执行机制。
3.js的执行和运行。
执行和运行有很大区别,js在不同的环境下,比如node,浏览器,ringo等等,执行方式不同。而运行大多是指js解析引擎,是统一的。
转载别人的,不知道原作者是谁,但是讲的很清晰,理解容易多了,感谢。
自己手动敲了一遍,补充加了一些别的内容。清晰了很多。