JavaScript的事件循环Event Loop

事件循环Event Loop

JavaScript语言的一大特点就是单线程,作为脚本语言,避免复杂性。因为如果是多线程的话,一个线程如果在某个DOM节点上添加内容,另外一个线程删除了这个节点,那么浏览器就要判断哪个线程为主,增加了程序的复杂性。这就意味着单线程作为javascript的核心标准,将一直沿用下去。

js实现异步的具体解决方案

  • 同步代码直接执行
  • 异步函数到了指定时间再放到异步队列
  • 同步执行完毕,异步队列轮询执行。

什么是事件轮询Event loop

  • 精简版:

当第一个异步函数执行完之后,再到异步队列监视。一直不断循环往复,所以叫事件轮询。

  • 详细版:

js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。

事实上,事件轮询与宏任务和微任务密切相关。

事件和回调函数

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当异步任务有了结果时,就会在任务队列上添加一个事件(即回调函数),当主线程开始执行异步任务,就是执行对应的回调函数。

"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

分析async/await 和 promise 的执行顺序

async 做一件什么事情

一句话概括: 带 async 关键字的函数,它使得你的函数的返回值必定是 promise 对象。

也就是,如果async关键字函数返回的不是promise,会自动用 Promise.resolve() 包装。

如果async关键字函数显式地返回promise,那就以你返回的promise为准。

这是一个简单的例子,可以看到 async 关键字函数和普通函数的返回值的区别:

async function fn1() {
  return '123';
}

function fn2() {
  return '123';
}

console.log(fn1); //Promise {<resolved>: "123"}
console.log(fn2); //"123"

关于async关键字还有那些要注意的?

  • 在语义上要理解,async表示函数内部有异步操作
  • 另外注意,一般 await 关键字要在 async 关键字函数的内部,await 写在外面会报错。

await 在等什么?

一句话概括: await等的是右侧「表达式」的结果。

也就是说,右侧如果是函数,那么函数的return值就是「表达式的结果」。

右侧如果是一个 'hello' 或者什么值,那表达式的结果就是 'hello'。

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

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

async1();
console.log('script start');

// async1 start
// async2
// script start
// async1 end

await会让出线程,阻塞后面的代码, 上面例子中, async2先于script start被打印,是因为await语句行从右向左的。先打印async2,后打印的 script start。

await 等到之后,做了一件什么事情?

从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。

很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。

由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。所以对于本题中的

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

等价于

async function async1() {
    console.log('async1 start');
    Promise.resolve(async2()).then(() => {
                console.log('async1 end');
        })
}

题目分析一

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('代码执行结束');

//【马上执行for循环啦 --- 代码执行结束 --- 执行then函数啦 --- 定时器开始啦】

宏任务

(macro)task(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

分类

事件 浏览器 Node
I/O
setTimeout
setInterval
setImmediate
requestAnimationFrame

特性:

  • 宏任务所处的队列就是宏任务队列
  • 第一个宏任务队列中只有一个任务:执行主线程上的JS代码;如果遇到上方表格中的异步任务,会创建出一个新的宏任务队列,存放这些异步函数执行完成后的回调函数。
  • 宏任务队列可以有多个
  • 宏任务中可以创建微任务,但是在宏任务中创建的微任务不会影响当前宏任务的执行。
  • 当一个宏任务队列中的任务全部执行完后,会查看是否有微任务队列,如果有就会优先执行微任务队列中的所有任务,如果没有就查看是否有宏任务队列

(macro)task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)

微任务

microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。

分类

事件 浏览器 Node
process.nextTick
MutationObserver
Promise.then catch finally

特性:

  • 微任务所处的队列就是微任务队列
  • 在上一个宏任务队列执行完毕后,如果有微任务队列就会执行微任务队列中的所有任务
  • new promise((resolve)=>{ 这里的函数在当前队列直接执行 }).then( 这里的函数放在微任务队列中执行 )
  • 微任务队列上创建的微任务,仍会阻碍后方将要执行的宏任务队列
  • 由微任务创建的宏任务,会被丢在异步宏任务队列中执行

microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)

运行机制

在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。

我们只需记住当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

在当前的微任务没有执行完成时,是不会执行下一个宏任务的。

题目分析二

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})

console.log(2)

setTimeout就是作为宏任务来存在的,而Promise.then则是具有代表性的微任务,上述代码的执行顺序就是按照序号来输出的。
所有会进入的异步都是指的事件回调中的那部分代码
也就是说new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。
在同步代码执行完成后才回去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行。
所以就得到了上述的输出结论1、2、3、4。

EXP1: 在主线程上添加宏任务与微任务

// 宏任务1
console.log('-------start--------');

setTimeout(() => {
  // 宏任务2
  console.log('setTimeout');  // 将回调代码放入另一个宏任务队列
}, 0);

new Promise((resolve, reject) => {
  for (let i = 0; i < 5; i++) {
    console.log(i);
  }
  resolve()
}).then(()=>{
  // 微任务1
  console.log('Promise实例成功回调执行'); // 将回调代码放入微任务队列
})

//-------start--------
// 0
// 1
// 2
// 3
// 4
// Promise实例成功回调执行
// setTimeout

步骤分析

  1. 主线程同步代码开始执行(理解为初始宏任务),标记为【宏任务1】
    • 执行console.log('-------start--------');
    • 当遇到setTimeout函数时,将回调函数放入宏任务队列,标记为【宏任务2】
    • 遇到promise,同步运行实例化代码,for循环内输出 console.log(i)
    • 遇到.then,将回调代码放入微任务队列,标记为【微任务1】
    • 此时初始宏任务执行完毕,检查微任务队列不为空,执行微任务队列里的事件
  2. 执行【微任务1】console.log('Promise实例成功回调执行')
    • 微任务队列为空,继续执行下一轮宏任务
  3. 执行【宏任务2】
    执行:console.log('setTimeout');
  4. 代码结束

由EXP1,我们可以看出,当JS执行完主线程上的代码,会去检查在主线程上创建的微任务队列,执行完微任务队列之后才会执行宏任务队列上的代码

EXP2: 在主线程上添加宏任务与微任务

//宏任务1

//宏任务2
setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve();
  console.log(1);
}).then(_ => {
  //微任务1
  console.log(3);
  Promise.resolve().then(_ => {
    console.log('before timeout')
  }).then(_ => {
    Promise.resolve().then(_ => {
      //微任务2
      console.log('also before timeout')
    })
  })
})

console.log(2);

// 1
// 2
// 3
// before timeout
// also before timeout
// 4

执行顺序:主线程 => 主线程上创建的微任务1 => 微任务1上创建的微任务2 => 主线程上创建的宏任务

步骤分析

  1. 主线程同步代码执行,(宏任务队列 【宏任务1】)
    • 遇到setTimeout,推入【宏任务2】
    • 实例化promise,执行console.log(1)
    • 将promise.then推入微任务队列,标记为【微任务1】
    • 执行console.log(2)
  2. 【宏任务1】执行完毕,检查微任务队列不为空:【微任务1】,执行【微任务1】
    • 执行console.log(3)
    • 实例化promise, 执行console.log('before timeout')
    • 将promise.then推入微任务队列,标记为【微任务2】
    • 此时【微任务1】执行完毕,但此时微任务队列仍不为空,存在【微任务2】
  3. 执行【微任务2】
    • 实例化promise,执行 console.log('also before timeout')
    • 代码执行完毕
  4. 结束

EXP3: 宏任务中创建微任务

// 同步代码 宏任务 1

setTimeout(() => {
  // 宏任务 2
  console.log('timer_1');

  setTimeout(() => {
    //宏任务4
    console.log('timer_3');
  }, 0);

  new Promise(resolve => {
    resolve();
    console.log('new promise');
  }).then(() => {
    // 微任务 1
    console.log('promise then');
  })
}, 0);


setTimeout(() => {
  //宏任务3
  console.log('timer_2');
}, 0);

console.log('========== Sync queue ==========');

输出顺序为:

========== Sync queue ==========
timer_1
new promise
promise then
timer_2
timer_3

分析步骤

  1. 主线程同步代码执行,(宏任务队列 【宏任务1】)
    • 遇到setTimeout,推入宏任务队列,标记为【宏任务2】
    • 往下执行,遇到setTimeout,推入宏任务队列,标记为【宏任务3】
    • 执行console.log('========== Sync queue ==========')
    • 此时【宏任务1】执行完毕,微任务队列为空,继续往下执行
  2. (宏任务队列【宏任务2】)已经进入队列,开始执行
    • 执行console.log('timer_1')
    • 遇到setTimeout,推入宏任务队列,标记为【宏任务4】
    • 继续执行,初始化promise,console.log('new promise')
    • 遇到promise.then,推入微任务队列,标记为【微任务1】
  3. 宏任务队列【宏任务2】执行完毕,检查微任务队列:【微任务1】,队列不为空
    • 执行微任务队列【微任务1】console.log('promise then')
  4. 微任务队列执行完毕,为空,开始执行【宏任务3】
    • console.log('timer_2')
    • 【宏任务3】执行完毕,检查微任务队列,发现为空,继续执行下一轮宏任务
  5. 执行【宏任务4】
    • 执行console.log('timer_3');

EXP4:微任务队列中创建的宏任务

// 宏任务1
new Promise((resolve) => {
  console.log('new Promise(macro task 1)');
  resolve();
}).then(() => {
  // 微任务1
  console.log('micro task 1');
  setTimeout(() => {
    // 宏任务3
    console.log('macro task 3');
  }, 0)
})

setTimeout(() => {
  // 宏任务2
  console.log('macro task 2');
}, 1000)

console.log('========== Sync queue(macro task 1) ==========');

输出顺序为:

new Promise(macro task 1)
Sync queue(macro task 1) ==========
micro task 1
macro task 3
macro task 2
  1. 主线程执行同步代码(宏任务队列第一个任务)
    • 实例化Promise,console.log('new Promise(macro task 1)'
    • 遇到.then,放入微任务队列,标记为【微任务1】
    • 跳出promise,往下执行,遇到setTimeout,推入【宏任务2】
    • 往下执行,console.log('========== Sync queue(macro task 1) ==========');
  2. 此时宏任务队列的【宏任务1】执行完毕,此时有微任务【微任务1】
    • 放入执行栈,输出 console.log('micro task 1')
    • 遇到setTimeout,放入宏任务队列,标记为【宏任务3】
    • 此时【宏任务1】执行完毕,微任务队列为空
  3. 执行【宏任务2】
    • 异步事件,1000毫秒后推入执行栈,console.log('macro task 2')
  4. 【宏任务2】执行完毕,微任务队列为空,执行【宏任务3】
    • 异步事件,立刻执行console.log('macro task 3');
  5. 代码结束

(由于宏任务2的等候时间大于宏任务3,因此,macro task 3要优先于macro task 2输出)

EXP总结

微任务队列优先于宏任务队列执行,微任务队列上创建的宏任务会被后添加到当前宏任务队列的尾端,微任务队列中创建的微任务会被添加到微任务队列的尾端。只要微任务队列中还有任务,宏任务队列就只会等待微任务队列执行完毕后再执行。

//宏任务1
console.log('======== main task start ========');
new Promise(resolve => {
  console.log('create micro task 1');
  resolve();
}).then(() => {
  // 微任务1
  console.log('micro task 1 callback');
  setTimeout(() => {
    //宏任务3
    console.log('macro task 3 callback');
  }, 0);
})

console.log('create macro task 2');


setTimeout(() => {
  //宏任务2
  console.log('macro task 2 callback');
  new Promise(resolve => {
    console.log('create micro task 3');
    resolve();
  }).then(() => {
    // 微任务3
    console.log('micro task 3 callback');
  })
  console.log('create macro task 4');
  setTimeout(() => {
    // 宏任务4
    console.log('macro task 4 callback');
  }, 0);
}, 0);

new Promise(resolve => {
  console.log('create micro task 2');
  resolve();
}).then(() => {
  // 微任务2
  console.log('micro task 2 callback');
})

console.log('======== main task end ========');
======== main task start ========
create micro task 1
create macro task 2
create micro task 2
======== main task end ========
micro task 1 callback
micro task 2 callback
macro task 2 callback
create micro task 3
create macro task 4
micro task 3 callback
macro task 3 callback
macro task 4 callback

分析步骤

  1. 主线程执行同步代码(宏任务队列第一个任务)
    • 执行console.log('======== main task start ========')
    • 实例化Promise,执行console.log('create micro task 1')
    • 将Promise.then里面的微任务推入微任务队列,标记为【微任务1】
    • 往下执行,console.log('create macro task 2')
    • 遇到setTimeout,推入【宏任务2】
    • 实例化Promise,console.log('create micro task 2')
    • 将Promise.then里面的微任务推入微任务队列,标记为【微任务2】
    • 执行console.log('======== main task end ========')
  2. 宏任务【宏任务1】执行完毕,查看微任务队列,发现不为空,【微任务1】,【微任务2】
    • 执行【微任务1】,console.log('micro task 1 callback')
    • 遇到【微任务1】里面的setTimeout,推入宏任务队列,标记为【宏任3】
    • 执行【微任务2】,console.log('micro task 2 callback');
  3. 微任务执行完毕,微任务队列为空,开始执行下一轮宏任务【宏任务2】
    • 执行【宏任务2】中,console.log('macro task 2 callback')
    • 遇到Promise,实例化promise,执行console.log('create micro task 3');
    • 将promise.then推入微任务队列,标记为【微任务3】
    • 继续往下执行,console.log('create macro task 4');
    • 遇到setTimeout,推入宏任务队列,标记为【宏任务4】
  4. 【宏任务2】执行完毕,查看微任务队列,发现不为空,【微任务3】
    • 执行微任务3, console.log('micro task 3 callback');
  5. 微任务执行完毕,微任务队列为空,开始执行下一轮宏任务【宏任务3】
    • 执行 console.log('macro task 3 callback')
  6. 【宏任务3】执行完毕,微任务队列为空,开始执行下一轮宏任务【宏任务4】
    • 执行 console.log('macro task 4 callback')
  7. 任务队列均执行完毕,结束

Notes from:

https://segmentfault.com/a/1190000017224799

http://www.ruanyifeng.com/blog/2014/10/event-loop.html

https://blog.csdn.net/u012925833/article/details/89306184

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

推荐阅读更多精彩内容