当前位置: 首页 > Web前端 > HTML5

优秀程序员的Web前端教程分享JavaScript的执行机制!

时间:2023-04-05 23:57:16 HTML5

优秀的程序员web前端教程分享JavaScript执行机制!Web工程师了解JavaScript的执行机制很重要。下面就让我们一起来看看吧!一、关于JavaScriptJavascript是一种单线程语言。Web-Worker是在最新的HTML5中提出的,但javascript的核心是单线程并没有改变。所以所有javascript版本的“多线程”都是用单线程模拟的,所有javascript多线程都是纸老虎!2.JavaScript事件循环由于js是单线程的,它就像一个只有一个窗口的银行。客户需要排队办理业务。同样,js任务也要一个一个执行。如果一个任务花费的时间太长,那么后一个任务也必须等待。那么问题来了,如果我们想浏览新闻,但是新闻中包含的超高清图片加载的很慢,我们的网页是不是应该一直卡到图片全部显示出来?因此,聪明的程序员将任务分为两类:1)同步任务2)异步任务当我们打开网站时,网页的渲染过程就是很多同步任务,比如页面骨架和页面元素的渲染。而加载图片、音乐等占用资源多、耗时长的任务,则属于异步任务。这部分有严格的文字定义,但本文的目的是以最小的学习成本彻底理解执行机制,所以我们用一张图来说明:如果图中要表达的内容用文字表达:同步和异步任务分别进入不同的执行“地方”,同步进入主线程,异步进入EventTable,注册函数。·当指定的事情完成后,EventTable会将这个函数移入EventQueue。·如果主线程中的任务执行完后为空,就会去EventQueue中读取相应的函数,进入主线程执行。·上述过程会不断重复,通常称为事件循环(eventloop)。我们不禁要问,我们怎么知道主线程执行栈是空的呢?js引擎有一个监控进程,会不断检查主线程执行栈是否为空。一旦为空,就会去EventQueue中查看是否有等待调用的函数。说了这么多文字,不如直接来一段代码更直白:1letdata=[];2$.ajax({3url:www.javascript.com,4data:data,5success:()=>{6console.log('发送成功!');7}8})9console.log('代码执行结束');以上是一段简单的ajax请求代码:Ajax进入EventTable,注册回调函数成功。·执行console.log('代码执行结束')。·ajax事件完成,回调函数success进入EventQueue。·主线程从EventQueue中读取回调函数success并执行。相信通过上面的文字和代码,你对js的执行顺序有了初步的了解。接下来我们来学习进阶话题:setTimeout。3.大名鼎鼎的setTimeout就不用多说了。你对它的第一印象是异步执行可以延迟。我们经常这样实现延迟3秒:1setTimeout(()=>{2console.log('delayTime3seconds');3},3000)渐渐的,setTimeout用的越来越多,问题也出现了.有的时候明明写的是延迟3秒,但是函数实际执行了5、6秒。怎么了?啊?先来看一个例子:1setTimeout(()=>{2task();3},3000)4console.log('Executeconsole');根据我们之前的结论,setTimeout是异步的,应该先执行console.log同步任务,所以我们的结论是:1//执行console2//task()验证一下,结果是正确的!然后我们修改之前的代码:1setTimeout(()=>{2task()3},3000)45sleep(10000000)乍一看差不多,但是我们在chrome中执行这段代码的时候,发现console执行task()的时间远远超过3秒,约定的延迟是3秒。为什么现在需要这么长时间?这时候我们需要重新理解一下setTimeout的定义。先说一下上面代码是如何执行的:task()进入EventTable并注册,开始计时。·执行休眠函数,很慢很慢,计时还在继续。·3秒到,计时事件超时完成,task()进入EventQueue,但是sleep太慢了,还没执行完,只好等待。·sleep终于执行完,task()终于从EventQueue进入主线程执行。上述过程完成后,我们知道setTimeout函数是在指定时间后将要执行的任务(本例中为task())添加到EventQueue中,由于是单线程任务,所以需要被一一执行。如果前面的任务耗时太长,那么我们只能等待,导致真正的延迟时间远远超过3秒。我们也经常遇到类似setTimeout(fn,0)这样的代码,0秒后执行是什么意思?可以立即执行吗?答案是不。setTimeout(fn,0)的意思是指定一个任务在主线程最早可用的空闲时间执行,也就是说不需要等待很多秒,只要主线程执行完所有的任务堆栈中的同步任务。执行完成后,栈为空时会立即执行。例如:1//code12console.log('先执行这里');3setTimeout(()=>{4console.log('执行')5},0);1//code22console.log('先执行这里');3setTimeout(()=>{4console.log('Execute')5},3000);代码1的输出是:1//先在这里执行2//先执行3代码2输出结果是:45//先在这里执行6//...3s后7//再执行。setTimeout要补充的是,即使主线程为空,0毫秒其实也是不可达的。根据HTML标准,最小值为4毫秒。感兴趣的同学可以自行学习。4、上面说完setIntervalsetTimeout,当然不能少了它的孪生兄弟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代码的执行顺序。进入整体代码(宏任务)后,开始第一个循环。然后执行所有微任务。然后再从宏任务开始,找到其中一个要执行的任务队列,然后执行所有的微任务。听起来有点绕,那我们就用文章开头的代码来说明一下:1setTimeout(function(){2console.log('setTimeout');3})45newPromise(function(resolve){6console.log('promise');7}).then(function(){8console.log('then');9})1011console.log('console');·这段代码作为宏任务进入主线程。·第一次遇到setTimeout时,注册它的回调函数,分发到宏任务EventQueue中。(注册过程同上,下面不再赘述)·当接下来遇到一个Promise时,立即执行新的Promise,然后将then函数分发到microtaskEventQueue中。·遇到console.log()立即执行。·好了,整体代码脚本作为第一个宏任务执行,我们来看看都有哪些微任务?我们发现then是在microtaskEventQueue中执行的。·好了,第一轮事件循环结束,我们开始第二轮循环,当然是从宏任务EventQueue开始。我们在宏任务EventQueue中找到setTimeout对应的回调函数,立即执行。结束。事件循环、宏任务、微任务的关系如图所示:下面分析一段比较复杂的代码,看看你是否真的掌握了js的执行机制:1console.log('1');23setTimeout(function(){4console.log('2');5process.nextTick(function(){6console.log('3');7})8newPromise(function(resolve){9console.log('4');10resolve();11}).then(function(){12console.log('5')13})14})15process.nextTick(function(){16console.log('6');17})18newPromise(function(resolve){19console.log('7');20resolve();21}).then(function(){22console.log('8')23})2425setTimeout(function(){26console.log('9');27process.nextTick(function(){28console.log('10');29})30newPromise(function(resolve){31console.log('11');32resolve();33}).then(function(){34console.log('12')35})36})第一轮事件循环流程分析如下:一个宏task进入主线程,遇到console.log,输出1。·遇到setTimeout,将其回调函数分发到宏任务EventQueue。我们暂且记录为setTimeout1。·当遇到process.nextTick()时,将其回调函数分发到microtaskEventQueue。我们将其表示为process1。遇到Promise,直接执行新的Promise,输出7。然后分发到microtaskEventQueue。我们将其表示为then1。·我们又遇到了setTimeout,它的回调函数被分发到宏任务EventQueue中,我们记录为setTimeout2。MacrotaskEventQueueMicrotaskEventQueuesetTimeout1process1setTimeout2then1·上表显示了第一轮事件循环宏任务结束时各个EventQueue的状态。这时候1和7已经输出了。·我们发现了两个微任务,process1和then1。·执行process1,输出6。·执行then1,输出8。好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:·首先输出2。接下来遇到process.nextTick(),同样分发到microtaskEventQueue,记录为process2。newPromise立即执行output4,然后也分发到microtaskEventQueue,记录为then2。MacrotaskEventQueueMicrotaskEventQueuesetTimeout2process2nullthen2·第二轮事件循环macrotask结束,我们发现有两个微任务process2和then2可以执行。·输出3.·输出5.·第二轮事件循环结束,第二轮输出2,4,3,5.·第三轮事件循环开始,此时只剩下setTimeout2,执行.·直接输出9。·分发process.nextTick()到微任务事件队列。将其表示为process3。·直接执行newPromise,输出11。·将then分发到微任务EventQueue,记为then3。MacrotaskEventQueueMicrotaskEventQueuenullprocess3nullthen3·第三轮事件循环macrotask执行结束,执行process3和then3两个microtask。·输出10。·输出12。第三轮事件循环结束,第三轮输出9、11、10、12。整个代码,一共三个事件循环,完整输出为1、7、6、8、2、4、3,5,9,11,10,12.(注意node环境下的事件监听依赖libuv和前端环境不完全一样,输出顺序可能有错误)6.写在最后(1)js异步我们一开始就说javascript是单线程语言,不管新框架新语法糖实现了什么样的所谓异步,其实都是通过同步方式模拟出来的。牢牢抓住单线程非常重要。(2)事件循环EventLoop事件循环是js实现异步的一种方法,也是js的执行机制。(3)javascript的执行和运行有很大的区别。JavaScript在不同的环境下执行,比如node、browser、Ringo等,执行方式不同。操作多参考javascript解析引擎,统一。(4)setImmediate微任务和宏任务的种类很多,比如setImmediate等,它们在执行上都有相似之处。感兴趣的同学可以自行学习。(5)最后javascript是单线程语言。EventLoop是javascript的执行机制。牢牢抓住两个基本点,专注于认真学习javascript,早日实现成为前端高手的伟大梦想!