JavaScript执行机制涉及单线程,同步与异步,执行栈,任务队列,宏任务与微任务,Event Loop(事件循环)
单线程
JavaScript是一门单线程语言,在执行一行代码过程中,不能执行其他代码,同一时间里,只能做同一件事。JS之所以是单线程与它的用途有关,JS主要用来操作DOM以及与用户交互,如果JS是多线程。一个线程是在某个DOM节点上增加内容,另一个线程是删除该节点,这时浏览器应该以那个线程为标准便成了问题,为了避免这样的问题,JS从诞生之初便是单线程。
同步与异步
同步
JavaScript同步是指代码按照出现的顺序依次执行,每执行一条代码,其他任何事情都不会发生。只有执行完当前代码,才会执行下一条代码。同步行为对应内存中顺序执行的处理器指令,每条指令都严格按照它出现的顺序执行,而每条指令执行后也能立即获取存储在系统本地的信息,这样的执行流程容易分析程序执行到代码任意位置时的状态,比如变量的值。
异步
JavaScript语句是按照出现顺序依次执行的,同步执行就导致了一个问题,如果遇到从服务器获取图片音频视频等比较耗时的代码时,下面的代码必须等待当前比较耗时的指令执行完才能继续执行,这样导致了很差的用户体验。于是就有了异步的概念
主线程、执行栈、任务队列
JavaScript运行时,对网页结构和元素的渲染,其实就是执行许多同步任务的过程。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。JavaScript代码运行时,浏览器会将同步任务和异步任务放到不同的地方进行执行。
1.对于同于任务,浏览器会将JavaScript执行过程中遇到的同步任务放入主线程中的执行栈执行。
2.对于异步任务,浏览器会将JavaScript执行过程中遇到的异步任务放入Event Table中,并注册事件函数,当事件函数有了结果时,才放进任务队列(Event Queue)中。所以并不是异步任务都会放进任务队列中,而是有了结果的异步任务才会被放进异步任务中。例如Ajax网络请求接收到服务器响应的数据才会放入任务队列中。
3.当浏览器执行完JavaScript脚本中所有的同步任务后,就会去Event Queue读取对应的函数,进入主线程执行栈执行。
例子:
$.ajax({ type: "GET", url: "http://www.liulongbin.top:3006/api/getbooks", data: { }, success: function () { console.log(“我是异步任务”); } }) console.log(\'我是同步任务\') //excepted output //我是同步任务 //我是异步任务
1.程序执行,Ajax是异步任务,进入Event Table,注册回调函数success。
2.执行同步任务console.log("我是同步任务")
3.Ajax接收到服务器的数据得到结果,回调函数success进入任务队列等待主线程执行完同步任务。
4.主线程执行完同步任务后,从任务队列读取回调函数进入执行栈执行。
例子:
setTimeout(() => { console.log(\'我会在一秒后被打印\') }, 1000); for (let i = 0; i < 100000; i++) { console.log("你不会在一秒后被执行") }
可以看到,定时器虽然定时1秒,但是1秒后定时器内的箭头函数并没有 被执行。原因是主线程执行for循环循环次数十分的多耗时较长,异步任务被放进EventTable,定时开始并注册箭头函数,定时时间到达1秒后,箭头函数被放入任务队列等待主线程的同步任务执行完毕后再被执行。所以定时器里的箭头函数执行时间实际上是大于所定时的时间的,定时器的定时时间指的是定时器里的回调函数进入任务队列前在EventTable等待的时间。
宏任务、微任务与事件循环
事件循环,宏任务,微任务的关系如下:
(图片来自掘金社区,我懒不想画了)
任务可以划分为同步任务与异步任务之外,还可以划分为:
宏任务:包括整体代码script,setTimeout,setInterval。
微任务:Promise,process.nextTick。
例子:
console.log(\'1\') setTimeout(function () { //setTimeout1 console.log(\'2\') process.nextTick(function () { //process2 console.log(\'3\') }) new Promise(function (resolve) { console.log(\'4\') resolve() }).then(function () { //then2 console.log(\'5\') }) }) process.nextTick(function () { //process1 console.log(\'6\') }) new Promise(function (resolve) { console.log(\'7\') resolve() }).then(function () { //then1 console.log(\'8\') }) setTimeout(function () { //setTimeout2 console.log(\'9\') process.nextTick(function () { //process3 console.log(\'10\') }) new Promise(function (resolve) { console.log(\'11\') resolve() }).then(function () { //then3 console.log(\'12\') }) })
第一次事件循环流程
1.以整体script代码作为第一次宏任务进入主线程,执行console.log("1") ,输出1 //1
2.遇到setTimeout,回调函数分发到宏任务队列中,记为setTimeout1。
3.遇到process.nextTick(),回调函数被分发到微任务队列中,记为process1
4.遇到new promise,执行console.log("7"),输出7,将then添加到微任务列表中 ,记为then1 //7
5.遇到setTimeout,将setTimeout添加到宏任务列表中,记为setTimeout2
6.宏任务执行完后,检查微任务队列中是否有微任务
7.发现微任务process1,执行微任务process1,输出6 //6
8.发现微任务then1,执行then1,输出8 //8
9.没有微任务,开始新的宏任务。
第二次事件循环流程
1.setTimeout1作为第二次宏任务进入主线程中,执行console.log("2"),输出 2 //2
2.遇到process,将process加入微任务队列,记为process2
3.遇到promise,执行console.log("4") ,将then加入到微任务列表,记为then2 //4
4.宏任务执行完毕,开始检查是否有微任务
5.有微任务,执行微任务procsee2 ,输出3 //3
6.执行微任务then2,输出5 //5
7.微任务执行完毕,开始执行下一个宏任务。
第三次事件循环流程
1.setTimeout2作为第三次宏任务进入主线程。执行console.log("9"),输出9 //9
2.遇到process,将process加入到微任务列表,记为process3
3.遇到promise,执行console.log("11"),输出11,将then加入微任务列表,记为then3 //11
4.宏任务执行完,检查是由有微任务
5.执行微任务Process3,输出10 //10
6.执行微任务then3,输出 12 //12
在node.js环境中运行检查结果
事件循环是js实现异步的一中方法,也是js的一中执行机制