通过例题学习JS的Event Loop(事件循环)

前言

Event Loop大家都有所耳闻,但是,由于我们日常业务中并不需要写那么复杂的异步任务,所以很多人对Event Loop都一知半解,今天就讨论一下。

Event Loop概念

Event loop是一个JS引擎执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。

微任务队列和宏任务队列

微队列,microtask,也叫jobs。 另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:

  • process.nextTick (Node独有)
  • Promise的then的回调(注意,new Promise()的函数参数是同步代码,then的回调才是异步)
  • await下方的代码和赋值代码(注意,await后方的函数是同步代码)
  • Object.observe(草案已废弃,这里仅做记录)
  • MutationObserver

宏队列,macrotask,也叫tasks。 一些异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:

  • setTimeout的回调
  • setInterval的回调
  • setImmediate的回调
  • requestAnimationFrame (浏览器独有)
  • I/O
  • UI rendering (浏览器独有)

如果你致力于弄懂Event Loop,请背过这些任务名,至少要记住:then的回调、await下面的代码属于微队列,setTimeout的回调、setInterval的回调属于宏队列。要记牢。

调用栈

可以理解为JS引擎的工作台。任务会从宏任务队列或者微任务队列放到调用栈,然后再执行这些任务。

异步方法

setTimeout这些都是window的异步方法,注意,异步方法本身是同步执行的,也就是说,下面代码中,setTimeout是立即执行的,但是setTimeout的回调函数就不会立即执行,而是1秒之后执行。

console.log(1);
setTimeout(() => {}, 1000);
console.log(2);

JavaScript代码执行的具体流程

  1. 执行全局Script同步代码,这些同步代码有一些是同步语句(new Promise()的函数参数是同步代码),有一些是异步语句(比如setTimeout等);
  2. 异步语句的回调任务会根据上方介绍的规定,放入微任务队列或宏任务队列。
  3. 全局Script代码执行完毕后,调用栈Stack会清空;
  4. 从微队列(microtask queue)中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
  5. 继续取出位于微队列队首的任务,放入调用栈Stack中执行,以此类推,直到把微队列中的所有任务都执行完毕。注意,如果在执行微队列任务的过程中,如果又产生了新的微任务,那么会加入到微任务队列的末尾,也会在这个周期被调用执行;
  6. 微任务队列中的所有任务都执行完毕,此时微任务队列为空队列,调用栈Stack也为空;
  7. 取出宏队列(macrotask queue)中位于队首的任务,放入Stack中执行;
  8. 继续观察微队列是否有任务,如果有,则全部执行微队列任务;
  9. 重复7-8步骤;
  10. 宏队列执行完毕后,调用栈Stack为空;
  11. 重复第4-8个步骤;
  12. ......

通俗说:微队列就是牛逼,优先级就是高,每执行一串微队列,则执行一个宏队列任务,然后又去执行微队列,重复重复再重复下去。。。这就好比VIP通道跟普通通道,VIP全部优先,服务完了之后看看普通通道有没有人,有则服务一个,然后赶紧看看VIP是不是又来人了,没有的话再服务一个普通人,然后再看看VIP是不是又来人了。周而复始。

例题1

试着自己回答一下这道题,求打印顺序:

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
})

setTimeout(() => {
  console.log(6);
})

console.log(7);

解答:

第1步,console.log(1);,打印1

位置 任务
调用栈 console.log(1);
微任务
宏任务

第2步,setTimeout,不打印

位置 任务
调用栈 setTimeout
微任务
宏任务 setTimeout的回调

第3步,new Promise的函数参数,注意,Promise的函数参数是同步执行的,打印4

位置 任务
调用栈 new Promise的函数参数
微任务 new Promise的then的回调
宏任务 setTimeout 的回调

第4步,第二个setTimeout,不打印

位置 任务
调用栈 第二个setTimeout
微任务 new Promise的then的回调
宏任务 setTimeout 的回调,第二个setTimeout的回调

第5步,console.log(7);,打印7

位置 任务
调用栈 console.log(7);
微任务 new Promise的then的回调
宏任务 setTimeout 的回调,第二个setTimeout的回调

第6步,第一个微任务,即new Promise的then的回调,打印5

位置 任务
调用栈 new Promise的then的回调
微任务
宏任务 setTimeout的回调,第二个setTimeout的回调

第7步,第一个宏任务,即setTimeout的回调,打印2,同时,Promise.resolve().then()的回调进入微队列

位置 任务
调用栈 setTimeout的回调
微任务 Promise.resolve().then()的回调
宏任务 第二个setTimeout的回调

第8步,第一个微任务,即Promise.resolve().then()的回调,打印3

位置 任务
调用栈 Promise.resolve().then()的回调
微任务
宏任务 第二个setTimeout的回调

第9步,第一个宏任务,即第二个setTimeout的回调,打印6

位置 任务
调用栈 第二个setTimeout的回调
微任务
宏任务

是不是跟你演算的一样呢?

例题2

考察then方法链的执行顺序:

new Promise((resolve, reject) => {
  console.log(1)
  resolve(2)
}).then((data) => {
  // 1号回调
  console.log(data);
  return 3
}).then((data) => {
  // 2号回调
  console.log(data);
})

new Promise((resolve, reject) => {
  console.log(5)
  resolve(6)
}).then((data) => {
  // 3号回调
  console.log(data);
  return 7;
}).then((data) => {
  // 4号回调
  console.log(data);
})

你以为会打印1 2 3 5 6 7吗?错!你以为会打印1 5 2 3 6 7吗?也错!

  1. new Promise的函数参数是同步代码,所以先打印1。同时将第一个then的回调(1号回调)放入微队列。

  2. 同理,打印5,将第二个new Promise的第一个then的回调(3号回调)放入微队列。

  3. 执行1号回调,打印2,将2号回调放入微队列。

  4. 执行3号回调,打印6,将4号回调放入微队列。

  5. 执行2号回调,打印3。

  6. 执行4号回调,打印7。

所以结果是1 5 2 6 3 7,你演算对了吗?

例题3

有如下代码:

setTimeout(() => {
  console.log(1)
  setTimeout(() => {
    console.log(2)
  })
})

不许修改这段代码,只允许在外层作用域添加代码,如何实现在打印1跟打印2中间插入打印3

其实这个题跟例题2类似,考察的也是基本知识:

setTimeout(() => {
  console.log(1)
  setTimeout(() => {
    console.log(2)
  })
})

setTimeout(() => {
  console.log(3)
  setTimeout(() => {
    console.log(4)
  })
})

结果:打印 1 3 2 4。

原因:同例题2。

例题4

这次考察对async/await的执行顺序的理解:

console.log('script start');

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

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

async1()

setTimeout(() => {
    console.log('setTimeout')
}, 0)

new Promise((resolve, reject) => {
    console.log('promise start');
    resolve()
})
.then(() => console.log('promise end'))

console.log('script end')
  1. 打印script start

  2. 执行async1()async1的第一行是await async2(),这一句应拆开考察,其中async2()会同步执行,同时把await下方的所有代码放到微队列。

  3. 执行setTimeout,不打印。同时把回调放入宏队列。

  4. 执行new Promise的函数参数,打印promise start,同时把then的回调放入微队列 ,此时,微队列有2块代码,分别是await下面的代码(即console.log('async1 end');),以及console.log('promise end')

  5. console.log('script end'),打印script end

  6. 执行微队列第一个,也就是打印async1 end

  7. 执行微队列第二个,也就是打印promise end

  8. 执行宏任务,也就是打印setTimeout

注意:低版本的Chrome浏览器(大约是70版本之前)会先打印promise end,后打印async1 end,原因我忘却了,大致是Chrome的早期实现里有Bug,其实原因也不重要,既然高版本的浏览器纠正归来了,就行了。

例题5

证明await的赋值操作是异步任务:

let x = 5;
let y;
function ret() {
    x += 1;
    console.log('x是', x);
    console.log('y是', y);
    return x;
}
async function a() {
    y = await ret();
    console.log(y);
}
async function b() {
    y = await ret();
    console.log(y);
}

a()
b()

得到:

x是 6
y是 undefined
x是 7
y是 undefined
6
7

我们用反证法,假定await赋值是同步操作,那么a()y = await ret()会同步执行,之后才执行b()ret(),此时y有值,不应该是undefined,但事实是undefined,说明await赋值是异步操作。

例题6

证明new Promise()的函数参数和await后面的函数是同步任务,证明很简单:

var i;
for (i = 0; i < 20; i++) {
  new Promise(resolve => {
    console.log(i)
  })
}

也是反证法,假如new Promise()的函数参数是异步任务,那么应该像setTimeout一样打印20个20,然而事实上会打印等差数列。

注意,不要用for(let i = 0;)这种写法,因为这会形成一个特殊作用域,不能反证出我们的结论。

function log(x) { console.log(x) }
async function a(i) {
  await log(i)
}
var i;
for (i = 0; i < 20; i++) {
    a(i);
}
function log(x) { console.log(x) }
var i;
for (i = 0; i < 20; i++) {
    async function a() {
        await log(i)
    }
    a();
}

同理,因为上面2段代码的结果也都是等差数列,所以await后面的语句是同步任务。

例题7

证明await会暂停for循环:

var i;
function ret() {
    console.log('ret里的i', i);
    return 100;
}
async function a() {
    for (i = 0; i < 20; i++) {
        await ret();
        console.log(i);
    }
}
a();

得到等差数列。

虽然微队列放入了20个任务,但是由于await会暂停循环,也就是说i并不会像setTimeout时一样自顾自的增长为20,而是会等待每一个微任务执行完毕,由此证明await会暂停for循环。

相反,setTimeout不会阻止for循环:

var i;
function ret() {
    console.log('ret里的i', i);
    return 100;
}
async function a() {
    for (i = 0; i < 20; i++) {
        setTimeout(() => {
            ret();
            console.log(i);
        });
    }
}
a();

总结

  1. 随时遇到异步,随时放入队列,这是起码的准则。

  2. 先同步任务,同步任务有异步回调的时候,根据规则放入队列。

  3. 同步任务都完成了就执行微队列。有异步则继续放入队列。

  4. 微队列都完成了就执行宏队列第一个。有异步则继续放入队列。

  5. 观察微队列,有则执行,没则执行宏队列第二个。

  6. 重复3/4/5步骤。

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