【前端】难以理解的Event loop

427 阅读10分钟

event loop 较难理解,面试中经常出现考察event loop执行顺序的问题,本文详细讲解event loop。

Event loop定义

1.这是 w3c 的定义

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section 为了协调事件,用户交互,脚本,渲染,网络等等,用户代理(:就是浏览器)必须使用本节中描述的事件循环。(:没有官方中文,用翻译软件翻译)

2.这是mdn的定义

JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务

3.这是node.js的定义

事件循环是 Node.js 处理非阻塞 I/O 操作的机制——尽管 JavaScript 是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中去。 例题1:

// 回答输出顺序
setTimeout(() => {
	console.log('setTimeout异步')
}, 0)
console.log('同步')

答案:

同步
setTimeout异步

回答错误的同学建议先复习一下异步JavaScript

例题2:

// 回答输出顺序
setTimeout(() => {
	console.log('setTimeout异步')
}, 0)
Promise.resolve().then(() =>{
    console.log('promise异步')
})
console.log('同步')

答案:

同步
promise异步
setTimeout异步

代码promise异步任务代码写在setTimeout异步任务代码之后,执行结果却是promise异步任务先执行setTimeout异步任务后执行,这是由event loop控制的,event loop控制异步任务回调的执行顺序。

task、microtask定义

event loop把异步任务分为task(:也有说法是macrotask,不过我没有在w3c和mdn中找到macrotask的说法和定义,下文统一用task)和microtask.

task

大多数异步操作都是task,例如:Event事件(click,mousemove等监听事件),fetch(网络请求),Parsing(浏览器对html的解析),Dom操作(dom的增删等),定时器(setTimeout/setInterval)等(:记住microtask就可以,除了microtask就是task,microtask好记)。

浏览器执行代码的时候遇到task会将task推入到一个task queue队列中,等同步代码都执行完,浏览器会在合适的时机按顺序依次执行task quque内的内容,直到清空task queue队列。
task queue队列执行中及清空后,新产生的task推入一个新的task queue中,等待浏览器的下次执行(:这段不是很严谨,不过代码的执行顺序是高度等效的,严谨的解释请参考w3c)。

// 伪代码近似表示
const taskQueue = [] // task queue队列
// 遇到task任务就加到队列里
taskQueue.push(() => {console.log('task任务1')})
taskQueue.push(() => {console.log('task任务2')})
taskQueue.push(() => {console.log('task任务3')})
while(taskQueue.length) {
  // 按顺序一个一个执行
	 taskQueue.shift()()     
}

例题:

// 回答输出顺序
console.log('同步1') // 同步任务直接执行
setTimeout(() => {
	console.log('setTimeout异步1') // 丢到 task queue里的第1个task
}, 0)
setTimeout(() => {
	console.log('setTimeout异步2') // 丢到 task queue里的第2个task
}, 0)
setTimeout(() => {
	console.log('setTimeout异步3') // 丢到 task queue里的第3个task
}, 0)
console.log('同步2') // 同步任务直接执行
// 现在执行task queue!

答案:

// 先执行同步任务
同步1
同步2
// 后依次执行task queue任务
setTimeout异步1
setTimeout异步2
setTimeout异步3

microtask

w3c:

A microtask is a task that is originally to be queued on the microtask queue rather than a task queue. 微任务是原本要在微任务队列而不是任务队列上排队的任务

mdn:

起初微任务和任务之间的差异看起来不大。它们很相似;都由位于某个队列的 JavaScript 代码组成并在合适的时候运行。但是,只有在迭代开始时队列中存在的任务才会被事件循环一个接一个地运行,这和处理微任务队列是殊为不同的。

单纯的microtask执行在表现上和task区别不大,microtask 任务推入microtask queue,执行完同步代码后,在依次执行microtask queue内的回调,但是microtask queue 执行过程中产生的新microtask 会直接推入当前microtask queue最后被执行,不需要像task queue一样等待浏览器下次执行。

例题:

Promise.resolve().then(() => {
    console.log('promise1')
})
Promise.resolve().then(() => {
    console.log('promise2')
}).then(() => {
    console.log('promise4')
})
Promise.resolve().then(() => {
    console.log('promise3')
})
console.log('同步1')

答案:

同步1
promise1
promise2
promise3
promise4

解析:

// 初始microtask queue 为空[]
/* 
微任务:() => {
    console.log('promise1')
}入队,队列[
    () => {
        console.log('promise1')
    }
] */
Promise.resolve().then(() => {
    console.log('promise1')
})
/* 
微任务:(() => {
    console.log('promise2')
}).then(() => {
    console.log('promise4')
})入队,队列[
    () => {
        console.log('promise1')
    },
    (() => {
        console.log('promise2')
    }).then(() => {
        console.log('promise4')
    })
] */
Promise.resolve().then(() => {
    console.log('promise2')
}).then(() => {
    console.log('promise4')
})
/* 
微任务:() => {
    console.log('promise3')
}入队,队列[
    () => {
        console.log('promise1')
    },
    (() => {
        console.log('promise2')
    }).then(() => {
        console.log('promise4')
    }),
    () => {
         console.log('promise3')
    }
] */
Promise.resolve().then(() => {
    console.log('promise3')
})
// 执行 console.log('同步1')
console.log('同步1')
// 执行 microtask queue
/* 
执行 console.log('promise1'),队列 [
    (() => {
        console.log('promise2')
    }).then(() => {
        console.log('promise4')
    }),
    () => {
         console.log('promise3')
    }
] */

/* 
执行 console.log('promise2'),
微任务:() => {
     console.log('promise4')
}入队,队列 [
    () => {
         cnsole.log('promise3')
    },
    () => {
        console.log('promise4')
    }
] */

/* 
执行 console.log('promise3'),队列 [
    () => {
        console.log('promise4')
    }
] */

/* 
执行 console.log('promise4'),队列 [], 微任务执行完毕 */

task vs micotask

mdn:

有两点关键的区别。

首先,每当一个任务存在,事件循环都会检查该任务是否正把控制权交给其他 JavaScript 代码。如若不然,事件循环就会运行微任务队列中的所有微任务。接下来微任务循环会在事件循环的每次迭代中被处理多次,包括处理完事件和其他回调之后。

其次,如果一个微任务通过调用  queueMicrotask(), 向队列中加入了更多的微任务,则那些新加入的微任务 会早于下一个任务运行 。这是因为事件循环会持续调用微任务直至队列中没有留存的,即使是在有更多微任务持续被加入的情况下。

一个任务就是指计划由标准机制来执行的任何 JavaScript,如程序的初始化、事件触发的回调等。 除了使用事件,你还可以使用 setTimeout() 或者 setInterval() 来添加任务**。** 任务队列和微任务队列的区别很简单,但却很重要:

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行.
  • 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。

看起来云里雾里,只需要记住一句:在task queue中每个task执行之前,都会先执行microtask queue 中的microtask,因为task的执行,可能会向microtask queue中添加新的microtask.
例题:

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

setTimeout(() => {
    console.log('setTimeout2')
    Promise.resolve().then(() => {
        console.log('promise2')
    })
}, 0)

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

Promise.resolve().then(() => {
    console.log('promise1')
})
console.log('同步1')

答案:

同步1
promise1
setTimeout1
setTimeout2
promise2
setTimeou3

解析:

// 初始宏任务队列 []
// 初始微任务队列 []
/* 
宏任务:() => {
    console.log('setTimeout1')
}入队宏任务队列,此时
宏任务队列:[
() => {
    console.log('setTimeout1')
}]
微任务队列: []

*/
setTimeout(() => {
    console.log('setTimeout1')
}, 0)
/* 
宏任务:() => {
    console.log('setTimeout2')
    Promise.resolve().then(() => {
        console.log('promise2')
    })
}入队宏任务队列,此时
宏任务队列:[
() => {
    console.log('setTimeout1')
},
() => {
    console.log('setTimeout2')
    Promise.resolve().then(() => {
        console.log('promise2')
    })
}]
微任务队列: []

*/
setTimeout(() => {
    console.log('setTimeout2')
    Promise.resolve().then(() => {
        console.log('promise2')
    })
}, 0)
/* 
宏任务:() => {
    console.log('setTimeout3')
}入队宏任务队列,此时
宏任务队列:[
() => {
    console.log('setTimeout1')
},
() => {
    console.log('setTimeout2')
    Promise.resolve().then(() => {
        console.log('promise2')
    })
},
() => {
    console.log('setTimeout3')
}]
微任务队列: []

*/
setTimeout(() => {
    console.log('setTimeou3')
}, 0)
/* 
微任务:() => {
    console.log('promise1')
}入队宏任务队列,此时
宏任务队列:[
() => {
    console.log('setTimeout1')
},
() => {
    console.log('setTimeout2')
    Promise.resolve().then(() => {
        console.log('promise2')
    })
},
() => {
    console.log('setTimeout3')
}]
微任务队列: [
    () => {
    console.log('promise1')
}
]

*/
Promise.resolve().then(() => {
    console.log('promise1')
})
// 执行同步代码 console.log('同步1')
console.log('同步1')
/* 
此时队列情况:
宏任务队列:[
() => {
    console.log('setTimeout1')
},
() => {
    console.log('setTimeout2')
    Promise.resolve().then(() => {
        console.log('promise2')
    })
},
() => {
    console.log('setTimeout3')
}]
微任务队列: [
    () => {
    console.log('promise1')
}
]
微任务优先级更高,执行宏任务前先执行微任务队列
执行微任务:() => {
    console.log('promise1')
}
微任务队列: []
宏任务队列:[
() => {
    console.log('setTimeout1')
},
() => {
    console.log('setTimeout2')
    Promise.resolve().then(() => {
        console.log('promise2')
    })
},
() => {
    console.log('setTimeout3')
}]

*/

/* 
微任务队列执行完毕,开始执行宏任务
执行宏任务:() => {
    console.log('setTimeout1')
}
微任务队列: []
宏任务队列:[
() => {
    console.log('setTimeout2')
    Promise.resolve().then(() => {
        console.log('promise2')
    })
},
() => {
    console.log('setTimeout3')
}]
*/

/* 
微任务优先级更高,执行宏任务前先执行微任务队列
微任务队列执行完毕(为空),开始执行宏任务
执行宏任务:() => {
    console.log('setTimeout2')
    // 这里产生了微任务,() => {
    //    console.log('promise2')
    //}入队微任务队列
    Promise.resolve().then(() => {
        console.log('promise2')
    })
},
微任务队列: [
    () => {
        console.log('promise2')
    }
]
宏任务队列:[
() => {
    console.log('setTimeout3')
}]
*/

/* 
微任务优先级更高,执行宏任务前先执行微任务队列
执行微任务:() => {
        console.log('promise2')
    }
微任务队列: []
宏任务队列:[
() => {
    console.log('setTimeout3')
}]
*/

/* 
微任务队列执行完毕,开始执行宏任务
执行宏任务:() => {
    console.log('setTimeout3')
},
微任务队列: []
宏任务队列:[]
 */
// 全部执行完成

微任务和宏任务的执行都用可能产生新的微任务和宏任务,因此先分别画出微任务和宏任务队列,然后根据执行入队和出队,否则很容易出错。

习题

promise习题

promise要注意区分哪些是同步代码,哪些是异步代码
例题:

new Promise((resolve) => {
    console.log('这是promise的同步1')
    resolve()
    console.log('这是promise的同步2')
}).then(() =>{
    console.log('这是promise的异步1')
})
console.log('同步')

答案:

这是promise的同步1
这是promise的同步2
同步
这是promise的异步1

解析:

// 不要看到promise就当异步处理,then里面才是异步,new Promise 声明是同步!
new Promise((resolve) => {
    // 这里是同步!
    console.log('这是promise的同步1')
    // 这里相当于then才是异步
    resolve()
    // 这里是同步!
    console.log('这是promise的同步2')
}).then(() =>{
    console.log('这是promise的异步1')
})
// 同步,但是在promise声明的后面!
console.log('同步')

async习题

async函数中的await左下部分相当于new Promise().then

/* 
async () => {
    xxx代码
    这上面也是右上
----------------|
这里开始是左下  | 这里是右上
    const p =  | await xxx();
               |-------------------
        这里也是左下       
           xxx代码    
}
*/


例题1:

const asyncFunction = async () => {

    console.log('async同步1')

    await new Promise((resolve) => {
        console.log('这是promise的同步1')
        resolve()
        console.log('这是promise的同步2')
    })

    console.log('async异步1')
}

asyncFunction()

console.log('同步1')

答案:

async同步1
这是promise的同步1
这是promise的同步2
同步1
async异步1

解析:

// async函数 第一个await 前面是同步,之后的相当于new Promise.then()是微任务
const asyncFunction =  () => {
    console.log('async同步1')

     new Promise((resolve) => {
        console.log('这是promise的同步1')
        resolve()
        console.log('这是promise的同步2')
    }).then(() => {
        console.log('async异步1')
    })
}

asyncFunction()

console.log('同步1')

例题2:

const asyncFunction = async () => {
    console.log('async同步1')
    await new Promise((resolve) => {
        console.log('这是promise的同步1')
        resolve()
        console.log('这是promise的同步2')
    })
    console.log('async异步1')
    await new Promise((resolve) => {
        resolve()
    })
    console.log('async异步2')
}

Promise.resolve().then(() => {
    console.log('promise异步1')
})
asyncFunction()
Promise.resolve().then(() => {
    console.log('promise异步2')
})

console.log('同步1')

答案:

async同步1
这是promise的同步1
这是promise的同步2
同步1
promise异步1
async异步1
promise异步2
async异步2

解析:

// 只要把async函数内的await都换成promise就可以了
const asyncFunction = () => {
    console.log('async同步1')
    new Promise((resolve) => {
        console.log('这是promise的同步1')
        resolve()
        console.log('这是promise的同步2')
        // 第一个await 变成.then
    }).then(() => {
        console.log('async异步1')
        return new Promise((resolve) => {
            resolve()
        })
        // 第二个await也变成.then
    }).then(() => {
        console.log('async异步2')
    })

}

Promise.resolve().then(() => {
    console.log('promise异步1')
})
asyncFunction()
Promise.resolve().then(() => {
    console.log('promise异步2')
})

console.log('同步1')

综合习题

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')

答案:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

解析:

// 1.先把async函数都改写成promise形式
function async1(){
    // 同步
    console.log('async1 start')
    new Promise((resolve) => {
        // resolve中的函数同步执行
        resolve(async2())
        // then入队微任务
    }).then(() => {
        console.log('async1 end')
    })
}
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')

总结

event loop 问题尤其是涉及到async的情况下非常复杂,因为篇幅限制,本文无法列举全部内容,需要读者多尝试总结,另外附上本文没有说明的event loop其他情况,读者查看对应连接即可:
1.为什么需要微任务
2.node环境下的setImmediate、process.nextTick