乐趣区

关于javascript:js事件循环机制eventloop

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

// 代码 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)

传统的定时器咱们曾经钻研过了,接着咱们探索 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 为核心,早日实现成为前端高手的平凡幻想!

退出移动版