Open liubinis86 opened 6 years ago
讲到 javascript 的事件循环。首先,必须知道的是 javascript 是一门单线程的语言。因此,javascript 是没有真正意义上的异步任务的。javascript 所有的异步任务都是通过同步去模拟实现的。 虽然 H5 新标准中的 web worker 充分利用了多核 CPU 的计算能力,被允许 javascript 创建多个线程,但是子线程是完全受到主线程控制的,且在 web worker 中无法访问到 window,document 等对象。因此,javascript 还是没有改变单线程的本质。
此处引入一段阮一峰的话:
JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什 JavaScript 不能有多个线程呢?这样能提高效率啊。JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
单线程就意味着所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就得一直等着,直到前一个任务执行完才会执行。为了解决这个问题,js 有了同步任务(synchronous)与异步任务(asynchronous)之分。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。如图
任务队列
console.log(1); // 同步任务 setTimeout(() => { console.log(2); }, 0); // 异步任务 ele.addEventListener("click", () => { console.log(3); }); // 回调也属于异步任务,ele触发点击的时候才会执行 console.log(4); // 同步任务 ele.click(); // 此处模拟点击的话,其实是直接把点击事件的回调函数拿来执行了。相当于同步任务,只有当用户在页面主动触发的时候,才是按照异步的流程走 console.log(5); // 同步任务 // log 1 4 3 5 2
event table
event queue
代码执行的顺序:
console.log(1)
console.log(4)
console.log(3)
console.log(2)
关于定时器,定时器可以设置执行的时间间隔。然而,有些时候并不能直接完美的按照我们设置的执行间隔来运行。因为定时器的回调函数是放到任务队列里面的,只有当主线程空闲的时候才会去执行任务队列里面的任务。举个例子:
// 假如此处的doSomething执行需要30ms的时间。那么定时器中的回调得等到30ms后才会触发,设定的20ms的delay是不生效的。 setTimeout(() => { console.log(1); }, 20); doSomething();
setInterval虽说是每隔一段时间就执行一次,但是如果其回调函数执行的时间超过了设定的时间间隔,实际上是无法实现每隔一段时间就执行一次的。举个例子:
setInterval
let start = new Date().getTime(); setInterval(() => { for (let i = 0; i < 10000; i++) { const pEle = document.createElement("p"); pEle.innerText = i; document.body.appendChild(pEle); if (i === 0) { const end = new Date().getTime(); console.log(end - start); // 22 384 209 start = end; } } }, 20); // 虽然设定了20ms的delay.但是每次执行到log的实际间隔是大于20ms的
因此,不到万不得已,不建议在日常开发中使用setTimtout与setInterval。若是做动画效果,可以使用requestAnimationFrame(callback),requestAnimationFrame 每 16ms(即 1000/60)执行一次,与浏览器显示页面的刷新频率一致,是性能最好的一个定时 API。
setTimtout
requestAnimationFrame(callback)
js 代码又可以分为宏观任务(整体代码、定时器)与微观任务(promise 与 node.js 中专有的 proces.nextTick); js 的执行顺序是,一开始先执行宏观任务,然后再查看微观任务队列中是否有任务可执行,若有则执行,执行结束后开始新的宏观任务,若无则直接开始新的宏观任务,循环往复如此。如图: 需要值得注意的是,异步的微观任务执行是优先于异步的宏观任务的。举个例子:
console.log(1); setTimeout(() => { console.log(2); }, 0); new Promise(resolve => { console.log(3); resolve(); }).then(() => { console.log(4); }); console.log(5); // log 1 3 5 4 2
宏观任务队列
微观任务队列
console.log(5)
再举个例子:
Tips:async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。 console.log(1); setTimeout(() => { console.log(2); }, 0); async function foo() { console.log(3); await setTimeout(() => { console.log(4); }, 20); console.log(5); } foo(); console.log(6); // log 1 3 6 5 2 4
Tips:async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
console.log(1); setTimeout(() => { console.log(2); }, 0); async function foo() { console.log(3); await setTimeout(() => { console.log(4); }, 20); console.log(5); } foo(); console.log(6); // log 1 3 6 5 2 4
console.log(6)
日常开发中理解了本文中所讲的浏览器中的事件循环就足够了。往深了研究还可以研究一堆东西。 布置个课后作业吧,判断下面的代码执行顺序,再去浏览器中执行,看看自己是否真的理解了吧!
console.log(1); setTimeout(() => { console.log(2); }, 0); new Promise(resolve => { console.log(3); resolve(); }).then(() => { console.log(4); }); async function foo() { console.log(5); await bar(); console.log(6); } function bar() { console.log(7); setTimeout(() => { console.log(8); }, 0); } console.log(9); foo();
本文参考:
ECMAScript 6 入门
Concurrency model and Event Loop
这一次,彻底弄懂 JavaScript 执行机制
JavaScript 运行机制详解:再谈Event Loop
What is the JavaScript event loop?
Philip Roberts: What the heck is the event loop anyway?
写得很好啊,我也没有系统学习过这个,之前都是一知半解
EVENT LOOP in Browser
讲到 javascript 的事件循环。首先,必须知道的是 javascript 是一门单线程的语言。因此,javascript 是没有真正意义上的异步任务的。javascript 所有的异步任务都是通过同步去模拟实现的。 虽然 H5 新标准中的 web worker 充分利用了多核 CPU 的计算能力,被允许 javascript 创建多个线程,但是子线程是完全受到主线程控制的,且在 web worker 中无法访问到 window,document 等对象。因此,javascript 还是没有改变单线程的本质。
为什么 JavaScript 是单线程?
此处引入一段阮一峰的话:
事件循环
单线程就意味着所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就得一直等着,直到前一个任务执行完才会执行。为了解决这个问题,js 有了同步任务(synchronous)与异步任务(asynchronous)之分。
1. 同步任务与异步任务:
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。异步任务指的是,不进入主线程、而进入"
任务队列
"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。如图event table
即整体异步事件回调函数的一个中转站。当异步事件的触发条件达成时,会将对应的回调函数加入到event queue
中。event queue
本质上是一个队列,因此也遵循“先进先出”的原则代码执行的顺序:
console.log(1)
。event table
中,delay 为 0,意味着马上将其回调函数马上加入到event queue
中。event table
中。console.log(4)
。event queue
中。console.log(3)
。console.log(2)
。关于定时器,定时器可以设置执行的时间间隔。然而,有些时候并不能直接完美的按照我们设置的执行间隔来运行。因为定时器的回调函数是放到任务队列里面的,只有当主线程空闲的时候才会去执行任务队列里面的任务。举个例子:
setInterval
虽说是每隔一段时间就执行一次,但是如果其回调函数执行的时间超过了设定的时间间隔,实际上是无法实现每隔一段时间就执行一次的。举个例子:因此,不到万不得已,不建议在日常开发中使用
setTimtout
与setInterval
。若是做动画效果,可以使用requestAnimationFrame(callback)
,requestAnimationFrame 每 16ms(即 1000/60)执行一次,与浏览器显示页面的刷新频率一致,是性能最好的一个定时 API。2.宏观任务(macro-task)与微观任务(micro-task):
js 代码又可以分为宏观任务(整体代码、定时器)与微观任务(promise 与 node.js 中专有的 proces.nextTick); js 的执行顺序是,一开始先执行宏观任务,然后再查看微观任务队列中是否有任务可执行,若有则执行,执行结束后开始新的宏观任务,若无则直接开始新的宏观任务,循环往复如此。如图: 需要值得注意的是,异步的微观任务执行是优先于异步的宏观任务的。举个例子:
代码执行的顺序:
console.log(1)
event table
中,delay 为 0,意味着马上将其回调函数马上加入到宏观任务队列
中。console.log(3)
,将后续操作的代码(此处的.then)加入到event table
中。当promise内的异步代码执行完毕后,会将后续操作的代码从event table
中移除,加入到微观任务队列
中。console.log(5)
。console.log(4)
console.log(2)
再举个例子:
代码执行的顺序:
console.log(1)
。event table
中,delay 为 0,意味着马上将其回调函数马上加入到event queue
中。console.log(3)
。遇到 await,执行 await 的函数,且将后面的代码加入到event table
中,等到 await 异步执行完毕后,将后面的代码加入到微观任务的event queue
中。foo 函数暂停。console.log(6)
。console.log(5)
。console.log(2)
、console.log(4)
。结语:
日常开发中理解了本文中所讲的浏览器中的事件循环就足够了。往深了研究还可以研究一堆东西。 布置个课后作业吧,判断下面的代码执行顺序,再去浏览器中执行,看看自己是否真的理解了吧!
本文参考:
ECMAScript 6 入门
Concurrency model and Event Loop
这一次,彻底弄懂 JavaScript 执行机制
JavaScript 运行机制详解:再谈Event Loop
What is the JavaScript event loop?
Philip Roberts: What the heck is the event loop anyway?