本文主要介紹Angular中的黑科技之WebWorker Renderer,使用Worker線程渲染如何渲染頁面?從源碼的角度切入,帶領帶你們看個究竟。html
開發框架版本:Angular 4.x前端
項目地址:Charway/angular-webworker-renderer-demoreact
對比對象:傳統的UI線程渲染和使用WebWorker線程渲染頁面git
對比方法:各執行1到1000的連乘,並循環20次,要求實時展現進度github
運行結果:web
首先是傳統的UI線程渲染效果:算法
其次時使用WebWorker線程渲染效果:
typescript
從動圖中很明顯能夠看出,使用了WebWorker Renderer渲染的頁面運行流暢,沒有卡頓。
windows
Workers是一種機制,經過它可使一個腳本操做在與Web應用程序的主執行線程分離的後臺線程中運行。這樣作的優勢是能夠在單獨的線程中執行繁瑣的處理,讓主(一般是UI)線程運行而不被阻塞/減慢。 —— Web Workers API from MDN
簡單來講,在出現WebWoker以前,Web開發人員沒法手動在瀏覽器中建立線程,而出現WebWoker以後,Web開發人員能夠進入多線程開發Web項目了。數組
下面根據AngularConf的YouTube視頻(見參考)中的內容總結了下使用WebWorker的優點:
對於最後一點的解釋,應該先轉化爲另一個問題,一些計算密集型的程序爲何不在服務端執行完畢後返回給前端?這在視頻中也給出瞭解釋,做者總結了一句話:It costs more to transmit a byte than to compute it,意思是傳輸一個byte比計算出一個byte的消耗更大。爲何呢?本身想吧
那麼真的有這麼多應用場景嗎?如下列舉了幾個場景:
仔細想想,這樣的場景仍是很特殊的,可能在實際的應用中並很少見。那麼,在目前的主流前端框架是否有利用到WebWorker的特性來幫助其提高性能呢?通過調研,調研的部分都還在探索階段,好比在React框架中的探索,Parashuram在2016年發佈了文章《Using Webworkers to make React faster》,文章是關於如何利用Webworker提高React的渲染速度,主要是把Virtual DOM的相關計算過程(如diff算法)放入WebWorker線程,從結果能夠看出,在Benchmark的對比下,使用WebWorker的一方幀率有所提升,感興趣的同窗能夠查看其演示示例和項目地址。這裏再忍不住要引用做者的一張圖(以下圖所示,縱軸是幀率,橫軸是節點的個數),簡要展現下React項目在使用WebWorker的狀況下,性能的提高效果。
(圖片來源:Using Webworkers to make React faster)
那麼WebWorker已經面世這麼久了,各大瀏覽器支持也跟上了,爲什麼其應用場景或者與主流框架的結合並無不少見?我想可能與如下幾點WebWorker的缺點相關:
雖然如此,Angular背後的Google團隊已經開始嘗試打破這些限制,並已經在Angular 2.x中得進行了嘗試(WebWorker Renderer),雖然到了目前的Angular 4.x在源碼中仍標識爲@experimental,但相信其在未來會成爲Angular框架的標配。接下來的文章內容,會分析到在Angular框架中Webworker Renderer是如何工做的,包括以下三個要點:
但願你能帶着這三個問題閱讀完如下的篇幅。
圖中顯示的基本是整個UI線程與WebWorker線程通信的過程,給你來個初步的影響,能夠幫助你在閱讀後續內容時有個總體觀的把控,圖中涉及的類、方法以及過程,在接下來的文章中會一一介紹到。
先來看看這個RenderStroe類,在Angular是被標識爲@Injectable()的可注入類,其中nextIndex是一個自增的索引號,經過allocateId函數遞增分配。store和remove函數是對lookupById和_lookupByObject兩個Map類型的容器進行新增和刪除操做,其中的傳入的id參數做爲惟一的索引號(經過allocateId函數分配而來)。最後deserialize和serialize方法分別是根據id取出內容和根據內容取出id。這意味中在RenderStore中序列化就是將對象轉換成一個惟一數字,而相對應的反序列化就是將數字轉換爲一個對象。
這樣一來一個被廣泛使用的RenderStore類就介紹完畢了,它承擔了線程間數據信息通信消息存儲和序列化/反序列化的重要工做。總的來講,就是將須要傳輸的內容對象與一個索引號對應起來,實現序列化和反序列化的過程。這個類會穿梭於整個工做流程,常常會注入到其餘關鍵類中,是UI線程與WebWorker線程公用的類,兩端共同維護相同的一個副本,間接到達線程間數據共享的目的。
經過這個RenderStore類,咱們已經能夠解決以前提出第一個問題,放張動圖你們先消化消化。聰明的你可能會有如下幾個疑問:
不慌,咱們接下去講。
這個Serializer類主要用於WebWoker線程與UI主線程之間通信的時候,提供消息信息序列化和反序列化的操做,其實仍是主要依賴於RenderStore提供的方法。
該類定義了序列化的類型,對於string,number,boolean類型,即PRIMITIVE類型,是不須要序列化/反序列化的。經過代碼枚舉得知,操做支持以下幾種類型:
enum SerializerTypes {
// RendererType2
RENDERER_TYPE_2,
// Primitive types,such as string,number,boolean
PRIMITIVE,
// An object stored in a RenderStore
RENDER_STORE_OBJECT,
}
複製代碼
對各個具體的類型是如何序列化的,作了以下說明:
其中,RenderComponentType, RendererType2類型是@angular/core中定義的,二者都是Angular編譯器中對DOM節點進行渲染處理時定義的類型,這裏很少作闡述。LocationType類型是針對瀏覽器的路由操做(windows.locaion.*)進行的包裝,包含href,protocol,host,hostname,port等,容易理解。
此外,serializeRendererType2和serializeRenderComponentType方法體中也是根據序列化對象的結構再進行拆分對待,並繼續調用serialize方法處理。好比_serializeRendererType2方法中是這樣的:
private _serializeRendererType2(type: RendererType2): {[key: string]: any} {
return {
'id': type.id,
'encapsulation': this.serialize(type.encapsulation),
'styles': this.serialize(type.styles),
'data': this.serialize(type.data),
};
}
複製代碼
從代碼中能夠看出,通信信息的序列化/反序列化過程其實就是主要針對string,number,boolean類型(PRIMITIVE類型)和RENDER_STORE_OBJECT類型在做處理,前者不須要序列化/反序列化,後者經過RenderStore提供的方法進行處理。
是時候回答下以前提出的問題:RenderStore中存的Object對象究竟是哪些?RENDER_STORE_OBJECT類型是指哪些類型呢?
因而,這裏就不得不提到NamedEventEmitter類,這個類維護了一個Map類型的容器_listener,存儲了事件名稱和對應的方法,並提供新增(listen)、刪除(unliten)以及觸發事件的方法(dispatchEvent)。
因而可知,事件的定義、維護和觸發在整個線程間通信中相當重要。
根據官方遠源碼介紹,MessageBus類是一個低級別的API,是一個抽象類,主要用於UI主線程與WebWorker線程的通訊相關。而雙方的通訊是基於通道(channel),通道的兩端分別是MessageBusSink(信息流出)和MessageBusSource(信息流入),後續會細說到。類中說起的Zone是Angular的魔法,因爲對與本文內容的理解不受影響,所以不作過多闡述,如感興趣請自行查看。
首先是來列舉下Angular中定義的三種通道的類型,三種通道負責不一樣的工做,分爲渲染、事件和路由。
// DOM渲染通道
export declare const RENDERER_2_CHANNEL = "v2.ng-Renderer";
// DOM事件通道
export declare const EVENT_2_CHANNEL = "v2.ng-Events";
// 路由通道
export declare const ROUTER_CHANNEL = "ng-Router";
複製代碼
接下來具體講下PostMessageBus類,做爲MessageBus抽象類的一個實現,類結構以下圖所示。
該類的兩個公共成員變量分別是source(PostMessageBusSource類型,是MessageBusSource類的一實現類)和sink(PostMessageBusSink類型,是MessageBusSink的實現類),能夠解釋爲水源和水槽。能夠這麼理解,信息比如是水,能夠經過水槽流出,也能夠流入到水源中。
類中的initChannel方法對這通道進行初始化,其中有2個的關鍵點:1)每一個通道的實例最多隻能有三個不一樣的通道類型;2)Channel通道信息初始化時候包含了一個EventEmitter類的實例對象,在Sink通道初始化的時候還會對其進行了訂閱操做,觸發後會執行相應的sendMessage操做,這個發送信息的方法的實現主要是經過該類的構造函數中傳入,後面會有所介紹。
另外須要介紹一下PostMessageBusSource類,該類在構造函數中會對Worker對象經過addEventListener方法監聽message事件,這個過程能監聽信息接收的事件,而且作相應的信息處理的操做,即經過EventEmitter類的emit方法來觸發相應的訂閱事件。
首先介紹一下WebWorkerRendererFactory2類,從類名中能夠解釋爲WebWorker渲染工廠,在Angular中也被標爲@Injectable()類型,其構造函數中依賴ClientMessageBrokerFactory, MessageBus,Serializer, RenderStore類的注入,並對其初始化,以下:
this._messageBroker = messageBrokerFactory.createMessageBroker(RENDERER_2_CHANNEL);
bus.initChannel(EVENT_2_CHANNEL);
const source = bus.from(EVENT_2_CHANNEL);
source.subscribe({next: (message: any) => this._dispatchEvent(message)});
複製代碼
從構造函數中能瞭解到,主要依賴注入類的做用。首先經過ClientMessageBrokerFactory建立了通道爲RENDERER_2_CHANNEL的代理人,雖然還未具體解釋ClientMessageBroker類的做用,但從類命名中就能夠了解到它的做用就是做爲與UI線程通信的中間代理人,在該類中負責向UI線程傳輸DOM節點渲染的工做,這個會在後續會詳細介紹。另外,經過自身的MessageBus建立了EVENT_2_CHANNEL通道,而且對信息源作了subscribe的訂閱操做,即當UI線程DOM事件觸發時,該MessageBus的Source會接收到信息,並觸發相應的_dispatchEvent函數操做,在WebWorker層中作相應的處理。
預知更多詳情,請看下回終解。