前几天在GoogleIO上,V8团队为我们分享了主题《What's New in JavaScript》。看完后,我整理了一个文字版,帮助大家快速了解分享内容。嘉宾们主要分享了以下几点:JS解析速度提升2倍,异步执行速度提升11倍,内存占用平均降低20%。在类中初始化的变量可以直接使用类字段,不需要写在构造函数中。私有变量前缀string.matchAll用于常规多重匹配。数字分隔符允许我们在写数字时使用_作为分隔符以提高可读性。新的大数类型支持Intl.NumberFormat本地化格式化数字显示Array.prototype.flat(),Array.prototype.flatMap()多层数组扁平化方法Object.entries()和Object.fromEntries()快速对数组进行操作objectsglobalThis是环境无关的Global这个支持Array.prototype.sort()的排序结果稳定输出Intl.RelativeTimeFormat(),Intl.DateTimeFormat()本地化显示时间Intl.ListFormat()本地化显示多个名词列表本地化的各种常量语言查询顶级await无需编写async支持Promise.allSettled()和Promise.any()增加丰富的Promise场景WeakRef类型用于部分变量弱引用减少内存泄漏Async执行速度比之前更快11at比赛一开始,11xfaster这个数字就让大家大吃一惊,很多同学都很好奇这是怎么做到的。其实这个优化不是最近才做的。去年11月,V8团队发表了一篇文章《Faster async functions and promises》,详细描述了如何将async/await优化到这个速度,主要得益于以下三点:TurboFan:新的JS编译器Orinoco:新的GC引擎AnawaitbugonNode.js8Chrome诞生于2008年,10年Chrome引入了Crankshaft编译器。很多年过去了,这位老手已经满足不了现有的了,毕竟当时的笔者也没想到前端世界会发展的这么快。至于为什么要用TurboFan代替Crankshaft,可以看《Launching Ignition and TurboFan》,原文是这样说的:Crankshaft只支持优化JavaScript的部分特性。它不通过结构化的异常处理来设计代码,即代码块不以try、catch、finally等关键字来划分。另外,Cranksshaft针对每一个新特性都要做九套不同的框架代码来适应不同的平台,所以适应新的Javascript语言特性也非常困难。此外,Crankshaft框架代码的设计也限制了优化机器代码的扩展。虽然V8引擎团队为每个芯片架构维护了10000多行代码,但Crankshaft只能为Javascript榨取一点性能。via:《Javascript是如何工作的:V8引擎的内核Ignition和TurboFan》和TurboFan提供了更好的架构,可以在不修改架构的情况下增加新的优化特性,为面向未来的JavaScript语言特性优化提供了良好的架构支持,让团队可以花更多的时间去处理不同平台的功能和编码。从原文的数据对比可以看出,仅仅改变编译器优化大约是8倍。。。给V8的大佬们跪下。Orinoco新的GC引擎使用单独的线程进行异步处理,这样就不会影响JS主线程的执行。至于最后一个async/awaitBUG,引起了V8团队的思考,把原来基于3个Promises的async/await实现减少到2个,最后减少到1个!最后,写async/await比直接写Promise更快。我们知道await后面跟着一个Promise对象,但是即使不是PromiseJS也会帮我们把它包装成一个Promise。在V8引擎中,要实现这种封装,至少需要一个Promise和两个microtask进程。这在已经是Promise的情况下有点失落。为了实现async/await需要在await之后恢复原函数的上下文并提供await之后的结果,此时规范定义还需要一个Promise,这在V8团队看来是不必要的,所以他们建议规范删除这个功能。最后,官方还建议我们:使用async/await代替手写Promise代码,使用JavaScript引擎提供的Promise代替自己实现。NumericSeperator随着Babel的出现,JS中已经没有多少语法糖了,NumericSeperator就是其中之一。简单的说,就是在我们手写数字的时候提供了分隔符的支持,让我们在写大数字的时候更易读。其实就是一个很简单的语法糖,为什么会单独列出来,主要是刚好解决了我之前实现的痛点。我有一个需求是一堆文章数据,我想按照产品给的规则插入广告。如图,非红框为文章,红框为广告。由于插入规则会根据产品的需求(感受)经常变化,所以我们最初使用两个变量来标记:constnews=[1,3,5,6,7,9,10,11];constads=[2,4,8,12];当位置发生变化时,我们需要同时修改这两个变量,这就导致了维护成本。于是想了个办法,广告的位置标为1,文章的位置标为0,一条记录用纯二进制表示,于是变成:+---+---+---+|0|1|0|+---+---+---+|1|0|0|+---+---+---+|0|1|0|+---+---+---+|0|0|1|+---+---+---+1011010100010001//第一位为常数1//2-4位记录每行行数//后续记录根据新闻和广告的位置最后我们用一个变量0b1011010100010001来完成两种信息的记录。这样大量的数据整合在一起,解决了我们之前的问题,但是又带来了新的问题。还可以看到,如果按照空格拆分注释,也能看懂,但是把空格放在段首。去除后,造成阅读困难。数字分隔符的语法糖正好可以解决这个问题,0b1_011_010_100_010_001这样读起来就容易多了。虽然Promise配合async/await可以在大部分场景下使用,但是很遗憾Promise的某些场景还是不可替代的。Promsie.all()和Promise.race()就是这样的特例。Promise.allSettled()和Promise.any()是新添加的方法。与他们的前辈相比,这两位具有忽略错误以达到目的的特点。我们需要将文件安全地存储在存储服务中。对于容灾,我们其实有两台S3,一台HBase,一台本地存储。所以每次都需要这样的逻辑:for(constserviceofservices){constresult=awaitservice.upload(file);if(result)break;}但是我其实并不关心错误,我的目的只是为了确保它最后只需要有一个可以成功执行的服务,所以Promise.any()实际上可以解决这个问题.等待Promise.any(services.map(service=>service.upload(file)));Promise.allsettled()和Promise.any()的引入,丰富了Promise更多的可能性。以后可能会加入更多的特性,比如Promise.try(),Promise.some(),Promise.reduce()...WeakRefWeakRef这个新类型一开始我不是很理解,毕竟我总觉得ChromeHe已经长大到可以自己处理垃圾了。然而,事情并没有我想的那么简单。我们知道JS垃圾回收主要有两种方式:“标记清理”和“引用计数”。引用计数就是只要变量被引用一次,计数就加1,少一个引用,计数就减1。当引用为0时,说明你有没有使用价值,去垃圾桶!在WeakRef之前,已经有两个类似的类型,分别是WeakMap和WeakSet。以WeakMap为例,它规定它的Key必须是一个对象,它的对象都是弱引用的。例如://map.jsfunctionusageSize(){constused=process.memoryUsage().heapUsed;返回Math.round(used/1024/1024*100)/100+'M';}global.gc();console.log(usageSize());//≈3.23Mletarr=newArray(5*1024*1024);constmap=newMap();map.set(arr,1);global.gc();console.log(usageSize());//≈43.22Marr=空;全球.gc();console.log(usageSize());//≈43.23M//weakmap.jsfunctionusageSize(){constused=process.memoryUsage().heapUsed;返回Math.round(used/1024/1024*100)/100+'M';}global.gc();console.log(usageSize());//≈3.23Mletarr=newArray(5*1024*1024);constmap=newWeakMap();map.set(arr,1);global.gc();console.log(usageSize());//≈43.22Marr=空;global.gc();console.log(usageSize());//≈3.23M分别执行node--expose-gcmap.js和node--expose-gcweakmap.js来找出区别。arr和Map都保留了对数组的强引用,所以单纯清空Map中的arr变量内存并不会释放它,因为Map还是有引用计数的。在WeakMap中,它的key是弱引用,不计入引用计数,所以当arr清零时,数组会被回收,因为引用计数为0。分享中提到,WeakMap和WeakSet已经足够好了,但是要求key必须是对象,在某些场景下不太实用。所以他们公开了更方便的WeakRef类型。Python中还有一个WeakRef类型,它做类似的事情。其实我们主要注意WeakRef的引用不计算引用计数,这个很好理解。比如MDN中提到的无法清理引用计数的循环引用问题:functionf(){varo={};varo2={};o.a=o2;//o指的是o2o2.a=o;//o2引用oreturn"azerty";}f();如果使用WeakRef重写,由于WeakRef不计算引用计数,因此计数一直为0,下次回收时会正常回收。函数f(){varo=newWeakRef({});varo2=o;o.a=o2;返回“azerty”;}f();我们向主进程发送数据的方式如下:constmetric='event';global.DATA[metric]={};process.on(metric,()=>{constdata=global.DATA[metric];删除global.DATA[metric];返回数据;});代码看起来很奇怪,因为global.DATA[metric]是强引用,如果直接在事件中返回global.DATA[metric],由于引用计数的存在,这个全局变量一直占内存。此时如果使用WeakRef重写,可以减少删除逻辑。constmetric='事件';global.DATA[metric]=newWeakRef({});process.on(metric,()=>{constref=global.DATA[metric];if(ref!==undefined){returnref.deref();}返回参考;});后记除了我上面提到的几个特性外,还有很多其他的特性也很不错。例如,String.matchAll()允许我们进行多次匹配而无需再写while!Intl本地化类的支持让我们早日抛弃moment.js,尤其是RelativeTimeFormat类确实解放了我们的生产力,但是目前界面的配置好像更加定制化,不知道后续如何-向上细粒度的需求支持将是。参考资料:《ES proposal: numeric separators》《内存管理》《ES6 系列之 WeakMap》
