作者简介
钟钦成,网名司徒正美,著名的JavaScript专家,去哪儿网前端架构师。在GITHUB拥有复数个著名的轮子,著有《javascript框架设计》一书。本文来自司徒正美在“携程技术沙龙——新一代前端技术实践”上的分享。
*视频由“IT大咖说”提供,时长约45分钟,请在WiFi环境下观看*
去哪儿网在React Native深耕多年,对React内部实现的了解在国内应该是非常领先的。迫于项目对React体积的极致需求,我们推出了自己的迷你化方案——Qreact。
Qreact比市面上的其他迷你react框架的实用性更强,基本上可以说无缝切换到任何已有的react 15工程中,能极大地改善对体积的压力,这在对流量非常苛刻的移动端上尤其重要!
我们从今年1月份快速启动项目,在1个月内大致完成了功能,Demo,并配合现有的复杂例子进行验收。本文将分享我们在做这轮子过程中的一些想法,包括竞品分析,实现思路,项目风险控制等等。
一、核心需求
我们并不是无事找事,为造轮子而造轮子。虽然有KPI的成分,但它的核心需求是来自业务线,可以说就算我们不造,公司内部其他人也会造一个,但那个质量可能无法保证,毕竟公司绝大部分的高手都分配到我们事业部。
由于没有产品经理,我们需要充当产品经理的角色,聆听业务线的需求,自己挖掘需求。
去哪儿深耕React多年,构建了两个基于React的UI库,它们都是用于移动端。如果这些库都是内置在APP中,应该没有要求。但是去哪儿分成十来个事业部,根据事业部的赚钱能力分配更新包的体积。为了能让用户在wifi上更新我们的APP,更新包的体积一般不超过100MB,因此像这样公用的框架与库体积越少越好。
此外,其中一个UI库是用于手机浏览器上,我们称之为React Web,用户每次打开我们的页面,都会加载一遍React与相关组件,这个对体积就更加敏感。因此当我们完成React Web,就着眼于迷你React的开发。
这个新的框架有三个核心需求:
1、体积小。移动端对体积一向敏感,因此在jQuery时代,zepto能割踞一方。
2、支持事件系统,这不是简单add Event Listener,是React原来的那套Synthetic Event。它帮我们搞定300ms延迟,还有滚动列表时误触发点击的问题。如果你不用它,你需要让业务线参照iscroll的原理自造一个。
3、能直接替换。换言之,新框架与原框架的功能几乎一致。因此许多业务已经
用React开发完毕,不希望做太多改动。由于业务线有时时间赶,碰到难题搞不定,会倾向用一些怪招歪招。在无法预料对方用什么API的情况,新框架框架覆盖原React的各种偏门用法。
下面是一个紧急修复的补丁:
图1
我们列举一下各种偏门的API与用法
1、mixin包含mixin。这个在RN很常见。
2、ref, setState传函数的用法
3、context与getChildContext的运用,虽然官方明确不建议大家用,但是著名的react-redux在源码里用到了。
4、_rootNodeID, _hostParent,_hostNode这些内部属性用在后端渲染与事件系统中。
二、竞品分析
图2
在立项后,我们开始找市场上的同类产品,如果有满足的,我们就不用开发了。目前,前端要找这些框架,只有一个去处,就是GITHUB。这是开源界的宝库,应有尽有,琳琅满目。
由于代码公开,大家可以抄抄,因此每流行一样的东西,大家都是一窝蜂上的。除开那些纯练手的项目,每个库都有自己独到之处。
自从React推出虚拟DOM来解决复杂应用的性能问题以来,GITHUB上有上百个虚拟DOM的库,包括之前的angular, vue2都在底层使用这种性能利器。
图3
这是一些虚拟DOM框架或库的数据,从相似度,性能,流行度,版本更新等情况综合考虑,我们也只能选上面三者:inferno, preact, react-lite。
inferno从各方面来看,是无可挑剔的,性能比排行第二的kivi快20倍,更不用说vue,angular什么之流。每个库都会吹自己的框架有多快,但inferno的主页上有大量测试页面,是有真实数据支撑的。但是它偏面追求性能,源码里的可读性太差。看不懂,无从入手,只能遗憾地放弃了。
其他性能流有citijs, snabbdom, virtual-dom。最早搞出性能引擎的是citijs,然后基于它上面分化出kivi, ivi, snabbdom,然后vue2.0又直接将snabbdom库整合到它里面。virtual-dom则是走另一种性能优化方式。但它们都是迷你库,API与React差太远。
于是只剩下preact与react-lite。
三、设计思路
由于是业务线的迫切需求,并且拖得越久,就越多项目用上RN,到时需要回归测试的项目就越多,因此必须尽快搞出来。我们就不打算重造轮子,而是在已有轮子上改改。
第一版是基于react-lite。这是因为react-lite是携程的工业聚大神写的,携程是我们的兄弟公司,应该比较好交流。但现实中发现,这个库的扩展性不足,比如说事件系统那里,需要传入4个参数,在react-lite里只能拿到三个参数,想尽方法也无法凑齐第四参数。还有一些内部属性,渲染流程与原装React差得太远了。在双方折腾了2个星期后,我们组有人心灰意冷,着手后备方案,preact。
preact比起react-lite多出几个优势:
1、官方提供兼容补丁preact-compat
2、插件巨多
3、ISSUR活跃,当天提问题,大概到晚上,外国人起床就有回应了。
4、扩展方便
尤其第4点,在开发qreact时,我们都为双方提了不少ISSUE。其实程序员还是比较腼腆,不愿麻烦人,因此我们写框架时还是多留一些扩展接口吧。
整个qreact的架构大概就是:
qreact= preact改+preact-compat改+react-web事件系统迷你版
在preact的源码里一个叫options.js的文件,里面有一个options的对象,它会被框架的多个关键方法调用。我们通过为它重新实现某些方法,就达到改写框架的目标。
https://github.com/developit/preact/blob/master/src/options.js
图4
两个react-lite的难点问题,由于options的扩展机制太灵活了,一下子被摆平。
1、事件系统需要传入4个参数的问题。在options添加一个handle Event方法。
2、内置属性问题,在options重写vnode方法。
重点说一下内部属性问题:
图5
随着版本的升级,这些内部属性越来越多,这里讲解一下其中三个:
图6
这了让preact支持它们,我们是在框架diff节点时,重新添加上它们的。因为这时,我们能轻松知道一个节点在DOM树的上下关系。
最后是对事件系统进行瘦身。React有16000行,其中10000行都是事件系统相关的。再加上React Native中的Pan Responder系统。这体积非常庞大。但是如果我们将要支持的浏览器收窄一点,不支持IE系列与firefox系列。起码在事件对象的构造器上,我们可以做一些合并操作。
下面React中的事件构造器列表:
https://github.com/facebook/react/tree/v15.3.2/src/renderers/dom/client/syntheticEvents
它们浓缩成一个事件构造器后,代码少了3000行。
我们再对事件插件进行围剿。因为我们不需要mouseenter, mouseleave, input, composition,beforeinput的兼容,又可以减少许多行。
https://github.com/facebook/react/tree/v15.3.2/src/renderers/dom/client/eventPlugins
最后成果是 qreact缩少到6000行,事件系统占其中的4000行,min后的体积为39kb。原版React的min体积是140kb。减少近80kb。
体积算是达标了,那么性能如何呢?毕竟我们使用React的初衷是因为它的性能太好了。React的性能主要来自它的虚拟DOM的diff算法。体积缩水了,它的diff算法肯定也打折扣。这时preact提出两个后备方案:
1、减少要比较的虚拟DOM的数量 hydrate。这是发端于inferno的优化方案,通过合并相邻的字符串或数字,减少虚拟DOM数量。
图7
2、减少要生成的真实DOM的数据recycle。上面的hycycle也会减少真实DOM的数量,但我们还可以将要移除的真实DOM保存起来,重复利用这些真实DOM。
通过这两种机制,大大弥补qreact diff算法的缺憾。此外,我们还可以通过动静分离的方式来提高性能。在定义JSX时,我们就能得知某个元素是否包含花括号,有花括号说明其是动态的,反之是静态的,但一个元素与其所有子孙都没有花括号,那么这个子树可以整体缓存起来,以后转换为真实DOM后,它能缓存起来。
然后在组件的render方法中,对于这部分的React Element每次返回相同的对象,并且在上面添加一个标记,碰到两个对象都有这个标记,就直接返回,不往下比较了。这是inferno提出的另一个性能优化方案。
最后验证性能是用ListView进行测试的,和原来一样流畅。
四、分享展示
里面最重要的两个例子就是yo-demo与qunar-react-native-web