手机版

彻底理解JavaScript执行机制

时间:2021-08-29 来源:互联网 编辑:宝哥软件园 浏览:

无论你是javascript新手还是老鸟,无论是面试求职还是做日常开发工作,我们经常会遇到这样的情况:给定几行代码,就需要知道输出内容和顺序。因为javascript是单线程语言,所以我们可以得出一个结论:

Javascript按照语句出现的顺序执行。看到这里,读者会打人:我不知道js是一行一行执行的吗?你一定要告诉我?冷静点,就因为js是一行一行执行的,我们觉得js是这样的:

让a=' 1console . log(a);让b=' 2console . log(b);然而,事实上,js是这样的:

settimeout(function(){ console . log(' timer starts ')});新承诺(函数(resolve) {console.log('立即执行for循环');for(var I=0;我10000;I){ I==99 resolve();}}).然后(function () {console.log('执行then function ')});Console.log('代码执行结束');根据js按照语句出现的顺序执行的思想,我自信地写下了输出结果:

//“定时器启动”//“立即执行for循环”//“执行然后函数”//“代码执行结束”去chrome验证,结果完全错了,我一下子就糊涂了。那一行一行的执行呢?

我们真的需要彻底了解javascript的执行机制。

1.关于javascriptjavascript是单线程语言,而Web-Worker是在最新的HTML5中提出的,但是javascript的核心是单线程保持不变。所以所有javascript版本的‘多线程’都是单线程模拟的,所有javascript多线程都是纸老虎!

2.javascript事件周期由于js是单线程,就像一个只有一个窗口的银行,客户需要一个个排队办理业务,js任务也要以同样的方式一个个执行。如果一个任务花费的时间太长,那么下一个任务也必须等待。那么问题来了,如果我们想浏览新闻,但新闻中包含的超清晰图片加载缓慢,我们的网页是否应该一直卡到图片完全显示出来?所以聪明的程序员把任务分为两类:

同步任务异步任务当我们打开一个网站时,网页的呈现过程是一大堆同步任务,比如呈现页面骨架和页面元素。而且占用资源和时间长的任务,比如加载图片和音乐,都是异步任务。这一部分有严格的定义,但本文的目的是以最小的学习成本彻底理解实现机制,所以我们用导图来说明:

指南图中要表达的内容用文字表达:

同步和异步任务分别进入不同的执行场所,同步进入主线程,异步进入事件表,注册函数。当指定的事情完成时,事件表将这个函数移入事件队列。如果主线程中的任务在执行后为空,它将进入事件队列读取相应的函数,并进入主线程执行。上述过程会不断重复,这通常被称为事件循环。我们不禁要问,怎么知道主线程执行栈是空的?js引擎中有一个监控过程,会持续检查主线程执行栈是否为空。一旦为空,它将转到事件队列检查是否有等待调用的函数。

说了这么多话,不如直接一段代码更直白:

让数据=[];$ .Ajax({ URL : www . JavaScript.com,data:data,success3360 ()={console.log('发送成功!');}})console.log('代码执行结束');上面是一个简单的ajax请求代码:

Ajax进入事件表并成功注册回调函数。执行console.log(“代码执行结束”)。ajax事件完成,回调函数成功进入事件队列。主线程从事件队列中读取回调函数成功并执行它。相信通过以上的文字和代码,大家对js的执行顺序有了初步的了解。接下来,我们将学习高级主题:setTimeout。

3.设定爱和恨的界限

关于著名的setTimeout,没有必要多说。大家对他的第一印象就是异步执行可以延迟。我们经常实施延迟3秒的执行:

SetTimeout(()={console.log('延迟3秒');},3000)逐渐地,setTimeout在更多的地方被使用,问题也随之出现。有时,当写得很清楚时,延迟是3秒,但实际功能是在5或6秒内执行的。这有什么不好?

先看一个例子:

setTimeout(()={ task();},3000)console . log(' execute console ');根据我们之前的结论,setTimeout是异步的,应该先执行同步任务console.log,所以我们的结论是:

//执行console//task()验证结果是否正确!然后我们修改前面的代码:settimeout (()={task ()},3000) sleep (1000000)。乍一看,几乎是一样的,但是当我们在chrome中执行这段代码时,我们发现控制台执行task()需要3秒多,约定的延迟是3秒。为什么现在要花这么长时间?

这时,我们需要重新理解setTimeout的定义。让我们从如何执行上述代码开始:

任务()进入事件表并注册,计时开始。执行睡眠功能非常慢,非常慢,计时还在继续。现在是3秒,定时事件超时完成,任务()进入事件队列,但是睡眠太慢,所以还没有完成,所以我们必须等待。Sleep最终完成执行,任务()最终从事件队列进入主线程。上述过程完成后,我们知道setTimeout函数会在指定时间后将需要执行的任务(本例中的task()添加到Event Queue中,由于是单线程任务,需要一个一个执行,如果前一个任务花费的时间太长,就只能等待,导致实际延迟时间远远大于3秒。

我们经常遇到像setTimeout(fn,0)这样的代码。0秒后执行是什么意思?能马上执行吗?

答案是否定的,setTimeout(fn,0)的意思是指定一个任务在主线程最早可用的空闲时间执行,这意味着只要主线程执行栈中的所有同步任务完成,栈为空,就会立即执行。举个例子:

//code 1console.log('先在这里执行');SetTimeout(()={ console . log(' executed ')},0);//code 2console.log('先在这里执行');SetTimeout(()={ console . log(' executed ')},3000);代码1的输出结果是://先在这里执行//执行它。代码2的输出结果是://先在这里执行//.3s以后//执行。关于setTimeout应该补充的是,即使主线程是空的,实际上也无法达到0毫秒。按照HTML的标准,最低是4ms。感兴趣的同学可以自学。4.爱恨情仇集区间

上面说了SetTimeout,但是当然不能错过它的孪生兄弟setInterval。它们类似,只是后者是循环执行。至于执行顺序,setInterval会在每个指定的时间将注册的函数放入事件队列。如果之前的任务耗时太长,也需要等待。

唯一需要注意的是,对于setInterval(fn,ms),我们已经知道fn不会每毫秒秒执行一次,而是每毫秒秒进入事件队列。一旦setInterval的回调函数fn的执行时间超过了延迟时间ms,则根本没有时间间隔。请仔细品味这句话。

5.承诺和过程

我们研究了传统计时器,然后探讨了Promise和process.nextTick(回调)的性能。

承诺的定义和作用在此不再赘述。不懂的读者可以了解一下阮一峰老师的《无极》。而process.nextTick(回调)类似node.js版本的‘setTimeout’,在事件周期的下一个周期调用回调函数。

让我们言归正传。除了广义的同步任务和异步任务,我们还有一个更详细的任务定义:

宏任务:包括整个代码脚本,setTimeout,setInterval微任务:Promise,process.nextTick勾选不同类型的任务会进入对应的事件队列,比如setTimeout和setInterval会进入同一个事件队列。

事件循环的顺序决定了js代码的执行顺序。输入完整代码(宏任务)后,开始第一个循环。然后执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列,执行所有的微任务。听起来有点拐弯抹角。让我们用文章开头的代码来说明:

setTimeout(function(){ console . log(' setTimeout ');})新Promise(函数(resolve){ console . log(' Promise ');}).然后(function(){ console . log(' then ');})console . log(' console ');

作为一个宏任务,这段代码进入主线程。先满足setTimeout,然后注册它的回调函数,分配给宏任务事件队列。(注册过程同上,但后面不再赘述。)接下来,当一个Promise被满足时,新的Promise被立即执行,然后函数被分发到微任务事件队列。遇到console.log(),立即执行。好了,整个代码脚本作为第一个宏任务结束。让我们看看有哪些微任务。我们发现它是在微任务事件队列中执行的。好了,第一轮活动循环结束。我们将开始第二轮循环,从宏任务事件队列开始。我们在宏任务事件队列中找到了setTimeout对应的回调函数,并立即执行。结束。事件周期、宏观任务和微观任务之间的关系如下:

让我们分析一段复杂的代码,看看您是否真正掌握了js的执行机制:

console . log(' 1 ');setTimeout(function(){ console . log(' 2 '));process . nexttick(function(){ console . log(' 3 ');})新Promise(函数(resolve){ console . log(' 4 ');resolve();}).然后(function(){ console . log(' 5 ')})})process . nexttick(function(){ console . log(' 6 ');})新Promise(函数(resolve){ console . log(' 7 ');resolve();}).然后(function(){ console . log(' 8 ')})setTimeout(function(){ console . log(' 9 ');process . nexttick(function(){ console . log(' 10 ');})新Promise(函数(resolve){ console . log(' 11 ');resolve();}).然后(function () {console.log ('12')})第一轮事件循环流程分析如下:

作为第一个宏任务,整个脚本进入主线程,遇到console.log,输出1。遇到SetTimeout,其回调函数被分发到宏任务事件队列。我们暂时把它写成setTimeout1。遇到process.nextTick(),其回调函数被分发到微任务事件队列。我们把它记为流程1。遇到承诺,新承诺直接执行,输出7。然后被分发到微任务事件队列。让我们把它记为1。再次遇到SetTimeout,它的回调函数被分发给宏任务Event Queue,我们记录为setTimeout2。

上表显示了第一轮事件周期宏任务结束时各事件队列的情况,此时已经输出了1和7。我们找到了两个微任务,进程1和进程1。执行进程1,输出6。执行然后1和输出8。好了,第一轮赛事循环正式结束,这一轮的结果是输出1、7、6、8。然后第二轮时间周期从setTimeout1宏任务开始:

先输出2。接下来,我遇到了process.nextTick(),它也被分发给微任务事件队列,并被记录为process2。新的Promise立即执行输出4,然后也分发到微任务事件队列,该队列被记录为then2。

在第二轮事件周期宏任务结束时,我们发现可以执行两个微任务process2和then2。产出3。产出5。第二轮活动周期结束,第二轮输出2、4、3和5。第三轮赛事循环开始,此时只剩下setTimeout2。执行。直接输出9。将process.nextTick()分发到微任务事件队列。将其写成流程3。直接执行新的承诺,输出11。然后分发给微任务事件队列,并记录为3。宏任务事件队列微任务事件队列

流程3

然后3

在第三轮事件循环中,宏任务执行结束,两个微任务process3和then3被执行。

产出10。产出12。

第三轮活动周期结束,第三轮输出9、11、10和12。

整个代码有三个事件周期,完整的输出是1、7、6、8、2、4、3、5、9、11、10、12。(请注意,节点环境中的事件监控依赖libuv与前端环境中的不完全相同,输出顺序可能存在错误。)

6.写在最后

(1)js的异步性1)我们从一开始就说过javascript是一种单线程语言。不管什么新的框架,新的语法,所谓的异步其实都是用同步的方法来模拟的。牢牢抓住单线程非常重要。

(2)事件循环事件循环是js实现异步的方法,也是js的执行机制。(javascript的执行和运行大不相同。JavaScript在不同的环境中以不同的方式执行,如节点、浏览器、Ringo等。而运行大多是指javascript解析引擎,这是统一的。(4)设置即时的微任务和宏任务有很多种,比如设置即时等。它们有一些共同之处,可以被感兴趣的学生自己理解。

(5)最后,最后

Javascript是一种单线程语言。事件循环是javascript执行机制的总结

以上是边肖介绍的JavaScript执行机制,希望对大家有所帮助。如果你有任何问题,请给我留言,边肖会及时回复你。非常感谢您对我们网站的支持!

版权声明:彻底理解JavaScript执行机制是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。