JS 的事件循环也算是一个老生常谈的话题了,面试中相信大部分人都有被问到:说一说 JS 的事件循环。那么要了解他的循环机制,首先我们要了解一些基本概念。今天我们就简单聊聊事件循环然后向外拓展。
我们知道 JS 他是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。
JS 在 Node 和浏览器中执行机制是不一样的,浏览器 Event Loop 是 HTML 中定义的规范,Node Event Loop 是由 libuv 库实现,这里主要讲的是浏览器部分。
JS 事件循环中有两种任务(同步任务、异步任务)
那在任务队列里存放的各种事件又是怎么个情况?首先他们分为宏任务和微任务。
宏任务 | 微任务 | |
---|---|---|
谁发起的 | 宿主(Node、浏览器) | JS引擎 |
具体事件 | script (可以理解为外层同步代码)、setTimeout/setInterval、UI rendering/UI事件、postMessage,MessageChannel、setImmediate,I/O(Node.js) | Promise.then、MutaionObserver |
谁先运行 | 后运行 | 先运行 |
会触发新一轮Tick吗 | 会 | 不会 |
事件循环流程:
浏览器的内核是多线程的,一个浏览器一般至少实现三个常驻线程:
这里大概了解一下不做重点叙述。
来一道简单的题目理解一下:
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve('success')
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
new Promise
,执行其中的同步代码1resolve('success')
, 将promise
的状态改为了resolved
并且将值保存下来promise
,往下执行,碰到promise.then
这个微任务,将其加入微任务队列promise.then
这个微任务且状态为resolved
,执行它。我们面试中经常问起的 Promise 相关题目都是跟 JS 的循环事件机制有关的,Promise 是 ES6 的产物,在还没有 Promise 时的远古时期我们使用回调只能用 callback(回调函数)。
function back(callback) {
// do something ...
callback()
}
back(() => {
console.log('hello')
})
若是要嵌套回调,就会让代码难以维护。
back(() => {
back({
() => {
// ....
}
})
})
这时候 Promist 被提出来,最早是由社区提出和实现的,Es6 将他写进规范并提供原生对象。在一定程度上解决了回调地狱。
function back(data) {
return new Promise((resolve, reject) => {
// do something ...
if(data) resolve(data)
else reject(data)
})
}
back('hello').then(res => {
// do something ...
return back('hi')
}).then(res => {
return back('nihao')
}).catch(err => {
console.log(err)
})
但 Promise 也并非十全十美,虽然大多情况他是 OK 的,但是 Promise 一旦执行了便无法取消,且错误不能被 try catch 捕获。
Generator 算是一个小插曲,是 ES6 提供的异步编程解决方案,他就是一个封装的异步任务或容器,异步操作需要暂停的地方,都用 yield 语句声明。
Generator 函数一般配合 yield 或 Promise 使用,他返回的是迭代器。
function* back() {
let a = yield 1
console.log(a)
let b = yield 2
console.log(b)
// so on...
}
const obj = back()
back.next() // { value: 1, done: false}
back.next() // { value: 2, done: true}
back.next() // { value: undefined, done: true}
Generator 后来被 async/await 取代,但是 Generator 独特的特性可以让我们在函数执行的过程中传递参数获取结果,使得函数调用变得更加灵活。
ES7 中引进的新鲜玩意,实际上 async 是一个语法糖,他的实现就是将 Generator 函数和自动执行器(co)封装在一个函数中, async/await 用过的人都知道这种跟同步代码一样的写法,条理清晰,不像 Promise 很多 then。并且他的错误可以被 try catch 捕获。
function back(data) {
return new Promise((resolve, reject) => {
// do something ...
if(data) resolve(data)
else reject(data)
})
}
async function main() {
// do something ...
await back(1)
await back(2)
await back(3)
}
这种写法,上一条语句的代码未执行完之前下面的代码都是无法执行的。其实就类似 Promise 中 then 后面还有 then 是一样的。这种写法也是我们当前使用的比较多的处理异步的写法。
都看到这里了,不点个赞再走吗?
欢迎在下方给出你的建议和留言。