共计 6586 个字符,预计需要花费 17 分钟才能阅读完成。
- 事件循环 (event-loop) 是什么?
总方针是先同步再异步,异步中先微工作,在宏工作。
- macro-task(宏工作):setTimeout,setInterval
- micro-task(微工作):Promise.then/catch,process.nextTick
这一次,彻底弄懂 JavaScript 执行机制
本文的目标就是要保障你彻底弄懂 javascript 的执行机制,如果读完本文还不懂,能够揍我。
不管你是 javascript 老手还是老鸟,不论是面试求职,还是日常开发工作,咱们常常会遇到这样的状况:给定的几行代码,咱们须要晓得其输入内容和程序。因为 javascript 是一门单线程语言,所以咱们能够得出结论:
- javascript 是依照语句呈现的程序执行的
看到这里读者要打人了:我难道不晓得 js 是一行一行执行的?还用你说?稍安勿躁,正因为 js 是一行一行执行的,所以咱们认为 js 都是这样的:
let a = '1';
console.log(a);
let b = '2';
console.log(b); 复制代码
然而实际上 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('代码执行完结'); 复制代码
按照 js 是依照语句呈现的程序执行这个理念,我自信的写下输入后果:
//"定时器开始啦"
//"马上执行 for 循环啦"
//"执行 then 函数啦"
//"代码执行完结" 复制代码
去 chrome 上验证下,后果齐全不对,霎时懵了,说好的一行一行执行的呢?
咱们真的要彻底弄明确 javascript 的执行机制了。
1. 对于 javascript
javascript 是一门单线程语言,在最新的 HTML5 中提出了 Web-Worker,但 javascript 是单线程这一外围仍未扭转。所以所有 javascript 版的 ” 多线程 ” 都是用单线程模仿进去的,所有 javascript 多线程都是纸老虎!
2.javascript 事件循环
既然 js 是单线程,那就像只有一个窗口的银行,客户须要排队一个一个办理业务,同理 js 工作也要一个一个程序执行。如果一个工作耗时过长,那么后一个工作也必须等着。那么问题来了,如果咱们想浏览新闻,然而新闻蕴含的超清图片加载很慢,难道咱们的网页要始终卡着直到图片齐全显示进去?因而聪慧的程序员将工作分为两类:
- 同步工作
- 异步工作
当咱们关上网站时,网页的渲染过程就是一大堆同步工作,比方页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的工作,就是异步工作。对于这部分有严格的文字定义,但本文的目标是用最小的学习老本彻底弄懂执行机制,所以咱们用导图来阐明:
导图要表白的内容用文字来表述的话:
- 同步和异步工作别离进入不同的执行 ” 场合 ”,同步的进入主线程,异步的进入 Event Table 并注册函数。
- 当指定的事件实现时,Event Table 会将这个函数移入 Event Queue。
- 主线程内的工作执行结束为空,会去 Event Queue 读取对应的函数,进入主线程执行。
- 上述过程会一直反复,也就是常说的 Event Loop(事件循环)。
执行流程:主线程从 ” 工作队列 ” 中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环。此机制具体如下: 主线程会一直从工作队列中按程序取工作执行,每执行完一个工作都会查看 microtask 队列是否为空(执行完一个工作的具体标记是函数执行栈为空),如果不为空则会一次性执行完所有 microtask。而后再进入下一个循环去工作队列中取下一个工作执行。
咱们不禁要问了,那怎么晓得主线程执行栈为空啊?js 引擎存在 monitoring process 过程,会继续一直的查看主线程执行栈是否为空,一旦为空,就会去 Event Queue 那里查看是否有期待被调用的函数。
说了这么多文字,不如间接一段代码更直白:
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {console.log('发送胜利!');
}
})
console.log('代码执行完结'); 复制代码
下面是一段繁难的 ajax
申请代码:
- ajax 进入 Event Table,注册回调函数
success
。 - 执行
console.log('代码执行完结')
。 - ajax 事件实现,回调函数
success
进入 Event Queue。 - 主线程从 Event Queue 读取回调函数
success
并执行。
置信通过下面的文字和代码,你曾经对 js 的执行程序有了初步理解。接下来咱们来钻研进阶话题:setTimeout。
3. 又爱又恨的 setTimeout
赫赫有名的 setTimeout
无需再多言,大家对他的第一印象就是异步能够延时执行,咱们常常这么实现延时 3 秒执行:
setTimeout(() => {console.log('延时 3 秒');
},3000)复制代码
慢慢的 setTimeout
用的中央多了,问题也呈现了,有时候明明写的延时 3 秒,理论却 5,6 秒才执行函数,这又咋回事啊?
先看一个例子:
setTimeout(() => {task();
},3000)
console.log('执行 console'); 复制代码
依据后面咱们的论断,setTimeout
是异步的,应该先执行 console.log
这个同步工作,所以咱们的论断是:
// 执行 console
//task()复制代码
去验证一下,后果正确!
而后咱们批改一下后面的代码:
setTimeout(() => {task()
},3000)
sleep(10000000)复制代码
乍一看其实差不多嘛,但咱们把这段代码在 chrome 执行一下,却发现控制台执行 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. 又恨又爱的 setInterval
下面说完了 setTimeout
,当然不能错过它的孪生兄弟setInterval
。他俩差不多,只不过后者是循环的执行。对于执行程序来说,setInterval
会每隔指定的工夫将注册的函数置入 Event Queue,如果后面的工作耗时太久,那么同样须要期待。
惟一须要留神的一点是,对于 setInterval(fn,ms)
来说,咱们曾经晓得不是每过 ms
秒会执行一次 fn
,而是每过ms
秒,会有 fn
进入 Event Queue。一旦 setInterval
的回调函数 fn
执行工夫超过了延迟时间ms
,那么就齐全看不出来有工夫距离了。这句话请读者认真品尝。
5.Promise 与 process.nextTick(callback)
传统的定时器咱们曾经钻研过了,接着咱们探索 Promise
与process.nextTick(callback)
的体现。
Promise
的定义和性能本文不再赘述,不理解的读者能够学习一下阮一峰老师的 Promise。而 process.nextTick(callback)
相似 node.js 版的 ”setTimeout”,在事件循环的下一次循环中调用 callback 回调函数。
咱们进入正题,除了狭义的同步工作和异步工作,咱们对工作有更精密的定义:
- 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
对应的回调函数,立刻执行。 - 完结。
事件循环,宏工作,微工作的关系如图所示:
咱们来剖析一段较简单的代码,看看你是否真的把握了 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.nextTick()
,其回调函数被散发到微工作 Event Queue 中。咱们记为process1
。 - 遇到
Promise
,new Promise
间接执行,输入 7。then
被散发到微工作 Event Queue 中。咱们记为then1
。 - 又遇到了
setTimeout
,其回调函数被散发到宏工作 Event Queue 中,咱们记为setTimeout2
。
宏工作 Event Queue
微工作 Event Queue
setTimeout1
process1
setTimeout2
then1
- 上表是第一轮事件循环宏工作完结时各 Event Queue 的状况,此时曾经输入了 1 和 7。
- 咱们发现了
process1
和then1
两个微工作。 - 执行
process1
, 输入 6。 - 执行
then1
,输入 8。
好了,第一轮事件循环正式完结,这一轮的后果是输入 1,7,6,8。那么第二轮工夫循环从 setTimeout1
宏工作开始:
- 首先输入 2。接下来遇到了
process.nextTick()
,同样将其散发到微工作 Event Queue 中,记为process2
。new Promise
立刻执行输入 4,then
也散发到微工作 Event Queue 中,记为then2
。
宏工作 Event Queue
微工作 Event Queue
setTimeout2
process2
then2
- 第二轮事件循环宏工作完结,咱们发现有
process2
和then2
两个微工作能够执行。 - 输入 3。
- 输入 5。
- 第二轮事件循环完结,第二轮输入 2,4,3,5。
- 第三轮事件循环开始,此时只剩 setTimeout2 了,执行。
- 间接输入 9。
- 将
process.nextTick()
散发到微工作 Event Queue 中。记为process3
。 - 间接执行
new Promise
,输入 11。 - 将
then
散发到微工作 Event Queue 中,记为then3
。
宏工作 Event Queue
微工作 Event Queue
process3
then3
- 第三轮事件循环宏工作执行完结,执行两个微工作
process3
和then3
。 - 输入 10。
- 输入 12。
- 第三轮事件循环完结,第三轮输入 9,11,10,12。
整段代码,共进行了三次事件循环,残缺的输入为 1,7,6,8,2,4,3,5,9,11,10,12。
(请留神,node 环境下的事件监听依赖 libuv 与前端环境不完全相同,输入程序可能会有误差)
6. 写在最初
(1)js 的异步
咱们从最结尾就说 javascript 是一门单线程语言,不论是什么新框架新语法糖实现的所谓异步,其实都是用同步的办法去模仿的,牢牢把握住单线程这点十分重要。
(2)事件循环 Event Loop
事件循环是 js 实现异步的一种办法,也是 js 的执行机制。
(3)javascript 的执行和运行
执行和运行有很大的区别,javascript 在不同的环境下,比方 node,浏览器,Ringo 等等,执行形式是不同的。而运行大多指 javascript 解析引擎,是对立的。
(4)setImmediate
微工作和宏工作还有很多品种,比方 setImmediate
等等,执行都是有共同点的,有趣味的同学能够自行理解。
(5)最初的最初
- javascript 是一门单线程语言
- Event Loop 是 javascript 的执行机制
牢牢把握两个基本点,以认真学习 javascript 为核心,早日实现成为前端高手的平凡幻想!