JavaScript的执行机制简单理解

178 阅读9分钟

js的单线程(为什么是单线程)

JavaScript语言的最大特点就是单线程,同一时间只能做一件事。
1.js的单线程与它的用途有关,作为浏览器脚本语言,js的主要用途就是与用户互动。(如果js有两个线程,一个线程操作dom,另一个线程也操作此dom,浏览器无法识别到底以哪个为准)
2.即使html5提出了Web Worker,允许js脚本创建多个线程,但是子线程完全受控于主线程,且不得操作dom,所以js单线程的本质并未改变。

任务队列

1.单线程就意味着所有任务都得排队,前一个结束,后一个才能执行。
2.排队是因为计算量大,CPU忙不过来,但是很多时候CPU是闲着的,因为IO(输入输出设备)很慢,比如ajax请求。因此JavaScript语言设计者意识到,主线程可以先不管IO设备,将等待任务挂起,先执行后面无需等待的任务。
3.因此出现两种任务,同步任务(在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务)和异步任务(不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行)。

具体的执行机制

1.所有同步任务都在主线程上执行,形成执行栈
2.主线程之外存在一个任务队列(task queue),只要异步任务有了结果,就在“任务队列”中放置一个事件。
3.一旦执行栈中同步任务执行完毕,系统会读取“任务队列”,看看有哪些事件,那些对应的异步任务,结束等待状态进入执行栈,开始执行。
4.主线程不断重复以上三个步骤
(以上4条执行机制摘自阮一峰老师博客

  • 同步和异步任务分别进入不同的执行“场所”,同步的进入主线程,异步的进入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事件完成,回调函数进入Event Queue
  • 主线程从Event Queue读取回调函数success并执行。

一些重要的、特殊的事件

1.setTimeout

普遍认知:异步,延迟执行
但是,实际操作中,很多时候延迟的时间往往比我们实际写的时间要多
举栗子

第一部分
for(var i=0;i<10000;i++){
    console.log('我是for循环,需要循环一万次')
}

setTimeout(() => {
    console.log('延时2秒');
},2000)
// 大家猜,我们两秒后会看到‘延时2秒’的字段吗?
第二部分
setTimeout(() => {
    task()
},3000)

sleep(10000000)
// 3秒后能否执行task函数呢?

给大家梳理下过程(以上两部分为分开的独立代码):
第一部分

  • 执行for循环,时光慢慢~需要很久(具体多久,不一定,每次试验时间都不一样,快的时候大约一秒多,慢的时候大概3秒。)
  • 大概一两秒多过去了,setTimeout内的console.log('延时2秒')进入Event Table并注册,计时开始
  • 2秒到了,计时事件完成,console.log('延时2秒')进入Event Queue
  • 由于主线程已无任务,console.log('延时2秒')从Event Queue进入主线程执行 (因此,两秒后是看不到延时2秒字段的。)

第二部分

  • task()进入Event Table并注册,计时开始
  • 执行sleep()函数,计时仍在继续......
  • 3秒到了,计时事件timeout完成,task()进入Event Queue,但是slepp还在执行,只能暂且等着
  • sleep不知道经历了多久执行完了,task()终于从Event Queue进入了主线程执行

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

我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?
答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。
关于setTimeout要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。

2.setInterval

  • setTimeout类似,不同的是setInterval是每隔指定时间将注册的函数置入到Event Queue,如果主线程任务耗时过多,同样是需要等待的
  • 需要注意的是,对于setInterval(fn,ms)来说,是每隔指定时间将注册的函数置入到Event Queue。一旦setInterval的回调函数fn执行时间超过了ms,就看不出来有时间间隔了。。。

3.Promise与process.nextTick(callback)

  • Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象,具体参考阮一峰老师详细讲解
  • process.nextTick(callback)类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。

除了同步和异步,你不得不知的-->对任务有更精确的定义

1.macro-task(宏任务):包括整体代码script,setTimeout,setInterval
2.micro-task(微任务):Promise,process.nextTick

不同的任务类型会进入不同的任务队列。

事件循环的顺序决定js代码执行的顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。

举栗

setTimeout(function() {
    console.log('setTimeout');
})

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');
  1. 这段js代码为宏任务
  2. 首先遇到setTimeout,将其回调函数注册后,发送到宏任务的Event Queue。
  3. 接下来遇到了new Promise,立即执行,then函数分发到微任务的Event Queue。
  4. 遇到console.log('console'),立即执行。
  5. 第一个宏任务结束,接下来看看有哪些微任务没有执行,我们发现then函数在微任务中,立即执行
  6. 第一轮的事件循环到此结束,接下来开始第二轮,从宏任务开始。发现setTimeout对应的回调函数,执行。
  7. 结束

个人理解(如有错误,劳烦指正)

  • 一段代码就是一个宏任务,会有同步和异步代码。
    1.同步的进入主线程,逐一执行
    2.异步的进入Event Table并注册函数,指定的事情完成后,Event Table将这个函数移入宏任务的Event Queue。简单来说就是异步代码被分发到了宏任务的任务队列中。
    3.这里需要注意的是promise的then函数会被分发到微任务的任务队列中,process.nextTick也是。
    4. 当主线程任务执行完后,去执行微任务队列中的代码 5. 微任务代码执行完毕,第一轮事件循环也就结束了,接下来去到第二步中,异步代码被分发到了宏任务的任务队列。现在开始执行这个里面的宏任务,以此往复。

任务图
上一段复杂的js代码,看你是否真正理解js的执行机制

console.log('1');

setTimeout(function() {     //=> 记为task1
    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() {   //=> 记为wei1
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {    //=> 记为wei2
    console.log('8')
})

setTimeout(function() {  //=> 记为task2
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

  • 第一轮事件循环
    1.console.log('1')为此轮宏任务的同步代码,直接执行。
    2.setTimeout(task1),将函数注册完毕后,分发到宏任务的任务队列
    3.process.nextTick,将函数注册完毕后,分发到微任务的任务队列(wei1)
    4.new Promise函数 console.log('7')直接执行,then函数将函数注册完毕后,分发到微任务的任务队列(wei2)
    5.setTimeout(task2),将函数注册完毕后,分发到宏任务的任务队列
    6.主线程任务执行完毕,去到微任务队列。发现wei1和wei2两个微任务,相继执行 console.log('6')和console.log('8')
    第一轮事件循环的结果:1,7 , 6 , 8

  • 第二轮事件循环
    1.先来看看第一个宏任务task1
    2.console.log('2'),同步,直接执行。
    3.process.nextTick,将函数注册完毕后,分发到微任务的任务队列,即console.log('3');
    4.new Promise函数 console.log('4')直接执行,then函数将函数注册完毕后,分发到微任务的任务队列,即console.log('5')
    5.主线程任务执行完毕,去到微任务队列。发现console.log('3')和console.log('5')两个微任务,相继执行
    第二轮事件循环的结果:2,4 , 3 , 5

  • 第三轮事件循环
    1.再来看看第二个宏任务task2
    2.console.log('9'),同步,直接执行。
    3.process.nextTick,将函数注册完毕后,分发到微任务的任务队列,即console.log('10');
    4.new Promise函数 console.log('11')直接执行,then函数将函数注册完毕后,分发到微任务的任务队列,即console.log('12')
    5.主线程任务执行完毕,去到微任务队列。发现console.log('10')和console.log('12')两个微任务,相继执行
    第三轮事件循环的结果:9,11 , 10 , 12

汇总结果:1,7,6,8,2,4,3,5,9,11,10,12(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

javascript是一门单线程语言

Event Loop(事件循环)是javascript的执行机制

文章参考:
1.阮一峰
2.ssssyoki
3.StarryLake

先总结到这里,如有错误,欢迎指正,互相学习~