js之事件循环

一道面试题

说出下面代码的运行结果,并说明原因:

async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}

async function async2(){
    console.log('async2')
}

console.log('script start')

setTimeout(function(){
    console.log('setTimeOut')
}, 0)

async1()

new Promise(function(resolve){
    console.log('promise1') 
    resolve()
}).then(function(){
    console.log('promise2') 
})

console.log('script end')

先贴一下在浏览器里的运行的结果(如果跟你的思路一模一样的话,大佬请直接Ctrl+F4):

//script start
//async1 start
//async2
//promise1
//script end
//async1 end
//promise2
//setTimeOut

如果跟你的思路不一样的话也不用担心,我们从简单的开始一点点剖析这道面试题。

单线程
首先我们都知道,JavaScript是一门单线程的语言,所谓单线程指的是在JavaScript引擎中负责解释和执行代码的线程只有一个,通常称为主线程。那么为什么JavaScript必须是单线程的语言,而不能像他的老大哥Java一样,手动开启多个线程呢?

因为这是由于JavaScript所运行的浏览器环境决定,他只能是单线程的。试想一下,如果JavaScript能开启多个线程,页面上有一个div,我们同时在多个线程中来改变这个div中的内容,那么最终这个div会变成什么样子谁也确定不了,最后只能听天由命,看哪个线程是最后一个运行结束的。

因此多线程带来了很多的不确定性,为了避免这种问题,JavaScript必须是单线程。

可能有的同学又会说了,JavaScript不是可以通过Web Worker开启多线程么?是的,Web Worker是可以开启另一个线程,但是这个新开线程的功能被限制了,只能做一些消耗CPU的逻辑运算等,数据传输也是通过回调的方式来进行,不会阻塞主线程的执行;而且最最重要的是,Web Worker不能来操作dom,笔者经过尝试发现,在新开的线程中甚至都不能获取到document和window对象。

所以还是没有改变JavaScript是单线程运行这一核心原则。当然,虽然JavaScript是单线程运行的,但是还是存在其他线程的;例如:处理Ajax请求的线程、定时器的线程、读写文件的线程(nodejs中)等。

同步任务和异步任务
因为JavaScript是单线程运行的,所有的任务只能在主线程上排队执行;但是如果某个任务特别耗时,比如Ajax请求一个接口,可能1s返回结果,也可能10s才返回,有很多的不确定因素(网络延迟等);如果这些任务也放到主线程中去,那么会阻塞浏览器(用户除了等,不能进行其他操作)。

于是,浏览器就把这些任务分派到异步任务队列中去,并且跟他们说:你们自己去后台玩儿,等你们好了再过来通知我!先来看简单的例子来理解一下同步和异步任务:

console.log('start')

setTimeout(function() {
    console.log('setTimeout')
}, 0)

console.log('end')

当主线程执行到setTimeout的时候,虽然是延迟了0s,但是并不会马上来运行,而是放到异步任务队列中,等下面的同步任务队列执行完了,再来执行异步队列中的任务,所以运行结果是:start、end、setTimeout。

但如果同步任务中有特别耗时的操作,阻塞了 setTimeout 的定时执行,那么 setTimeout 就不会按时来完成。来看下面的例子:

console.log('start')
console.time('now')
let list = []

setTimeout(function() {
    console.timeEnd('now')
}, 1000)


for(let i = 0;i<9999999;i++){
    let now = new Date()
    list.push(i)
}

虽然我们让 setTimeout 1s后执行,但是for循环占用了太多的线程资源,实际执行会在2s后。所以 事件循环 的流程大致如下:

所有任务都在主线程上执行,形成一个执行栈。
主线程发现有异步任务,就在“任务队列”之中加入一个任务事件。
一旦“执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”(先进先出原则)。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
主线程不断重复上面的第三步,这样的一个循环称为事件循环。

宏任务与微任务
如果任务队列中有多个异步任务,那么先执行哪个任务呢?于是在异步任务中,也进行了等级划分,分为宏任务(macrotask)和微任务(microtask);不同的API注册的任务会依次进入自身对应的队列中,然后等待事件循环将它们依次压入执行栈中执行。

宏任务包括:
script(整体代码)
setTimeout, setInterval, setImmediate,
I/O
UI rendering

微任务包括:
process.nextTick
Promise
Object.observe(已废弃)
MutationObserver(html5新特性)

我们可以把整体的JS代码也看成是一个宏任务,主线程也是从宏任务开始的。我们把上面事件循环的步骤更新一下:

执行一个宏任务
执行过程中如果遇到微任务就加入微任务队列,遇到宏任务就加入宏任务队列
宏任务执行完毕后,检查当前微任务队列,如果有,就依次执行(一轮事件循环结束)

开始下一个宏任务


让我们来看一个例子:

console.log('start')

setTimeout(function() {
    console.log('timeout');
}, 0)

new Promise(function(resolve) {
    console.log('promise');
    //注意这边调用resolve
    //不然then方法不会执行
    resolve()
}).then(function() {
    console.log('then');
})

console.log('end');

分析一下执行流程:

start
promise
end
then
timeout

我们把Promise进行一下改变,看一下下面的例子:

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
async1()
console.log('script end')

刚开始我们会想当然的认为执行顺序是: async1 start –> async2 –> async1 end –> script end 。但是当真正理解了async函数的本质后,我们知道async函数还是基于Promise的一些封装,而Promise是属于微任务的一种;因此会把 await async2() 后面的所有代码放到Promise的then回调函数中去,因此,如果把上面代码进行如下改写,会好理解很多:

async function async1() {
    console.log('async1 start')
    new Promise(function(resolve){
        console.log('async2')
        resolve()
    }).then(function(){
        console.log('async1 end')
    })
}
async1()
console.log('script end')

根据上面对微任务的理解, console.log('async1 end') 会放到微任务队列中,所以实际执行顺序是: async1 start –> async2 –> script end –> async1 end 。

最后来看那道面试题,相信已经不难理解了。

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeOut

(网络资源整理而来)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容