1. 事件循环(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)的含意是,指定某个工作在主线程最早可得的闲暇工夫执行,意思就是不必再等多少秒了,只有主线程执行栈内的同步工作全副执行实现,栈为空就马上执行。举例说明:

//代码1console.log('先执行这里');setTimeout(() => {    console.log('执行啦')},0);复制代码
//代码2console.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)

传统的定时器咱们曾经钻研过了,接着咱们探索Promiseprocess.nextTick(callback)的体现。

Promise的定义和性能本文不再赘述,不理解的读者能够学习一下阮一峰老师的Promise。而process.nextTick(callback)相似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。

咱们进入正题,除了狭义的同步工作和异步工作,咱们对工作有更精密的定义:

  • macro-task(宏工作):包含整体代码script,setTimeout,setInterval
  • micro-task(微工作):Promise,process.nextTick

不同类型的工作会进入对应的Event Queue,比方setTimeoutsetInterval会进入雷同的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。(注册过程与上同,下文不再形容)
  • 接下来遇到了Promisenew 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
  • 遇到Promisenew Promise间接执行,输入7。then被散发到微工作Event Queue中。咱们记为then1
  • 又遇到了setTimeout,其回调函数被散发到宏工作Event Queue中,咱们记为setTimeout2

宏工作Event Queue

微工作Event Queue

setTimeout1

process1

setTimeout2

then1

  • 上表是第一轮事件循环宏工作完结时各Event Queue的状况,此时曾经输入了1和7。
  • 咱们发现了process1then1两个微工作。
  • 执行process1,输入6。
  • 执行then1,输入8。

好了,第一轮事件循环正式完结,这一轮的后果是输入1,7,6,8。那么第二轮工夫循环从setTimeout1宏工作开始:

  • 首先输入2。接下来遇到了process.nextTick(),同样将其散发到微工作Event Queue中,记为process2new Promise立刻执行输入4,then也散发到微工作Event Queue中,记为then2

宏工作Event Queue

微工作Event Queue

setTimeout2

process2

 

then2

  • 第二轮事件循环宏工作完结,咱们发现有process2then2两个微工作能够执行。
  • 输入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

  • 第三轮事件循环宏工作执行完结,执行两个微工作process3then3
  • 输入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为核心,早日实现成为前端高手的平凡幻想!