无论你是Javascript新手还是老手,无论你是求职面试还是日常开发工作,我们经常会遇到这样的情况:给定几行代码,我们需要知道它的输出内容和顺序。因为javascript是单线程语言,所以我们可以得出结论,javascript是按照语句出现的先后顺序执行的。如果你在自学中遇到困难,想找一个前端学习环境,可以加入我们的前端学习圈,点我加入,会节省很多时间,减少很多学习中遇到的困难。看到这里,读者要打人了:难道我不知道js是一行一行执行的吗?你需要说什么?保险起见,因为js是逐行执行的,所以我们认为js是这样的:leta='1';console.log(a);letb='2';console.log(b);但是,js实际上看起来像这样:setTimeout(function(){console.log('定时器开始')});newPromise(function(resolve){console.log('立即执行for循环');for(vari=0;i<10000;i++){i==99&&resolve();}}).then(function(){console.log('executethenfunction')});console.log('代码执行结束');按照js是按照语句出现的先后顺序执行的思路,我自信的记下了输出结果://“定时器已经启动”//“现在执行for循环”//“执行then函数”//“代码执行结束”在chrome上验证,结果完全错误,一时懵逼,约定好的一行一行的执行是什么?我们真的要吃透javascript的执行机制。一、关于javascriptJavascript是一种单线程语言。Web-Worker是在HTML5中提出的,但javascript的核心是单线程并没有改变。所以所有javascript版本的“多线程”都是用单线程模拟的,所有javascript多线程都是纸老虎!2.javascript事件循环由于js是单线程的,就像银行只有一个窗口。客户需要排队办理业务。同样,js任务也要一个一个执行。如果一个任务花费的时间太长,那么后一个任务也必须等待。那么问题来了,如果我们想浏览新闻,但是新闻中包含的超高清图片加载的很慢,我们的网页是不是应该一直卡到图片全部显示出来?因此,聪明的程序员将任务分为两类:同步任务和异步任务。当我们打开网站时,网页的渲染过程是很多同步的工作,比如页面骨架、页面元素的渲染。而加载图片、音乐等占用资源多、耗时长的任务,则属于异步任务。这部分有严格的文字定义,但本文的目的是以最小的学习成本彻底理解执行机制,所以我们用一张图来说明:这一次,彻底理解JavaScript的执行机制。图中要表达的内容用文字来表达:同步和异步任务进入不同的执行“地方”,同步进入主线程,异步进入EventTable和注册函数。当指定的事情完成后,EventTable会把这个函数移到EventQueue中。主线程中的任务执行完后为空,会去EventQueue中读取对应的函数,进入主线程执行。上述过程会不断重复,这就是常说的事件循环(eventloop)。我们不禁要问,我们怎么知道主线程执行栈是空的呢?js引擎有一个监控进程,会不断检查主线程执行栈是否为空。一旦为空,就会去EventQueue中查看是否有等待调用的函数。说了这么多文字,直接写一段代码更直接:letdata=[];$.ajax({url:www.javascript.com,data:data,success:()=>{console.log('发送成功!');}})console.log('代码执行结束');上面是一段简单的ajax请求代码:ajax进入EventTable,注册回调函数成功。执行console.log('代码执行结束')。ajax事件完成,回调函数success进入EventQueue。主线程从EventQueue中读取回调函数success并执行。相信通过上面的文字和代码,你对js的执行顺序有了初步的了解。接下来我们来学习进阶话题:setTimeout。3.又爱又恨setTimeout大名鼎鼎的setTimeout不用多说了。大家对他的第一印象就是异步执行可以延迟。我们经常这样实现延迟3秒:setTimeout(()=>{console.log('延迟3秒');},3000)渐渐的,setTimeout用的越来越多,问题也随之而来。有的时候明明写的是延迟3秒,但是实际的功能却执行了5、6秒。怎么了?怎么了?先来看一个例子:setTimeout(()=>{task();},3000)console.log('Executeconsole');根据我们之前的结论,setTimeout是异步的,应该先执行console.log的同步任务,所以我们的结论是://executeconsole//task()验证一下,结果是正确的!然后我们修改之前的代码:setTimeout(()=>{task()},3000)sleep(10000000)乍一看差不多,但是我们在chrome中执行这段代码,却发现控制台执行的是task()耗时远不止3秒,约定的延迟是3秒,为什么现在要这么久?这时候我们需要重新理解一下setTimeout的定义。先说一下上面代码是如何执行的:task()进入EventTable并注册,开始计时。执行sleep函数很慢,很慢,计时还在继续。3秒到,计时事件超时完成,task()进入EventQueue,但是sleep太慢了,还没执行完,只好等待。sleep终于执行完了,task()终于从EventQueue进入主线程执行。上述过程完成后,我们知道setTimeout函数是在指定时间后将要执行的任务(本例中为task())添加到EventQueue中,由于是单线程任务,所以需要被一一执行。如果前面的任务耗时太长,那么我们只能等待,导致真正的延迟时间远远超过3秒。我们也经常遇到类似setTimeout(fn,0)这样的代码,0秒后执行是什么意思?可以立即执行吗?答案是不。setTimeout(fn,0)的意思是指定一个任务在主线程最早可用的空闲时间执行,也就是说不需要等待很多秒,只要主线程执行完所有的任务堆栈中的同步任务。执行完成后,栈为空时会立即执行。例如://代码1console.log('先执行这里');setTimeout(()=>{console.log('执行')},0);//代码2console.log('先执行这里');setTimeout(()=>{console.log('执行')},3000);代码1的输出是://首先在这里执行//执行代码2的输出是://首先在这里执行//...3秒后//执行。setTimeout需要补充的是,即使主线程为空,0毫秒其实也是不可达的。根据HTML标准,较低的是4毫秒。感兴趣的同学可以自行学习。4.又爱又恨setInterval上面说了setTimeout,当然不能少了它的孪生兄弟setInterval。它们很相似,只是后者是循环执行。对于执行顺序,setInterval每隔指定的时间就会将注册的函数放入EventQueue中,如果之前的任务耗时太长,也需要等待。需要注意的一点是,对于setInterval(fn,ms),我们已经知道fn不会每ms秒执行一次,而是每ms秒就会进入EventQueue。一旦setInterval的回调函数fn的执行时间超过延迟时间ms,那么就根本没有时间间隔了。请仔细阅读这句话。5.Promise和process.nextTick(callback)我们研究了传统的定时器,接下来我们来探究Promise和process.nextTick(callback)的性能。Promise的定义和作用本文不再赘述。不懂的读者可以向阮一峰老师学习Promise。而process.nextTick(callback)类似于node.js版本的“setTimeout”,在事件循环的下一个循环中调用callback回调函数。让我们进入正题。除了广义的同步任务和异步任务,我们对任务还有更细化的定义:macro-task(宏任务):包括整体代码脚本、setTimeout、setIntervalmicro-task(微任务):Promise、process.nextTick不同不同类型的任务都会进入对应的EventQueue,比如setTimeout和setInterval都会进入同一个EventQueue。事件循环的顺序决定了js代码的执行顺序。进入整体代码(宏任务)后,开始第一个循环。然后执行所有微任务。然后再从宏任务开始,找到其中一个要执行的任务队列,然后执行所有的微任务。听起来有点乱,我们用文章的第一段代码来说明:setTimeout(function(){console.log('setTimeout');})newPromise(function(resolve){console.log('promise');}).then(函数(){console.log('then');})console.log('console');这段代码作为宏任务进入主线程。先遇到setTimeout,注册它的回调函数,分发到宏任务EventQueue中。(注册过程同上,下面不再赘述。)接下来,当遇到Promise时,立即执行新的Promise,然后将then函数分发到微任务EventQueue中。遇到console.log(),立即执行。好了,整体代码脚本作为第一个宏任务执行,我们来看看都有哪些微任务?我们发现then是在microtaskEventQueue中执行的。好了,第一轮事件循环结束了,我们开始第二轮循环,当然是从宏任务EventQueue开始。我们在宏任务EventQueue中找到setTimeout对应的回调函数,立即执行。结束。事件循环、宏任务、微任务的关系如图所示:这次要彻底理解JavaScript的执行机制,我们来分析一段更复杂的代码,看看你是否真的掌握了js的执行机制:console.log('1');setTimeout(function(){console.log('2');process.nextTick(function(){console.log('3');})newPromise(function(resolve){console.log('4');resolve();}).then(function(){console.log('5')})})process.nextTick(function(){console.log('6');})newPromise(function(resolve){console.log('7');resolve();}).then(function(){console.log('8')})setTimeout(function(){console.log('9');process.nextTick(function(){console.log('10');})newPromise(function(resolve){console.log('11');resolve();}).then(function(){console.log('12')})})第一轮事件循环流程分析如下:整体脚本作为第一个宏任务进入主线程,遇到console输出1。日志。遇到setTimeout时,将其回调函数分发到宏任务EventQueue中。我们暂且记录为setTimeout1。当遇到process.nextTick()时,它的回调函数被分发到microtaskEventQueue。我们将其表示为process1。遇到Promise,直接执行newPromise,输出7。然后分发到microtaskEventQueue。我们将其表示为then1。又遇到了setTimeout,它的回调函数被分发到宏任务EventQueue中,我们记录为setTimeout2。这一次,彻底理解JavaScript执行机制这一次,彻底理解JavaScript执行机制第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。输出3.输出5.第二轮事件循环结束,第二轮输出2,4,3,5.第三轮事件循环开始,此时只剩下setTimeout2,执行。直接输出9。分发process.nextTick()到微任务事件队列。将其表示为process3。直接执行newPromise,输出11,将then分发到microtaskEventQueue中,记为then3。这次彻底理解了JavaScript的执行机制,第三轮事件循环宏任务执行结束,执行process3和then3两个微任务。输出10.输出12.第三轮事件循环结束,第三轮输出9,11,10,12.整个代码,一共三个事件循环,完整输出为1,7,6,8,2,4,3,5,9,11,10,12。(注意node环境下的事件监听依赖libuv和前端环境不完全一样,可能输出顺序不对)6.写在最后(一)js异步我们一开始就说javascript是单线程语言,不管新框架新语法糖实现了什么样的所谓异步,其实都是模拟的一种同步方法。牢牢抓住单线程非常重要。(2)事件循环EventLoop事件循环是js实现异步的一种方法,也是js的执行机制。(3)javascript的执行和运行有很大的区别。JavaScript在不同的环境下执行,比如node、browser、Ringo等,执行方式不同。操作多参考javascript解析引擎,统一。(4)setImmediate微任务和宏任务的种类很多,比如setImmediate等,它们在执行上都有相似之处。感兴趣的同学可以自行学习。(5)最后javascript是单线程语言。EventLoop是javascript的执行机制。牢牢抓住两个基本点,专注于认真学习javascript,早日实现成为前端高手的伟大梦想!
