纸头折飞机
欢迎大家加入KitJs官方高级QQ群88093625,讨论前端技术,上海携程招聘H5,iOS,android,产品,设计,交互,测试,有意者发简历到xueduanyang1985@163.com

 

前言:

了解js编译原理的屌丝们都知道,js是单线程的,想当年各路神仙为了实现js的多线程,为了解决innerHTML输出大段HTML卡页面的顽疾,纷纷设计了诸如假冒的“多线程“实现,我自己也在写开源框架KitJs时候,写过类似的组件http://www.cnblogs.com/xueduanyang/archive/2012/05/30/2526422.html ,其原理就是改造代码中的for为setInterval,改递归为尾递归等等,为可怜的刷新率60Hz争取17ms的微弱时间。

当然了,这些都不是真正的多线程。其实W3C很早就有关于纯前端真多线程实现的,就是http://www.w3.org/TR/workers/ ,一直以来打着HTML5的旗号,各大浏览器厂商都都有对应标准的worker实现,具体支持程度我们可以看http://caniuse.com/#feat=webworkers ,值得一提的是咱们的UC也支持WebWorker,这几年随着移动HTML5项目的兴起,手机多核的炒作,现在就连华强北这种屌丝手机都开始以双核为卖点了,而真正从APP或者Web为多线程做优化的项目又有多少开发者呢?

作为一个猥琐的先驱者,本着让用户爽,不爽不舒服斯基的原则,我们率先在触屏项目实现了多个多线程工作模型,本文介绍的多线程渲染模板就是其中的一个。

之前在春田花花群里面,曾今说过我们有个多线程渲染系统,遭到众多同学的反击,今天终于能给我一个机会,为大家展现我们的辛苦付出,欢迎大家看完之后热烈讨论。

架构设计:

上一篇介绍我们的调试系统的时候,我贴过一张咱触屏的架构图,

clip_image002

从图中可以看出,在我们MVVM层次的Modules中,有两个tpl引擎,一个是基本的tpl引擎(引入的是Mustache),被引用指向集成到我们的Common大模块里面了,还有一个是独立的多线程模板引擎(基于HTML5 WebWorker工作模式的双核渲染引擎Mustache+JTemplate)。

该模块名字叫做tplRender(有点土,起名一直是个技术活…),其他业务视图模块VeiwMddules只需要requires模块,通过tplRender.tpl方法渲染模板,默认就会采用多线程工作方式。

我们的多线程工作模板的工作方式见下图

clip_image004

首先,以Page当前页面为一个Application生命周期,在引入该模板的时候,会自动为当前页面初始化一个WebWorker对象,再加上本来的当前工作进程LocalMainThread,组成两个等待作业池

当业务代码执行到模板渲染时,会向tplRender模块买一张门票,门票上记录着将当前页面需要执行模板的相关配置信息(模板所在Dom Element,模板内容,渲染依赖JSON Data,作业总数)以及此次提交给tplRender自动生成的一个MissionId

clip_image006

tplRender模块在内部做协调调度,目前的调度算法实现还比较简单,用的按2取模,均匀分币多个模板渲染任务到Worker和Local(当前主线程)任务队列中,分别计算

clip_image008

没完成一个队列子任务,会默认触发一次回调,用于进度判断,会传递总作业任务剩余数,只有当总业务进度为0时候,表示任务全部完成

clip_image010

计算完毕,收回门票,撕毁门票,告诉主线程去innerHTML渲染页面走人。

clip_image012

这么做的好处是,

首先从原理上看,是将主线程需要同步计算的多个任务打散,分解一半到了异步Worker线程去计算,减轻了主线程的负担

我们都知道

clip_image014

在浏览器中,渲染线程和当前Js主线程是或的关系,但是Worker以及请求和渲染线程是可以并行的,所以使用了异步计算模式既可以计算完一小块即渲染,提高CPU的利用的率

由于是Worker异步计算,对于主线程的页面事件响应,Gif动画,js动画,完全不存在卡住僵死的问题

在浏览器刷新中,有个重要的原则是尽量做局部刷新,不要做整体刷新,刷新范围越小,速度越快,资源消耗越小,我们刚好切合这个原则

另外,可以最大限度的利用现代手机多核的优势,不浪费资源

那么实际效果呢?

我们测试过简单的10X单个1w次整数循环,对比的数据结果是单线程运算比多线程模式平均速度要慢上20%~30%,可以肯定的是,对于复杂的字符计算,正则,多重条件判断等耗时运算,使用多线程双工模式是一个不错的选择。

但是,这里要也要提的是,在简单模板运算上,一般集中在300ms左右的计算中,多线程工作效率要比单线程要低5%左右的差距

为什么会有这5%的差距呢,因为从前面的代码页可以看到,因为异步的加入,导致我们需要维护一系列临时状态去保证正常的执行队列,所以这部分消耗是正常的,另外,不要只看着这5%,要知道这是在牺牲用户触屏响应的前提上做的速度,而多线程方式虽然会慢一点,但是不会牺牲用户触屏的交互响应,也就是常说的不会卡死。

性能

之前群里面讨论,主要集中在对于Worker工作模式的怀疑,下图可以看到Worker工作模式的标准流程

clip_image016

Worker的主要消耗在于

1. 与主线程之间的通信,主要是通过PostMessage(发送),onMessage(接受),消息的回调都是异步的,且触发通信都是在本地进行,消耗在90~100ms之间,此部分消耗是异步的,非阻塞的,不影响当前线程,底层并行实现,且经过多次实验结果表明,与通信数据长度无明显关系,就是说稳定维持在这样的一个开销,单个的话,最小要等这么长时间的,多次通信回调的话,非线程增长,消耗反而会降低(因为worker一旦加载到本地内存中,其实就是本地线程间通信,其速度应该是非常快的,主要耗费在底层实现的调度上)

2. Worker初始化加载,Worker是通过new Worker(url)的方式加载的,一个页面一个worker进程,默认url走的是http请求,也就是说我们可以通过浏览器的Expires,过期头走浏览器缓存,或者可以想办法通过MainFest方式,走本地浏览器缓存

3. Worker自身的计算性能,这点通过PC和手机做了对比,发现一个很奇怪的现象,就是PC主线程的计算1w次循环的速度,一般要比worker中1w次循环的速度快5%~10%不等,这个地方估计要具体有机会看Webkit的实现源代码,才能知道原因,不过一般情况,普通的计算,Worker计算速度与主线程基本无差

多线程计算中的共享对象问题:

一般情况下,我们不会遇到这问题,但是一些特殊情况,比如需要在Worker进程中取得主线程的Window下一些变量等等

我们现在是通过一个全局对象$ENV传递给Worker和LocalThread,在模板里面,都可以访问到$ENV这个全局对象,只要在执行tplrender.tpl方法前,给这个$ENV对象赋值,既可以把值带到模板里面去计算

当然这只是简单的带对象进去,还有做过把对象从Worker中带出来,比如一些特殊需求,需要批量计算步长,瀑布流模式下的索引,埋点上报的ytag自增长等等,在Worker和LocalThread并行同步计算时候,Worker里面对于这个ytag的修改,对于LocalThread也需要有影响,那么就需要解决Worker与主线程的变量同步问题

我们目前采用的方式是单一任务步长变量同步(所谓单一任务,就是一次tplrender.tpl开始的记一次任务,一次任务里面根据需要渲染的元素多有少个,会拆分到不同小子任务,分布到Worker或者LocalThread中去,但是这些子任务一起算一个大的任务),其做法是

clip_image018

clip_image020

我们会在主线程,定义一个全局变量$ENV,在$ENV下挂一个step命名空间

在我们的业务代码里面,默认给每个页面起一个步长的变量ytag=1001

clip_image022

在模板代码里面使用自定义方法去递增步长

clip_image024

这时候,在当前页面的生命周期中,在worker进程和主进程,同一个tpl任务中,worker与主线程各自的ytag是各自自增长,当任务完成时候,会更新当前页面的$ENV.step.ytag到最新最大的值

clip_image026

完成ytag变量的自增长

那如何保证ytag自增长值不会重复呢?我们通过方法判断当前模板计算是在主线程还是在worker里面走不一样的增量即可实现

多线程计算中自定义方法共享:

我们做模板的意义,在于在MVVM框架中,将显示的逻辑从VM中最大限度的剥离出来,直接放于View中,最大限度的解放Modules(其只需要与后端通信,对于取回的数据不需要做过多的处理),因为在业务开发中,View是千变万化的一层,而后端一般不需要大动,所以我们将容易变化的逻辑,比如数据筛选,数据格式,循环显示等等逻辑统统放在了View层进行,我们也引入了Mustache+JTemplate双模板引擎解析的双工模式,去处理带有业务逻辑的模板代码,由于JTemplate的引入,使得我们的模板支持js语法,支持了js语法就意味着我们的一些业务逻辑,就可以抽象成模板方法去复用。

所以这就带来一个问题,我们是怎么复用我们的自定义模板方法的

首先我们实现了一个自定义模板方法对象,所有的符合我么业务要求的自定义模板内使用的js方法都放入这个对象中

clip_image028

第二,我们改造了JTemplate实现,在原有的基础上,修复了”带来的bug,且传入了tplFn这个对象供模板内js使用

第三,我们在Worker实现了最小版本的Require和define,用于适应我们现在r.js的打包工作,实现worker需要的方法自动打包合并

clip_image030

这样一来,在Worker和本地线程都有tplFn这个方法,也就都可以使用自定义方法了

为什么要整合支持Js语法的模板引擎:

其实前面已经说了,我们要将显示的业务逻辑,放到模板里去,简单的模板功能太少,不能发挥并行计算优势,另外对于显示逻辑前置化也是大事所趋,后端只需要关注大数据,对于显示逻辑安排,全部交给前端来做即可。

所以我们在采用Mustache语法系作为我们的基本模板引擎后,加入最小版本的js语法解析引擎jtemplate,在修复了众多bug之后,并入了我们的模板解析中。

为什么模板解析要用大括号开头的{%和{{:

因为{{是Mustache默认的符号,使用jtpl原来的<%,一个是好多HTML编辑器对于<开头会认为是HTML标记,导致显示不正确,因为我们目前和重构的合作模式是重构可以直接修改我们的HTML代码,使用{%,可以最大限度的还原原来重构的页面,及时用浏览器打开我们的模板,也可以直接看到页面

对于不支持多线程的如何处理:

首先我们代码里面有容错,和特诊判断,

clip_image032

其次,我们有是否使用Worker的开关,以及基本的Mustache引擎的tpl可以使用,不一定非是多线程渲染的方式

模板的预编译:

其实理论上这段和多线程没啥关系,既然说到了模板,那么就说说预编译吧,其实预编译没啥神奇的地方,无非就是把html模板转换成js相加的字符串功能,以提高eval这部分的性能,这个是一般模板引擎都会自带的功能,比如Mustache 或者jtpl的预编译功能,只是大多数同学现在研究在后端用nodejs跑这个预编译,出来的直接是js 而不是模板了。

所以说我们也支持这种预编译形式,由于worker的限制,不能传递引用类型的变量,但是在Worker内部空间里面,第一次执行后模板,被编译后,在页面的生命周期内是,是一直存在的,所以说第二次,第三次在渲染,使用的就是第一次编译后的模板,这个加速是存在的。

当然了脱离页面生命周期,这个预编译也就失效了,在js主线程中,由于http缓存策略的原因,这种预编译也还可以存在,但是真正对于真实需要大计算量的js计算,比如正则,条件判断,这种预编译起的效果值得考量,而多线程的工作模式正式为这种应用场景而生的。

后记:

后面我们会继续分享无线前端开发特有的创新,海量离线本地存储以及版本控制策略,敬请期待!模板技术只有结合本地存储,把一个在线WebApp,变成一个真正的离线WebApp,才是真正的价值所在。

posted on 2013-08-01 19:43  薛端阳  阅读(4805)  评论(8编辑  收藏  举报