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

JavaScript运行原理分析

时间:2023-03-12 11:25:49 科技观察

说到JavaScript运行原理,自然要避开JS引擎、运行上下文、单线程、事件循环、事件驱动、回调函数等概念。为了更好地理解JavaScript是如何工作的,我们必须首先理解以下概念。JSEngine(JS引擎)Runtime(运行上下文)CallStack(调用栈)EventLoop(事件循环)Callback(回调)1.JSEngine简单来说,JS引擎主要是对JS代码进行词法和语法分析,通过编译器将代码编译成可执行的机器码供计算机执行。目前最流行的JS引擎是V8。Chrome浏览器和Node.js使用的引擎是V8引擎。引擎的结构可以用下图简单表示:和JVM虚拟机一样,JS引擎也有堆(MemoryHeap)和栈(CallStack)的概念。堆。用于存放方法调用的地方,以及基本数据类型(比如vara=1)也存放在栈中,会随着方法调用结束自动销毁(入栈-->方法后调用-->从堆栈中弹出)。堆。JS引擎中分配给对象的内存空间是放在堆中的。如varfoo={name:'foo'}那么这个foo指向的对象就存放在堆中。另外,JS中还有一个闭包的概念。如果基本类型变量存在于闭包中,它也会被存储在堆中。有关详细信息,请参阅此处关于闭包的1和3,其中涉及捕获的变量。我们知道LocalVariables是最简单的情况,直接存放在栈中。而CapturedVariables是针对闭包和with的存在变量,trycatch。functionfoo(){varx;//localvariablesvary;//捕获的变量,bar引用yfunctionbar(){//bar中的上下文会捕获变量yuse(y);}returnbar;}如上,变量y存在,bar()在闭包,所以y是一个捕获变量,存放在堆中。2、RunTimeJS可以在浏览器中调用浏览器提供的API,比如window对象,DOM相关API等。这些接口不是V8引擎提供的,而是存在于浏览器中。所以简单来说,就是这些相关的外部接口可以在运行时被JS调用,还有JS的事件循环(EventLoop)和事件队列(CallbackQueue),统称为RunTime。在某些地方,JS使用的corelib核心库也被视为RunTime的一部分。同样,在Node.js中,Node的各种库提供的API都可以称为RunTime。所以可以理解为Chrome和Node.js都使用了相同的V8引擎,只是运行时环境(RunTimeEnvironments)不同[4]。3.CallStackJS被设计成在单线程上运行。这是因为JS主要用来实现很多交互相关的操作,比如DOM相关的操作。如果是多线程,会造成复杂的同步问题。所以JS从一出生就是单线程的,主线程用于界面相关的渲染操作(为什么是主线程,是因为HTML5提供了WebWorker,一个独立的后台JS,用来处理一些耗时的数据操作。因为不会修改相关的DOM和页面元素,所以不会影响页面性能),如果出现阻塞,浏览器会卡顿。如果递归调用没有终止条件,是死循环,会因为内存不足导致调用栈溢出,如:functionfoo(){foo();}foo();例子中foo函数在没有终止条件的情况下循环调用自己,浏览器控制台输出调用栈达到最大调用次数。JS线程遇到读取文件、AJAX请求操作等耗时操作怎么办?这里JS使用Callback回调函数来处理。对于CallStack中的每一个方法调用,都会形成自己的执行上下文ExecutionContext。关于执行上下文的详细解释可以参考这篇文章4.EventLoop&CallbackJS通过回调异步处理耗时任务。一个简单的例子:varresult=ajax('...');控制台日志(结果);此时复制代码是拿不到result的值的,result是undefined。这是因为ajax调用是异步的,当前线程并没有等待ajax请求的结果才执行console.log语句。而是将调用ajax后请求的操作交给回调函数,它立即返回。正确的写法应该是:ajax('...',function(result){console.log(result);})这样才能正确输出请求返回的结果。JS引擎实际上并不提供异步支持,异步支持主要取决于运行环境(浏览器或Node.js)。因此,例如,当您的JavaScript程序发出Ajax请求以从服务器获取一些数据时,您在函数中设置“响应”代码(“回调”),然后JS引擎告诉托管环境:“嘿,我现在要暂停执行,但是每当你完成那个网络请求,并且你有一些数据时,请回调这个函数。”然后浏览器被设置为监听来自网络的响应,当它有东西要返回给你,它会通过将回调函数插入到事件循环中来安排要执行的回调函数。以上两段摘自HowJavaScriptworks,通俗地解释了JS是如何调用回调函数实现异步处理的。那么什么是事件循环?EventLoop只做一件事,负责监听CallStack和CallbackQueue。当CallStack中的调用栈运行完毕变空时,EventLoop将CallbackQueue中的第一个事件(实际上是回调函数)放入调用栈并执行,然后继续循环执行这个操作。setTimeout示例和对应的EventLoop动态图:console.log('Hi');setTimeout(functioncb1(){console.log('cb1');},5000);console.log('Bye');关于setTimeout,有一点需要注意。比如上面的例子延迟执行5s,严格来说不是5s。准确地说,它至少会在5秒后执行。因为WebAPI会设置一个5s的定时器,时间到后回调函数会加入到队列中。这个时候回调函数可能不会马上运行,因为之前队列中可能还有其他的回调函数加入,而且还必须等到CallStack为空后才能从队列中取出一个回调函数执行。所以setTimeout(callback,0)的常见做法是在引入正则调用后立即运行回调函数。console.log('Hi');setTimeout(function(){console.log('callback');},0);console.log('Bye');//输出//Hi//Bye//callback说一个容易出错的栗子:for(vari=0;i<5;i++){setTimeout(function(){console.log(i);},1000*i);}//输出:上面栗子的55555不是输出0,1,2,3,4,第一反应觉得应该是这样的。但是梳理完JS的时间循环,应该就很容易理解了。调用栈先执行for(vari=0;i<5;i++){...}方法,里面的timer会在时间到后直接将回调函数放入事件队列,等待for循环按顺序执行完取出来放到调用栈中。for循环执行的时候,i的值变成了5,所以最后输出的全是5。关于定时器,可以看这篇有意思的文章。最后,关于EventLoop,大家可以参考这个视频。至此所说的事件循环就是前端浏览器中的事件循环。关于NodejsEventLoop的详细介绍,请看我的另一篇文章Node.js设计模式:Reactor(事件循环)。两者的比较可以查看这篇文章你不知道的事件循环,对两种事件循环进行了总结和比较。总结最后总结一下,JS的运行原理主要有以下几个方面:JS引擎主要负责将JS代码转换成机器可以执行的机器码,并提供一些JS代码中调用的WEBAPI由其运行环境。是浏览器。JS是单线程运行,每次都是把代码从调用栈中取出来调用。如果当前代码非常耗时,就会阻塞当前线程,导致浏览器卡顿。回调函数被添加到事件队列中,等待EventLoop将其取出并放入调用栈中进行调用。只有当EventLoop检测到调用栈为空时,才会从事件队列中取出回调函数,放入调用栈中。