当谈及Javascript时, 咱们经常听到 单线程、异步非阻塞、事件循环这样的关键词,然而它们是什么? 为何单线程还能够实现异步?怎么实现?相信这些问题都曾今或正在困扰着许多前端爱好者。经过这篇文章咱们将对它们一一梳理。文章将讲解:javascript
若是你对它们感兴趣,就请继续往下读吧。前端
笼统抽象地说:java
回到编程的世界里具体地说:web
用图表达:编程
因为步骤2 的执行时间较长,在同步执行过程当中他会阻塞步骤3的执行一段时间,反之在异步的机制中,若是咱们标明了步骤2是异步的, 那么在完成步骤1以后咱们只会开始执行步骤二并让他在另外一个世界里执行,而后马上开始执行步骤3.promise
用JavaScript来模拟上面的过程:浏览器
const step1 = () => console.log(1); const step3 = () => console.log(3); const step4 = () => console.log(4); // 用 Promise 简单模拟一个执行时间为1秒,并在结束时候打印2的函数 // 若是你不了解Promise,不要紧,盖住函数的内容,只要记得它执行时间很长,会在结束时打印2,并会告诉你”我执行完了“ const step2 = () => new Promise((resolve, _) => { setTimeout(() => { console.log(2); resolve(); }, 1000); }); // 异步机制执行 // JS引擎支持返回Promise的函数异步执行,因此咱们不须要作额外的包装,直接执行step2,它即是异步的) const asyncExecute = () => { step1(); step2(); step3(); step4(); }; // 同步机制执行 // 若是你不了解async、await没关系,只要知道 await 就是要等到它后面的语句执行完了才会进行下一步 const syncExecute = async () => { step1(); await step2(); step3(); step4(); }; asyncExecute(); // 打印 1 3 4 2 syncExecute(); // 打印 1 2 3 4
同步与异步各有优势,同步能够保证执行的顺序,异步能够保证程序的非阻塞。 在一个web应用中,若是咱们把向服务端请求一个资源当作一个时间很长的步骤,那么处理这个请求返回信息就须要咱们去同步执行。然而在若是咱们在请求资源的同时还想让用户能够继续使用咱们的应用,那这就须要异步地去实现。 Javascript 和他的引擎给咱们提供丰富的资源去实现这两种机制。数据结构
讲到这里,也许还有些抽象,但相信在下面的章节里,这一切会变得愈来愈清晰。异步
首先咱们来理解几个概念:async
在一个JS运行时环境中,JS 代码只在一个线程中执行, 因此咱们说 JS 是单线程的。然而运行时环境自己(好比在浏览器中)并非单线程的,他包含了JS引擎的运行、一系列的web API 调用、以及咱们后面要讲到的事件循环机制的运行等。
若是说线程是程序自我分割、并行执行的最小单元,那在单个线程里执行的JS代码又怎么可能实现并行,也就是异步呢?
假设咱们在Chrome浏览器中,事件循环的机制能够用这样一张图来解释。
在这里咱们须要记住五个模块:
接下来让咱们一一解释
当 JS 引擎解析JS代码的过程当中遇到一些变量或者函数申明的时候,它会将它们存储到里面。
undefined
),引擎就会把这个调用从stack顶端删除, 而后继续执行它下面的函数。举个例子:
const func2 = ()=> { console.log("我是 func2 ") } const func1 = () => { console.log("func1 开始了") func2(); console.log("func1 结束了") } func1(); // 打印: // func1 开始了 // 我是 func2 // func1 结束了
执行这段代码时,引擎就会先调用 func1
, 将它的调用放到stack里,而后执行func1
中第一行打印。而后执行第二行 调用 func2
。这时引擎会把 func2
调用放到stack的顶端(如大图中所示),而后执行 func2
的内容也就是打印。结束以后,由于func2
中没有更多的内容,引擎会删除stack顶端的func2
的调用,而后继续执行func1
第三行,当第三行结束完毕,引擎删除stack中func1
的调用。最后咱们会看到这段程序的打印如代码最后的注释中所示。
callback1 = () => console.log("我是 callback1"); callback2 = () => console.log("我是 callback2"); const func2 = () => { console.log("func2 开始"); // setTimeout 就是一个异步调用的 Timer API, 他会让 Timer 计时必定的时间,好比这里是1秒,而后触发计时结束,随后callback将会被放入 callback queue setTimeout(callback2, 1000); console.log("func2 结束"); }; const func1 = () => { console.log("func1 开始"); setTimeout(callback1, 0); func2(); console.log("func1 结束"); }; func1(); // 打印 // func1 开始 // func2 开始 // func2 结束 // func1 结束 // 我是 callback1 // 我是 callback2
咱们来讲说这段代码是怎么在在刚才解释的机制下执行的(超长!若是你已经理解了能够跳过这段 ^^):
因为4和5是在两个线程里执行的,因此咱们能够把它们当作几乎是同时执行的。
因为9和10是在两个线程里执行的,因此咱们能够把它们当作几乎是同时执行的
文字表现比较局限,咱们能够按步骤动手画一画,就很是清晰了。
Web应用的性能是个很大的话题,在这里咱们只讨论性能中与 JS 的单线程和异步相关的部分。
首先提几个概念做为准备:
那若是Stack中有一个function执行时间超过16.66ms 会怎么样?答案是它会致使下一个render的推迟执行。在这个function结束前,页面是停在一个静止的状态的,用户在页面上点击也不会有什么反应。这就是咱们有时会感觉到的 “页面有点卡”。因此为了防止这种性能差的表现,咱们不建议将耗时的function放到 JS 的主线程里执行。
其实因为render自己的执行也须要消耗时间,因此咱们还要给它留出空间。根据谷歌的官方文档,咱们最好是将本身的逻辑保证在10ms如下,甚至是3-4ms。
然而因为业务的须要,在开发中一些耗时的逻辑是没法避免的,例如排序、搜索等。在这样的状况下咱们能够将逻辑分红小块,而后使用requestAnimationFrame,或者将耗时的逻辑放到service worker中进行。 具体如何使用在这里不作细说,咱们能够参照谷歌的这篇文档 Optimize JavaScript Execution,上面有详细的解说。
长话短说:
setTimeout
只是在给定的时间以后将它的 callback function 放入 callback queue 但并不能保证function 的准时执行。setInterval
也是,只是每隔固定的时间放入一次callback function. 因此它们是否能准时执行都取决于当时stack 和 callback queue 的状态。但咱们仍是能够粗略地认为它们是准时的,由于大部分状况下这些不许时只是毫秒级的,但也须要理解它们其中的原理来处理和解释那些小部分的状况。
详细解释:
看了 MDN 或 w3schools 对 setTimeout
的解释,咱们容易简单地认为它的做用是在必定的时间后执行一个callback function。然而这并不彻底正确。根据咱们在第二节中解释的 stack 和 callback queue 的概念, setTimeout 只能保证将它的callback function在必定时间以后放入callback queue 而不是执行。 若是此时callback queue 中只有这个function 且stack是空的,固然它就会被准时执行。但若是此时stack中还有还没有执行完的内容,或者在callback queue 中还有好几个callback在排队,显然咱们的function会被推后执行,这个推后的时间取决与stack中的内容 和 callback queue 中排在前面的 callback 要执行多久。
但咱们仍是能够粗略地认为它是准时的。 只要咱们不在JS的线程里放入一个十分耗时的function, 或者在callback queue里瞬间塞入一大堆callback, 那么stack是时常会被空出来执行咱们 setTimeout
扥 callback 的。在这样的状况下不许时的误差也就只是毫秒级的。
细心的你也许发现了在第二节的例子中咱们使用了 setTimeout(callback,0)
, 也就是在 0 毫秒后将callback
放入 callback queue。 因为queue 中的 callback 会在stack 空了以后在执行,那么这个用法其实能够做为一种控制执行顺序的工具。 让咱们来看一个简单地例子:
setTimeout(() => console.log("我想后执行"), 0); console.log("我想先执行"); // 打印 // 我想先执行 // 我想后执行
setInterval
和 setTimeout
的实现原理是类似的。咱们粗略地理解它为每隔必定的时间执行一次callback。实际上是每隔必定的时间在 callback queue 中加入一次 callback, 因此它先后两次执行callback 的间隔时间也是不能保证的, 它们可长可短, 取决于stack和callback queue 的状态。
谢谢你一直读到如今。这是个人第一篇博文,它记录了我对前端知识学习和思考的过程。但愿你在阅读过程当中有所收获。我会继续坚持下去分享我在学习工做中的心得和体会。
最后,感谢这些帮助我学习文章相关内容的资料:
Asynchronous JavaScript: Promises, Callbacks, Async Await
The Javascript Runtime Environment
What the heck is the event loop anyway? | Philip Roberts | JSConf EU