深入理解Js 执行机制

870 阅读6分钟

一、事件循环 event loop

js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。

假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?

于是便有了 同步任务和异步任务两类。

当打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

以下为一个栗子:

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('发送成功!');
    }
})
console.log('代码执行结束');

执行顺序为:

  • ajax进入Event Table,注册回调函数success。
  • 执行console.log('代码执行结束')。
  • ajax事件完成,回调函数success进入Event Queue。
  • 主线程从Event Queue读取回调函数success并执行。

二、setTimeout

有时候setTimeout写了三秒钟,结果最后比三秒要长??

setTimeout(() => {
    task()
},3000)

sleep(10000000)

以上代码的执行过程:

  • task()进入Event Table并注册,计时开始。
  • 执行sleep函数,很慢,非常慢,计时仍在继续。
  • 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。
  • sleep终于执行完了,task()终于从Event Queue进入了主线程执行。

setTimeout是经过指定时间后,把要执行的任务(task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。

In addition, setTimeout(fn,0)也不一定非得 0秒后执行,是指某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行,而且不可能0毫秒后就理解执行,会有4毫秒延迟,防止嵌套调用导致的队列堵塞。参照我之前的文章: www.jianshu.com/p/c1ff02a3d…


三、setInterval

循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。

需要注意的,对于setInterval(fn,ms)来说,并不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。


四、macro-task(宏任务)和micro-task(微任务)

macro-task(宏任务):包括整体代码script,setTimeout,setInterval

micro-task(微任务):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开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
  • 结束。

最后一个复制的栗子:

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。
  2. 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。
  3. 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。
  4. 遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。
  5. 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。
宏任务Event Queue微任务Event Queue
setTimeout1process1
setTimeout2then1
  1. 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。

  2. 我们发现了process1和then1两个微任务。

  3. 执行process1,输出6。

  4. 执行then1,输出8。

第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:

  1. 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
宏任务Event Queue微任务Event Queue
setTimeout2process2
then2
  1. 第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。
  2. 输出3。
  3. 输出5。
  4. 第二轮事件循环结束,第二轮输出2,4,3,5。
  5. 第三轮事件循环开始,此时只剩setTimeout2了,执行。
  6. 直接输出9。
  7. 将process.nextTick()分发到微任务Event Queue中。记为process3。
  8. 直接执行new Promise,输出11。
  9. 将then分发到微任务Event Queue中,记为then3。
宏任务Event Queue微任务Event Queue
process3
then3
  1. 第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。
  2. 输出10。
  3. 输出12。
  4. 第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)