当前位置: 首页 > 科技观察

Javascript的事件循环机制

时间:2023-03-16 22:02:55 科技观察

单线程JavascriptJavaScript是一种单线程语言,主要用于与用户交互和操作DOM。多线程需要共享资源,可以互相修改运行结果,还有上下文切换。JS运行时UI渲染可能会阻塞,这意味着两个线程是互斥的。这是因为JS可以修改DOM。如果执行JS时UI线程还在工作,可能会导致UI渲染不安全。JS运行在单线程上,可以节省内存,节省上下文切换时间。为了利用多核CPU的计算能力,HTML5提出了WebWorker标准,允许JavaScript脚本创建多个线程,但子线程完全由主线程控制,不得操作DOM。单线程同步等待非常影响效率,任务要一个一个等待执行,这对于web应用来说是不能接受的。所以Javascript使用事件循环机制来解决异步任务的问题。SynchronousvsAsynchronousMacroTaskvsMicroTask首先了解同步和异步的区别:同步:当一个函数返回时,调用者可以获得预期的结果。同步任务:在主线程上排队等待执行的任务。只有完成了上一个任务,才能执行下一个任务。异步:当函数返回时,调用者无法得到预期的结果,需要在以后通过一定的方式获得。异步任务:不进入主线程,而是放在“任务队列”中的任务。如果有多个异步任务,则需要排队等待进入主线程执行栈执行。任务队列其实不止一种。根据任务类型的不同,可以分为微任务队列和宏任务队列。常见任务如下:宏任务:脚本(整体代码)、setTimeout、setInterval、I/O、UI交互事件、setImmediate;需要具体的异步线程执行,有明确的异步任务执行,回调。Microtasks:Promise、MutaionObserver、process.nextTick(Node.js环境,会先于其他microtasks执行);不需要特定的异步线程去执行,没有明确的异步任务去执行,只有回调。一个Eventloop循环将处理一个宏任务和在这个循环中生成的所有微任务。执行顺序如下:第一个例子:varreq=newXMLHttpRequest();req.open('GET',url);req.onload=function(){};req.onerror=function(){};req.send();//相当于varreq=newXMLHttpRequest();req.open('GET',url);req.send();req.onload=function(){};req.onerror=function(){};上面代码中的req.send方法是一个向服务器发送数据的Ajax操作。它是一个异步任务,也就是说只有当当前脚本的所有代码都执行完后,系统才会去读取“任务队列”。指定回调函数的部分(onload和onerror)在send()方法之前或之后无关紧要,因为它们是执行栈的一部分,系统总是在读取“任务队列”之前执行它们。第二个例子:console.log('1第一个循环开始执行');setTimeout(function(){console.log('2第二个循环开始执行');newPromise(function(resolve){console.log('3循环microtask结束');resolve();}).then(function(){console.log('4循环microtask执行')})},0)newPromise(function(resolve){console.log('5个宏任务第一次循环结束');resolve();}).then(function(){console.log('6个微任务第一次循环执行')})setTimeout(function(){console.log('7第三个循环开始执行');newPromise(function(resolve){console.log('8第三个循环宏任务结束');resolve();}).then(function(){console.log('9微任务执行的第三个循环')})},0)/*结果1第一个循环开始执行5第一个循环宏任务结束6第一个循环微任务执行2第二周期开始执行3第二周期宏任务结束2第二周期微任务执行7第三周期startexecution8Thirdcyclemacrotaskend9ThirdtimeCyclicmicrotaskexecution*/timer定时器功能主要由setTimeout()和setInterval()这两个函数完成。它们的内部运行机制是完全一样的。不同的是前者指定的代码是执行一次,而后者是Repeatedly。如果setTimeout()的第二个参数设置为0,则表示当前代码执行完毕后(执行栈清空),立即执行指定的回调函数(间隔0毫秒)。主线程尽可能早的执行,但是没办法保证回调函数一定会在setTimeout()指定的时间执行,因为主线程会执行它指定的回调函数,直到当前代码(执行栈)被执行。因此,单线程无法实现真正??的异步,因为仍然存在阻塞。HTML5标准规定setTimeout()的第二个参数的最小值(最短间隔)不得低于4毫秒,低于这个值会自动增加。在此之前,旧版浏览器将最小间隔设置为10毫秒。另外,对于那些DOM变化(尤其是那些涉及页面重新渲染的),它们通常不会立即执行,而是每16毫秒执行一次。这时候使用requestAnimationFrame()的效果比setTimeout()要好。在Node.js环境中,还提供了另外两个方法:process.nextTick方法可以在当前“执行栈”结束,下一个EventLoop之前触发回调函数。也就是说,它指定的任务总是在这个“事件循环”中触发,发生在所有异步任务之前,也先于所有微任务执行。setImmediate方法在当前“任务队列”的末尾添加一个事件,即它指定的任务总是在后续的EventLoop中执行,这与setTimeout(fn,0)非常相似。多个process.nextTick语句总是在当前“执行栈”中执行一次,而多个setImmediate可能需要多次循环执行。未完待续...Node.js使用V8作为js的解析引擎,使用自己设计的libuv进行I/O处理。libuv是一个事件驱动的跨平台抽象层,封装了不同操作系统的一些底层特性,对外提供了统一的API,事件循环机制也在其中实现。(在Python中,uvloop是asyncio事件循环的完整替代品,也是基于libuv并用Cython编写的。)这种机制与浏览器中的Javascript事件循环机制不同。