【朴灵评注】JavaScript 运行机制详解:再谈Event Loop

PS: 我先旁观下大师们的讨论,得多看书了~
别人说的:“看了一下不觉得评注对到哪里去,只有吹毛求疵之感。 比如同步异步介绍,本来就无大错;比如node图里面的OS operation,推敲一下就可以猜到那是指同步操作(自然不走event loop了);至于watcher啥的,显然只是实现上的特色,即使用同一个queue实现也未尝不可”
【原帖: http://www.ruanyifeng.com/blog/2014/10/event-loop.html 作者:阮一峰】
一年前,我写了一篇《什么是 Event Loop?》,谈了我对Event Loop的理解。
上个月,我偶然看到了Philip Roberts的演讲《Help, I'm stuck in an event-loop》。这才尴尬地发现,自己的理解是错的。我决定重写这个题目,详细、完整、正确地描述JavaScript引擎的内部运行机制。下面就是我的重写。
【JavaScript引擎的内部运行机制跟Event loop没有半毛钱的关系。】
【这里的错误在于要分清楚JavaScript执行环境和执行引擎的关系,通常说的引擎指的是虚拟机,对于Node来说是V8、对Chrome来说是V8、对Safari来说JavaScript Core,对Firefox来说是SpiderMonkey。JavaScript的执行环境很多,上面说的各种浏览器、Node、Ringo等。前者是Engine,后者是Runtime。】
【对于Engine来说,他们要实现的是ECMAScript标准。对于什么是event loop,他们没兴趣,不关心。】
【准确的讲,要说的应该是Runtime的执行机制。】
进入正文之前,插播一条消息。我的新书《ECMAScript 6入门》出版了(版权页,内页1,内页2),铜版纸全彩印刷,非常精美,还附有索引(当然价格也比同类书籍略贵一点点)。预览和购买点击这里。

【新书还是要支持】
一、为什么JavaScript是单线程?
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
【这段没啥大问题,谢谢阮老师】

二、任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时CPU完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
【这个跟Brendan Eich没半毛钱关系。进程在处理IO操作的时候,操作系统多半自动将CPU切给其他进程用了】
于是,JavaScript就有了两种执行方式:一种是CPU按顺序执行,前一个任务结束,再执行下一个任务,这叫做同步执行;另一种是CPU跳过等待时间长的任务,先处理后面的任务,这叫做异步执行。程序员自主选择,采用哪种执行方式。
【纯粹扯蛋。】
【给CPU啥指令它就执行啥,哪有什么CPU跳过等待时间长的任务。】
【归根结底,阮老师没有懂什么叫异步。】
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
【上面这句话表现出不仅不懂什么是异步,更不懂什么是同步。】

(1)所有任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。系统把异步任务放到"任务队列"之中,然后继续执行后续的任务。

(3)一旦"执行栈"中的所有任务执行完毕,系统就会读取"任务队列"。如果这个时候,异步任务已经结束了等待状态,就会从"任务队列"进入执行栈,恢复执行。

(4)主线程不断重复上面的第三步。

 

【上面这段初步地在说event loop。但是异步跟event loop其实没有关系。准确的讲,event loop是实现异步的一种机制】
【一般而言,操作分为:发出调用和得到结果两步。发出调用,立即得到结果是为同步。发出调用,但无法立即得到结果,需要额外的操作才能得到预期的结果是为异步。同步就是调用之后一直等待,直到返回结果。异步则是调用之后,不能直接拿到结果,通过一系列的手段才最终拿到结果(调用之后,拿到结果中间的时间可以介入其他任务)。】
【上面提到的一系列的手段其实就是实现异步的方法,其中就包括event loop。以及轮询、事件等。】
【所谓轮询:就是你在收银台付钱之后,坐到位置上不停的问服务员你的菜做好了没。】
【所谓(事件):就是你在收银台付钱之后,你不用不停的问,饭菜做好了服务员会自己告诉你。】
下图就是主线程和任务队列的示意图。

 

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

【JavaScript运行环境的运行机制,不是JavaScript的运行机制。】

三、事件和回调函数

"任务队列"实质上是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
【任务队列既不是事件的队列,也不是消息的队列。】
【任务队列就是你在主线程上的一切调用。】
【所谓的事件驱动,就是将一切抽象为事件。IO操作完成是一个事件,用户点击一次鼠标是事件,Ajax完成了是一个事件,一个图片加载完成是一个事件】
【一个任务不一定产生事件,比如获取当前时间。】
【当产生事件后,这个事件会被放进队列中,等待被处理】

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当异步任务从"任务队列"回到执行栈,回调函数就会执行。
【他们压根就没有被执行过,何来挂起之说?】
【异步任务不一定要回调函数。】
【从来就没有什么执行栈。主线程永远在执行中。主线程会不断检查事件队列】
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先返回主线程。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动返回主线程。但是,由于存在后文提到的"定时器"功能,主线程要检查一下执行时间,某些事件必须要在规定的时间返回主线程。
【先产生的事件,先被处理。永远在主线程上,没有返回主线程之说】
【某些事件也不是必须要在规定的时间执行,有时候没办法在规定的时间执行】

四、Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
【事件驱动的的实现过程主要靠事件循环完成。进程启动后就进入主循环。主循环的过程就是不停的从事件队列里读取事件。如果事件有关联的handle(也就是注册的callback),就执行handle。一个事件并不一定有callback】

为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)。

 

【所以上面的callback queue,其实是event queue】

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

执行栈中的代码,总是在读取"任务队列"之前执行。请看下面这个例子。

    var req = new XMLHttpRequest();req.open('GET', url);    req.onload = function (){};    req.onerror = function (){};    req.send();

上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取"任务队列"。所以,它与下面的写法等价。

    var req = new XMLHttpRequest();req.open('GET', url);req.send();req.onload = function (){};    req.onerror = function (){};   
【等价个屁。这个调用其实有个默认回调函数,Ajax结束后,执行回调函数,回调函数检查状态,决定调用onload还是onerror。所以只要在回调函数执行之前设置这两个属性就行】
也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列”。

五、定时器

除了放置异步任务,"任务队列"还有一个作用,就是可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。

setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。

console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);

上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。

如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。

setTimeout(function(){console.log(1);}, 0);
console.log(2);

上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行"任务队列"中的回调函数。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。

另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
【定时器并不是特例。到达时间点后,会形成一个事件(timeout事件)。不同的是一般事件是靠底层系统或者线程池之类的产生事件,但定时器事件是靠事件循环不停检查系统时间来判定是否到达时间点来产生事件】

 

六、Node.js的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

请看下面的示意图(作者@BusyRich)。

 

以我对Node的了解,上面这个图也是错的。】
【OS Operation不在那个位置,而是在event loop的后面。event queue在event loop中间】
js —> v8 —> node binding —> (event loop) —> worker threads/poll —> blocking operation
   <—     <—                   <——  (event loop)<—————— event  <——————

 

根据上图,Node.js的运行机制如下。
(1)V8引擎解析JavaScript脚本。(2)解析后的代码,调用Node API。(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。(4)V8引擎再将结果返回给用户。
【完全不是不同的任务分配给不同的线程。只有磁盘IO操作才用到了线程池(unix)。】
【Node中,磁盘I/O的异步操作步骤如下:】
【将调用封装成中间对象,交给event loop,然后直接返回】
【中间对象会被丢进线程池,等待执行】
【执行完成后,会将数据放进事件队列中,形成事件】
【循环执行,处理事件。拿到事件的关联函数(callback)和数据,将其执行】
【然后下一个事件,继续循环】
除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对"任务队列"的理解。

process.nextTick方法可以在当前"执行栈"的尾部----主线程下一次读取"任务队列"之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部触发回调函数,也就是说,它指定的任务总是在主线程下一次读取"任务队列"时执行,这与setTimeout(fn, 0)很像。请看下面的例子(via StackOverflow)。

process.nextTick(function A() {console.log(1);process.nextTick(function B(){console.log(2);});
});setTimeout(function timeout() {console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。

现在,再看setImmediate。

setImmediate(function A() {console.log(1);setImmediate(function B(){console.log(2);});
});setTimeout(function timeout() {console.log('TIMEOUT FIRED');
}, 0)
// 1
// TIMEOUT FIRED
// 2

上面代码中,有两个setImmediate。第一个setImmediate,指定在当前"任务队列"尾部(下一次"事件循环"时)触发回调函数A;然后,setTimeout也是指定在当前"任务队列"尾部触发回调函数timeout,所以输出结果中,TIMEOUT FIRED排在1的后面。至于2排在TIMEOUT FIRED的后面,是因为setImmediate的另一个重要特点:一次"事件循环"只能触发一个由setImmediate指定的回调函数。

我们由此得到了一个重要区别:多个process.nextTick语句总是一次执行完,多个setImmediate则需要多次才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列”!
【10.0版就不用纠正了吧】
process.nextTick(function foo() {process.nextTick(foo);
});

事实上,现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。另外,由于process.nextTick指定的回调函数是在本次"事件循环"触发,而setImmediate指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。

关于setImmediate与setTimeout(fn,0)的区别是,setImmediate总是在setTimeout前面执行,除了主线程第一次进入Event Loop时。请看下面的例子。

setTimeout(function () {console.log('1');
},0);setImmediate(function () {console.log('2');
})

上面代码的运行结果不确定,有可能是1,2,也有可能是2,1,即使setTimeout和setImmediate两个函数互换位置,也是如此。因为这些代码是主线程第一次读取Event Loop之前运行。但是,如果把这段代码放在setImmediate之中,结果就不一样。

setImmediate(function () {setTimeout(function () {console.log('1');},0);setImmediate(function () {console.log('2');})
})
上面代码运行结果总是2,1,因为进入Event Loop之后,setImmediate在setTimeout之前触发。 
还是会出现1, 2的情况。呵呵。不信试试】
(完) 
【准确讲,使用事件驱动的系统中,必然有非常非常多的事件。如果事件都产生,都要主循环去处理,必然会导致主线程繁忙。那对于应用层的代码而言,肯定有很多不关心的事件(比如只关心点击事件,不关心定时器事件)。这会导致一定浪费。】
【这篇文章里没有讲到的一个重要概念是watcher。观察者。】
【事实上,不是所有的事件都放置在一个队列里。】
【不同的事件,放置在不同的队列。】
【当我们没有使用定时器时,则完全不用关心定时器事件这个队列】
【当我们进行定时器调用时,首先会设置一个定时器watcher。事件循环的过程中,会去调用该watcher,检查它的事件队列上是否产生事件(比对时间的方式)】
【当我们进行磁盘IO的时候,则首先设置一个io watcher,磁盘IO完成后,会在该io watcher的事件队列上添加一个事件。事件循环的过程中从该watcher上处理事件。处理完已有的事件后,处理下一个watcher】
【检查完所有watcher后,进入下一轮检查】
【对某类事件不关心时,则没有相关watcher】
 
【最后,如有问题,谢谢指出】

 转自:https://blog.csdn.net/lin_credible/article/details/40143961

 

转载于:https://www.cnblogs.com/duhuo/p/4479116.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/257355.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c语言 strcpy原型,浅谈C语言中strcpy,strcmp,strlen,strcat函数原型

实例如下&#xff1a;//strcat(dest,src)把src所指字符串添加到dest结尾处(覆盖dest结尾处的\0)并添加\0char *strcat(char * strDest, const char *strSrc){char *resstrDest;assert((strDest!NULL)&&(strSrc!NULL));while(*strDest)strDest;while(*strDest*strSrc){s…

angular——更多按钮的上拉菜单(路由跳转)

<button class"btn gray_text_btn list_item" ng-click"action.toMoreOptions()"><i class"icon ion-navicon"></i> </button> <!-------------------- 底部按钮 -----------------------><section class&qu…

Python版——博客网站四 编写日志创建页

2019独角兽企业重金招聘Python工程师标准>>> 开源地址&#xff1a;https://github.com/leebingbin/Python3.WebAPP.Blog 单从编码来说&#xff0c;WebApp开发真正困难的地方在于编写前端页面。前端页面需要混合HTML、CSS和JavaScript&#xff0c;如果对这三者没有深…

c语言0-1匀分布随机数,C++ generate_canonical均匀分布随机数函数用法详解

标准均匀分布是一个在范围 [0&#xff0c;1) 内的连续分布。generate_canonical() 函数模板会提供一个浮点值范围在 [0&#xff0c;1) 内&#xff0c;且有给定的随机比特数的标准均匀分布。它有 3 个模板参数&#xff1a;浮点类型、尾数的随机比特的个数&#xff0c;以及使用的…

第三十四天 how can I 坚持

“不要把所有的鸡蛋放在同一个篮子里”是错误的&#xff0c;投资应该像马克吐温说的那样&#xff0c;要把所有的鸡蛋放在同一篮子里&#xff0c;并小心的看好他。---巴菲特。 那盆花还没死&#xff0c;但是我又能做什么呢&#xff1f;技术。永远的技术。睡觉。转载于:https://w…

01-Swift 介绍

简介 Swift 语言由苹果公司在 2014 年推出&#xff0c;用来撰写 OS X 和 iOS 应用程序2014 年&#xff0c;在 Apple WWDC 发布 几家欢喜,几家愁愁者:只学Object-C的人欢喜者:之前做过java/python/js语言的人历史 2010 年 7 月&#xff0c;苹果开发者工具部门总监 Chris Lattner…

2017—2018 实验报告:实验一

实验一&#xff1a;实验报告 课程&#xff1a;程序设计与数据结构 班级&#xff1a; 1623 姓名&#xff1a; 张旭升 学号&#xff1a;20162329 指导教师&#xff1a;娄嘉鹏 王志强 实验日期&#xff1a;9月25日 实验密级&#xff1a; 非密级 预习程度&#xff1a; 已预习 必修/…

病床呼叫系统有显示屏c语言,病床呼叫系统毕业设计

内容介绍完整版大学病床呼叫系统毕业设计&#xff0c;已修改完格式摘 要医院已经从人工管理模式向智能化方向发展。“病床呼叫系统”可以实现对病房的智能化管理&#xff0c;可实现呼叫、求救警报、信息存储、显示等等功能。患者在住院期间&#xff0c;可能会在任意时间请求医…

1. mybatis批量插入数据

通过list <insert id"saveByList" useGeneratedKeys"true" parameterType"java.util.List"> insert into T_App_Default_User(UserID,AppType,CreateTime)values <foreach collection"list" item"item" index&quo…

iOS开发 - Swift实现清除缓存功能

前言: 开发移动应用时&#xff0c;请求网络资源是再常见不过的功能。如果每次都去请求&#xff0c;不但浪费时间&#xff0c;用户体验也会变差&#xff0c;所以移动应用都会做离线缓存处理&#xff0c;其中已图片缓存最为常见。 但是时间长了&#xff0c;离线缓存会占用大量的…

c语言结构体单元测试,C语言结构体单元练习.doc

C语言结构体单元练习1.有以下定义和语句&#xff1a;struct student{ int age;int num; };struct student stu[3]{{1001,20},{1002,19},{1003,21}};main(){ struct student *p;pstu;…… }则以下不正确的引用是 。A) (p)->num B) p C) (*p).num D) p&stu.age2.有以下结构…

如何开启IIS7以上的“IIS6管理兼容性”

护卫神PHP套件的安装&#xff0c;需要开启“IIS6管理兼容性”&#xff0c; 那么&#xff0c;如何开启IIS7、IIS7.5、IIS8.0的IIS6兼容模式呢&#xff1f; 设置的时候&#xff0c;请参照如下截图&#xff1a; 本文转自黄聪博客园博客&#xff0c;原文链接&#xff1a;http://www…

pop to 特定的UIViewController

1. 我们可以推出到特定的UIViewController 2. 有一个类没有navigationController&#xff0c;以前一般用delegate&#xff0c;我觉得我们可以把引用一个navigationController&#xff0c;然后使用它来推出另一个UIViewController转载于:https://www.cnblogs.com/studyNT/p/4486…

蓝桥杯:矩阵翻硬币

题目地址&#xff1a;http://lx.lanqiao.org/problem.page?gpidT126 这道题强烈建议用java做&#xff0c;毕竟自带BigInteger类。 此题看似是一道模拟题&#xff0c;但由于数据规模很大&#xff08;10的1000次方&#xff09;&#xff0c;只能找规律。规律是最终结果为sqrt(n)*…

ssh服务端口转发详解

端口转发的概念和应用什么是端口转发呢&#xff0c;我们知道&#xff0c;SSH 会自动加密和解密所有 SSH 客户端与服务端之间的网络数据。但是&#xff0c;SSH 还同时提供了一个非常有用的功能&#xff0c;这就是端口转发。它能够将其他 TCP 端口的网络数据通过 SSH 链接来转发&…

W ndoWs文件夹窗口,如何在本地网络中访问-Synology-NAS-上的文件-(Wndows).pdf

如何在本地网络中访问-Synology-NAS-上的文件-(Wndows)如何在本地网络中访问 Synology NAS 上的文件 (Windows)概述Synology NAS 经过专门设计&#xff0c;可快速简单地在本地网络中存储和共享文件&#xff0c;让您直接访问 SynologyNAS 上的文件而没有每次登录 DSM 的麻烦。例…

左移与右移

左移 无论被移动的数是有符号还是无符号&#xff0c;左移一位相当于乘2(在不溢出的情形下) 右移 对于无符号数&#xff0c;右移一位相当于除以2&#xff1b; 对于有符号数&#xff0c;如果还想获得同样右移除以2的效果&#xff0c;就要考虑算数右移&#xff0c;即符号位始终不变…

Serializing Lua objects into Lua Code

The following little snippet allows you to ‘pickle’ Lua objects directly into Lua code (with the exception of functions, which are serialized as raw bytecode). Metatable support is on the way, but for now, it should be useful enough. Example code: view s…

布局管理器android,Android课程---布局管理器之相对布局(一)

下面示例的是在父容器里如何设置按钮的位置&#xff0c;难度&#xff1a;***&#xff0c;重点是找到一个主按钮&#xff0c;设置它的id&#xff0c;然后根据它来设置其他按钮在父容器的位置。代码示例&#xff1a;android:layout_width"match_parent"android:layout_…

【Cocos2d-Js基础教学 入门目录】

本教程视地址频在&#xff1a;九秒课堂 完全免费从接触Cocos2dx-Js以来&#xff0c;它的绽放的绚丽让我无法不对它喜欢。我觉得Js在不断带给我们惊喜&#xff1b;在开发过程中&#xff0c;会大大提升我们对原型开发的利用率&#xff0c;使用Js语言做游戏开发&#xff0c;使游戏…