HTML5觸屏版多線程渲染模板技術分享

 

前言:

瞭解js編譯原理的屌絲們都知道,js是單線程的,想當年各路神仙爲了實現js的多線程,爲了解決innerHTML輸出大段HTML卡頁面的頑疾,紛紛設計了諸如假冒的「多線程「實現,我本身也在寫開源框架KitJs時候,寫過相似的組件http://www.cnblogs.com/xueduanyang/archive/2012/05/30/2526422.html ,其原理就是改造代碼中的for爲setInterval,改遞歸爲尾遞歸等等,爲可憐的刷新率60Hz爭取17ms的微弱時間。html

固然了,這些都不是真正的多線程。其實W3C很早就有關於純前端真多線程實現的,就是http://www.w3.org/TR/workers/ ,一直以來打着HTML5的旗號,各大瀏覽器廠商都都有對應標準的worker實現,具體支持程度咱們能夠看http://caniuse.com/#feat=webworkers ,值得一提的是我們的UC也支持WebWorker,這幾年隨着移動HTML5項目的興起,手機多核的炒做,如今就連華強北這種屌絲手機都開始以雙核爲賣點了,而真正從APP或者Web爲多線程作優化的項目又有多少開發者呢?前端

做爲一個猥瑣的先驅者,本着讓用戶爽,不爽不舒服斯基的原則,咱們率先在觸屏項目實現了多個多線程工做模型,本文介紹的多線程渲染模板就是其中的一個。node

以前在春田花花羣裏面,曾今說過咱們有個多線程渲染系統,遭到衆多同窗的反擊,今天終於能給我一個機會,爲你們展示咱們的辛苦付出,歡迎你們看完以後熱烈討論。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,纔是真正的價值所在。

相關文章
相關標籤/搜索