总方针是先同步再异步,异步中先微任务,在宏任务。javascript
本文的目的就是要保证你完全弄懂javascript的执行机制,若是读完本文还不懂,能够揍我。前端
不论你是javascript新手仍是老鸟,不管是面试求职,仍是平常开发工做,咱们常常会遇到这样的状况:给定的几行代码,咱们须要知道其输出内容和顺序。由于javascript是一门单线程语言,因此咱们能够得出结论:java
看到这里读者要打人了:我难道不知道js是一行一行执行的?还用你说?稍安勿躁,正由于js是一行一行执行的,因此咱们觉得js都是这样的:node
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是按照语句出现的顺序执行这个理念,我自信的写下输出结果:es6
//"定时器开始啦" //"立刻执行for循环啦" //"执行then函数啦" //"代码执行结束"复制代码
去chrome上验证下,结果彻底不对,瞬间懵了,说好的一行一行执行的呢?面试
咱们真的要完全弄明白javascript的执行机制了。ajax
javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。因此一切javascript版的"多线程"都是用单线程模拟出来的,一切javascript多线程都是纸老虎!chrome
既然js是单线程,那就像只有一个窗口的银行,客户须要排队一个一个办理业务,同理js任务也要一个一个顺序执行。若是一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如咱们想浏览新闻,可是新闻包含的超清图片加载很慢,难道咱们的网页要一直卡着直到图片彻底显示出来?所以聪明的程序员将任务分为两类:promise
当咱们打开网站时,网页的渲染过程就是一大堆同步任务,好比页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。关于这部分有严格的文字定义,但本文的目的是用最小的学习成本完全弄懂执行机制,因此咱们用导图来讲明:
导图要表达的内容用文字来表述的话:
执行流程:主线程从"任务队列"中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环。此机制具体以下:主线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查microtask队列是否为空(执行完一个任务的具体标志是函数执行栈为空),若是不为空则会一次性执行完全部microtask。而后再进入下一个循环去任务队列中取下一个任务执行。
咱们不由要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
说了这么多文字,不如直接一段代码更直白:
let data = []; $.ajax({ url:www.javascript.com, data:data, success:() => { console.log('发送成功!'); } }) console.log('代码执行结束');复制代码
上面是一段简易的ajax
请求代码:
success
。console.log('代码执行结束')
。success
进入Event Queue。success
并执行。相信经过上面的文字和代码,你已经对js的执行顺序有了初步了解。接下来咱们来研究进阶话题: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
函数,很慢,很是慢,计时仍在继续。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毫秒。有兴趣的同窗能够自行了解。
上面说完了setTimeout
,固然不能错过它的孪生兄弟setInterval
。他俩差很少,只不事后者是循环的执行。对于执行顺序来讲,setInterval
会每隔指定的时间将注册的函数置入Event Queue,若是前面的任务耗时过久,那么一样须要等待。
惟一须要注意的一点是,对于setInterval(fn,ms)
来讲,咱们已经知道不是每过ms
秒会执行一次fn
,而是每过ms
秒,会有fn
进入Event Queue。一旦setInterval
的回调函数fn
执行时间超过了延迟时间ms
,那么就彻底看不出来有时间间隔了。这句话请读者仔细品味。
传统的定时器咱们已经研究过了,接着咱们探究Promise
与process.nextTick(callback)
的表现。
Promise
的定义和功能本文再也不赘述,不了解的读者能够学习一下阮一峰老师的Promise。而process.nextTick(callback)
相似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。
咱们进入正题,除了广义的同步任务和异步任务,咱们对任务有更精细的定义:
不一样类型的任务会进入对应的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()
,当即执行。then
在微任务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') }) })复制代码
第一轮事件循环流程分析以下:
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
process1
和then1
两个微任务。process1
,输出6。then1
,输出8。好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1
宏任务开始:
process.nextTick()
,一样将其分发到微任务Event Queue中,记为process2
。new Promise
当即执行输出4,then
也分发到微任务Event Queue中,记为then2
。宏任务Event Queue
微任务Event Queue
setTimeout2
process2
then2
process2
和then2
两个微任务能够执行。process.nextTick()
分发到微任务Event Queue中。记为process3
。new Promise
,输出11。then
分发到微任务Event Queue中,记为then3
。宏任务Event Queue
微任务Event Queue
process3
then3
process3
和then3
。整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不彻底相同,输出顺序可能会有偏差)
咱们从最开头就说javascript是一门单线程语言,不论是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,紧紧把握住单线程这点很是重要。
事件循环是js实现异步的一种方法,也是js的执行机制。
执行和运行有很大的区别,javascript在不一样的环境下,好比node,浏览器,Ringo等等,执行方式是不一样的。而运行大多指javascript解析引擎,是统一的。
微任务和宏任务还有不少种类,好比setImmediate
等等,执行都是有共同点的,有兴趣的同窗能够自行了解。
紧紧把握两个基本点,以认真学习javascript为中心,早日实现成为前端高手的伟大梦想!