-
Notifications
You must be signed in to change notification settings - Fork 441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
从一道题浅说 JavaScript 的事件循环 #61
Comments
笔记总结微任务与宏任务
我们的JavaScript的执行过程是单线程的,所有的任务可以看做存放在两个队列中——执行队列和事件队列。 执行队列里面是所有同步代码的任务,事件队列里面是所有异步代码的宏任务,而我们的微任务,是处在两个队列之间。 当JavaScript执行时,优先执行完所有同步代码,遇到对应的异步代码,就会根据其任务类型存到对应队列(宏任务放入事件队列,微任务放入执行队列之后,事件队列之前);当执行完同步代码之后,就会执行位于执行队列和事件队列之间的微任务,然后再执行事件队列中的宏任务。 实例
以上代码来源于博主的该篇博客,比较具有代表性,因为包含了宏任务、微任务、同步代码。在我们分析输出时,可以给所有的任务划分归类,结果如下:
结尾这是我在阅读博主的博文之后,根据自己的想法理解所写出来的,难免存在错误,敬请大家指出! |
试了下,setTimout和setImmediate却不是同源的,
setImmediate和setInterval有自己的任务队列 |
感谢博主 |
按你这个解释似乎有点矛盾。不过感觉上就是t2的then的代码先执行到了, 所以t2的then被先加入了队列。😂 |
@njleonzhang 你看下阮老师对 Promise.resolve 的表述:
示例如下: let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
new Promise(resolve => {
resolve(1);
Promise.resolve(thenable).then((t) => {
// t2
console.log(t)
});
console.log(4)
}).then(t => {
// t1
console.log(t)
});
console.log(3); //输出 4 3 1 42 |
@dwqs 学到了。 |
从 自己动手实现 ES6 Promise 一文可以看到部分相关实现: // ...
// 根据 x 值,解析 promise 状态
resolveProcedure(promise, x)function resolveProcedure({ resolve, reject, promise2 }, x) {
// 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
if (promise2 === x) {
reject(new TypeError(x));
}
if (x instanceof ES6Promise) { // 2.3.2 If x is a promise, adopt its state
x.then(value => resolveProcedure({resolve, reject, promise2}, value), reason => reject(reason));
} else if ((typeof x === 'object' && x !== null) || (typeof x === 'function')) { // 2.3.3
let resolvedOrRejected = false;
try {
let then = x.then; // 2.3.3.1 Let then be x.then
if (typeof then === 'function') { // 2.3.3 If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where:
then.call(x, value => {
if (!resolvedOrRejected) {
resolveProcedure({ resolve, reject, promise2 }, value); // 2.3.3.3.1 If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
resolvedOrRejected = true;
}
// 2.3.3.3.3 If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
}, reason => {
if (!resolvedOrRejected) {
reject(reason); // 2.3.3.3.2 If/when rejectPromise is called with a reason r, reject promise with r.
resolvedOrRejected = true;
}
// 2.3.3.3.3 If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
});
} else { // 2.3.3.4 If then is not a function, fulfill promise with x.
resolve(x);
}
} catch (e) {
if (!resolvedOrRejected) {
// 2.3.3.2 If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
// 2.3.3.4 If calling then throws an exception e
reject(e);
}
}
} else {
resolve(x); // 2.3.4 If x is not an object or function, fulfill promise with x.
}
}
// ... |
requestAnimationFrame不属于task,它是浏览器渲染过程的一步,和task/microtask的执行是分离的 |
已修改 @yaodingyd |
楼上的这个 console.log('script start'); setTimeout(function() { new Promise(resolve => { console.log('script end'); 在浏览器中执行的结果 和在node环境中执行的结果,并不一致。 有大佬解释下吗 |
@sideFlower 浏览器和 Node.js 的事件循环机制是有区别的。单这道题来说,区别在于浏览器是把 macro task 1, 2 加入队列,挨个执行,并把 macro 中的 micro 执行。也就是 timer1(macro) 先执行,其中的 promise then(micro) 再执行,完毕后再跑 timer2。而 node 中的 micro task 是在 node 事件循环的各个阶段之间执行,也就是主线程跑完后,会把 micro 池中的事件清空。然后是 timers 阶段,也就是定时器这种,按照加入事件池的先后顺序执行,此时 micro task 加入新的 promise.then,直到所有的定时器执行完,才会从 micro task 取出任务,挨个执行,所以才会最后调用 then2。 这是从这道题来说的不同,具体你可以从网上搜一下 浏览器和 node 环境事件循环的不同。 |
非常感谢这篇文章的详解以及讨论,学习到了Event Loop的很多细节。但是还有一处不明,还请帮忙解释一下。非常感谢你的时间。 // example 1
new Promise(resolve => {
resolve(1)
Promise.resolve().then(() => console.log(42)) // then1
console.log(4)
}).then(t => console.log(t)) // then2
console.log(3)
// 输出结果依此为:4, 3, 42, 1 // example 2
let thenable = {
then: (resolve, reject) => {
resolve(42)
}
}
new Promise(resolve => {
resolve(1)
Promise.resolve(thenable).then((t) => console.log(t)) // then1
console.log(4)
}).then(t => console.log(t)) // then2
console.log(3)
// 输出结果依此为:4, 3, 1, 42 问题是:不明白在Promise.resolve()的参数是“无参数”和“thenable对象”时,没搞明白Event Loop中这两者的执行顺序??
20190225: 一位小伙伴(也没留github id)在 SF 上让我回来把我想通的思路再说一下。惭愧,我都快忘记我问过啥了,我就借机又重温了一下,非常感谢。 总结一下我的问题:不明白在Promise.resolve()的参数是“非 thenable 对象”和“ thenable 对象”时的执行原理,以至于对例子中的执行顺序不确定。 这次换个例子便于理解 // demo
const thenable = {
then: function(resolve, reject) {
console.log('flag-0');
resolve(1);
console.log('flag-1');
}
};
const p1 = Promise.resolve();
p1.then(() => {
console.log(2);
});
console.log('flag-2');
const p2 = Promise.resolve(thenable);
p2.then((value) => {
console.log(value);
});
setTimeout(() => {
console.log('timeout');
}, 0);
const p3 = Promise.resolve();
p3.then(() => {
console.log(3);
});
console.log('flag-3');
// 输出结果依此为:flag-2, flag-3, 2, flag-0, flag-1, 3, 1, timeout 先从 Event Loop 的角度说一下个人理解: Event Loop 的执行机制,我也是看这篇文章的作者学习的,就不多说,只说这个例子里面的时间循环。 第一轮: 第二轮: 因此输出结果顺序为:flag-2, flag-3, 2, flag-0, flag-1, 3, 1, timeout 建议在浏览器中直接打断点走,可以很直接看出每一轮的执行顺序以及对应的值,如下: 再从 Promise Promise.resolve() 的角度说一下个人理解:
Promise.resolve(thenable), Promise.resolve(thenable) 的最终状态值需要 "follow" 这个 thenable 对象, 使用它的最终状态( result ),我理解为 fulfill/reject 以上是初步的理解,后面想再进一步学习一下 Event Loop 和 Promise,更好的理解,到时候再来这里放链接。我觉得有些时候英文的那个词可能编码者更容易理解,说成中文,不太容易对号入座,所以看规范更好学习。欢迎交流。 |
@Huooo 第二个问题 你可以看下上面「部分相关实现」我贴出的代码 |
Event Loop的理解有点磕磕绊绊的,主要担心自己理解的不是正确的,但是关于Promise.resolve()源码的部分已经看明白了,非常感谢 @dwqs |
@dwqs 怎么MessageChannel既在microTask又在macTask中呀? |
@WangBiaoxuan 已纠正 谢谢指出 |
MessageChannel 是属于 microTask 啊 |
另外 new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => console.log(2));
console.log(4)
}).then(t => console.log(t));
console.log(3); 之所以 2 比 1 要早,是因为 |
@fi3ework hey,man!you are right |
tick有些疑问,什么叫一次tick?如果一次循环叫一次tick,那为什么不直接叫nextLoop呢?一次循环指的是清空队列,还是取队列里的一个event并执行? |
非常感谢!!讲的很详细,大致都明白了! 不过我还有一点疑惑,希望大佬能解答一下 setTimeout(() => {
console.log(0)
})
new Promise(resolve => {
resolve(1)
Promise.resolve().then(t => {
console.log(2)
Promise.resolve().then(t => {
console.log(4)
})
})
console.log(3)
})
.then(t => {
console.log(t)
})
console.log(5) 输出顺序是 3 5 2 1 4 0,为什么4会比1先输出?不是说 Promise.resolve().then 会更先执行吗? |
@qingtianiii 看 then的注册顺序:首先注册的是 console.log(2) 这个then,接下来注册 console.log(t) 这个then,所以先输出2 (在2执行时又注册了一个then),再输出1. 1输出结束之后,发现microtask队列还有一个新的task,则执行它输出 4. |
大佬,MutaionObserver 有个拼写错误哈~ |
在chrome中输出是4321 |
图破了。。。 |
setTimeout(() => { |
执行队列里面是所有同步代码的任务,事件队列里面是所有异步代码的宏任务,而我们的微任务,是处在两个队列之间。 不要这样理解啊大哥 |
这里的措辞有问题:“防止主线程的不阻塞”的意思就是要主线程阻塞,但是引入Event loop不就是为了解决阻塞问题吗?此处应该是“防止主线程的阻塞”,才是正确的吧? |
这才是正解,顺便附上源码地址 |
这是来自QQ邮箱的假期自动回复邮件。您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
这是来自QQ邮箱的自动回复邮件。您好,您的邮箱我已经收到了,感谢您的来信!
|
1 similar comment
这是来自QQ邮箱的自动回复邮件。您好,您的邮箱我已经收到了,感谢您的来信!
|
楼主逻辑梳理是错的,首先PromiseA+规范并不是浏览器实现规范,浏览器实现的是ecma对promise的要求。 然后Promise.resolve()本身就已经有微任务进去了,可以看网上的一些v8对promise的实现代码,还有更复杂的就是当promise返回一个Promise时,微任务队列有点绕,这里就不多说了 |
这是来自QQ邮箱的自动回复邮件。您好,您的邮箱我已经收到了,感谢您的来信!
|
这是来自QQ邮箱的假期自动回复邮件。您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
阮老师在其推特上放了一道题:
看到此处的你可以先猜测下其答案,然后再在浏览器的控制台运行这段代码,看看运行结果是否和你的猜测一致。
事件循环
众所周知,JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。根据 HTML 规范:
为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生。Event Loop 包含两类:一类是基于 Browsing Context,一种是基于 Worker。二者的运行是独立的,也就是说,每一个 JavaScript 运行的"线程环境"都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop。
那么在事件循环机制中,又通过什么方式进行函数调用或者任务的调度呢?
任务队列
根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
仔细查阅规范可知,异步任务可分为
task
和microtask
两类,不同的API注册的异步任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。(macro)task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
示例
纯文字表述确实有点干涩,这一节通过一个示例来逐步理解:
首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。所以,上面例子的第一步执行如下图所示:
然后遇到了
console
语句,直接输出script start
。输出之后,script 任务继续往下执行,遇到setTimeout
,其作为一个宏任务源,则会先将其任务分发到对应的队列中:script 任务继续往下执行,遇到
Promise
实例。Promise 构造函数中的第一个参数,是在new
的时候执行,构造函数执行时,里面的参数进入执行栈执行;而后续的.then
则会被分发到 microtask 的Promise
队列中去。所以会先输出promise1
,然后执行resolve
,将then1
分配到对应队列。构造函数继续往下执行,又碰到
setTimeout
,然后将对应的任务分配到对应队列:script任务继续往下执行,最后只有一句输出了
script end
,至此,全局任务就执行完毕了。根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。
因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中,只有
Promise
队列中的一个任务then1
,因此直接执行就行了,执行结果输出then1
。当所有的microtast
执行完毕之后,表示第一轮的循环就结束了。这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务
macrotask
开始。此时,有两个宏任务:timeout1
和timeout2
。取出
timeout1
执行,输出timeout1
。此时微任务队列中已经没有可执行的任务了,直接开始第三轮循环:第三轮循环依旧从宏任务队列开始。此时宏任务中只有一个
timeout2
,取出直接输出即可。这个时候宏任务队列与微任务队列中都没有任务了,所以代码就不会再输出其他东西了。那么例子的输出结果就显而易见:
总结
在回头看本文最初的题目:
这段代码的流程大致如下:
Promise
实例,构造函数首先执行,所以首先输出了 4。此时 microtask 的任务有t2
和t1
t2
和t1
,分别输出 2 和 1综上,上述代码的输出是:4321
为什么
t2
会先执行呢?理由如下:Promise.resolve
方法允许调用时不带参数,直接返回一个resolved
状态的Promise
对象。立即resolved
的Promise
对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。所以,
t2
比t1
会先进入 microtask 的Promise
队列。相关链接
The text was updated successfully, but these errors were encountered: