解密Angular WebWorker Renderer (一)

本文主要介紹Angular中的黑科技之WebWorker Renderer,使用Worker線程渲染如何渲染頁面?從源碼的角度切入,帶領帶大家看個究竟。

先來做個對比

開發框架版本:Angular 4.x

項目地址:Charway/angular-webworker-renderer-demo

對比對象:傳統的UI線程渲染和使用WebWorker線程渲染頁面

對比方法:各執行1到1000的連乘,並循環20次,要求實時展示進度

運行結果

首先是傳統的UI線程渲染效果:

其次時使用WebWorker線程渲染效果:

從動圖中很明顯可以看出,使用了WebWorker Renderer渲染的頁面運行流暢,沒有卡頓。

先簡單介紹下Web WokerWeb

Workers是一種機制,通過它可以使一個腳本操作在與Web應用程序的主執行線程分離的後台線程中運行。這樣做的優點是可以在單獨的線程中執行繁瑣的處理,讓主(通常是UI)線程運行而不被阻塞/減慢。 —— Web Workers API from MDN

簡單來說,在出現WebWoker之前,Web開發人員無法手動在瀏覽器中創建線程,而出現WebWoker之後,Web開發人員可以進入多線程開發Web項目了。

Web Worker的優勢

下面根據AngularConf的YouTube視頻(見參考)中的內容總結了下使用WebWorker的優勢:

  • 運行過程中不會阻礙主線程(UI渲染線程)的運行,特別適合執行計算密集型的程序
  • WebWorker線程可跨窗口或frames(使用SharedWorker)
  • 使用WebWorker後能更優雅地執行測試過程(一些脫離可DOM操作的測試)
  • 兼容性(IE 10+)
  • 更高效地利用電量

對於最後一點的解釋,應該先轉化為另外一個問題,一些計算密集型的程序為什麼不在服務端執行完畢後返回給前端?這在視頻中也給出了解釋,作者總結了一句話:It costs more to transmit a byte than to compute it,意思是傳輸一個byte比計算出一個byte的消耗更大。為什麼呢?自己想吧

Web Worker可能的使用場景

那麼真的有這麼多應用場景嗎?以下列舉了幾個場景:

  • 解析一個龐大的JSON結構
  • 圖片/音頻處理
  • 大規模數據可視化

仔細想一想,這樣的場景還是很特殊的,可能在實際的應用中並不多見。那麼,在目前的主流前端框架是否有利用到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的缺點相關:

  • 在Webworker線程中無法訪問DOM節點
  • 無法與UI線程共享內存
  • 與UI線程通訊的信息需要序列化
  • 線程間通訊不可避免的並發問題

雖然如此,Angular背後的Google團隊已經開始嘗試打破這些限制,並已經在Angular 2.x中得進行了嘗試(WebWorker Renderer),雖然到了目前的Angular 4.x在源碼中仍標識為@experimental,但相信其在將來會成為Angular框架的標配。接下來的文章內容,會分析到在Angular框架中Webworker Renderer是如何工作的,包括如下三個要點:

  • 通訊信息如何序列化與反序列化?內存數據如何共享?
  • 如何打破Webworker線程不能操作DOM節點的局限?
  • 如何處理並發?

希望你能帶著這三個問題閱讀完以下的篇幅。

先感受一下

圖中顯示的基本是整個UI線程與WebWorker線程通訊的過程,給你來個初步的影響,可以幫助你在閱讀後續內容時有個整體觀的把控,圖中涉及的類、方法以及過程,在接下來的文章中會一一介紹到。

介紹幾個基本的類

先來看看這個RenderStroe類,在Angular是被標識為@Injectable()的可注入類,其中nextIndex是一個自增的索引號,通過allocateId函數遞增分配。store和remove函數是對lookupById和_lookupByObject兩個Map類型的容器進行新增和刪除操作,其中的傳入的id參數作為唯一的索引號(通過allocateId函數分配而來)。最後deserialize和serialize方法分別是根據id取出內容和根據內容取出id。這意味中在RenderStore中序列化就是將對象轉換成一個唯一數字,而相對應的反序列化就是將數字轉換為一個對象。

這樣一來一個被普遍使用的RenderStore類就介紹完畢了,它承擔了線程間數據信息通訊消息存儲和序列化/反序列化的重要工作。總的來說,就是將需要傳輸的內容對象與一個索引號對應起來,實現序列化和反序列化的過程。這個類會穿梭於整個工作流程,經常會注入到其他關鍵類中,是UI線程與WebWorker線程公用的類,兩端共同維護相同的一個副本,間接到達線程間數據共享的目的。

通過這個RenderStore類,我們已經可以解決之前提出第一個問題,放張動圖大家先消化消化。聰明的你可能會有以下幾個疑問:

  • Object對象里存的到底是什麼東西?
  • 難倒只能由WebWorker線程向UI線程單向地發送同步RenderStore數據的指令?

不慌,我們接下去講。

這個Serializer類主要用於WebWoker線程與UI主線程之間通訊的時候,提供消息信息序列化和反序列化的操作,其實還是主要依賴於RenderStore提供的方法。

該類定義了序列化的類型,對於string,number,boolean類型,即PRIMITIVE類型,是不需要序列化/反序列化的。通過代碼枚舉得知,操作支持如下幾種類型:

enum SerializerTypes {n // RendererType2n RENDERER_TYPE_2,n // Primitive types,such as string,number,booleann PRIMITIVE,n // An object stored in a RenderStoren RENDER_STORE_OBJECT,n}n

對各個具體的類型是如何序列化的,做了如下說明:

  • PRIMITIVE類型(原始類型),serializer方法不做任何處理,直接返回;
  • Array類型,使用map方法對數組中的每一項再serializer,然後返回;
  • RENDER_STORE_OBJECT類型,通過RenderStore類中的serialize方法序列化後返回;
  • RENDERER_TYPE_2類型,通過調用_serializeRendererType2方法處理後返回;
  • RenderComponentType類型,通過調用_serializeRenderComponentType方法處理後返回;
  • LocationType類型,通過調用_serializeLocation方法處理後返回;

其中,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} {n return {n id: type.id,n encapsulation: this.serialize(type.encapsulation),n styles: this.serialize(type.styles),n data: this.serialize(type.data),n };n}n

從代碼中可以看出,通訊信息的序列化/反序列化過程其實就是主要針對string,number,boolean類型(PRIMITIVE類型)和RENDER_STORE_OBJECT類型在作處理,前者不需要序列化/反序列化,後者通過RenderStore提供的方法進行處理。

是時候回答下之前提出的問題:RenderStore中存的Object對象到底是哪些?RENDER_STORE_OBJECT類型是指哪些類型呢?

  • WebWorkerRenderer2類型,繼承自Renderer2類(該類是Angular的核心類,用於操作DOM相關,這裡就不啰嗦了)
  • WebWorkerRenderNode類型,該類有且只有一個類型為NamedEventEmitter的成員變數events

於是,這裡就不得不提到NamedEventEmitter類,這個類維護了一個Map類型的容器_listener,存儲了事件名稱和對應的方法,並提供新增(listen)、刪除(unliten)以及觸發事件的方法(dispatchEvent)。

由此可見,事件的定義、維護和觸發在整個線程間通訊中至關重要。

再說說與通訊相關的類

根據官方遠源碼介紹,MessageBus類是一個低級別的API,是一個抽象類,主要用於UI主線程與WebWorker線程的通信相關。而雙方的通信是基於通道(channel),通道的兩端分別是MessageBusSink(信息流出)和MessageBusSource(信息流入),後續會細說到。類中提及的Zone是Angular的魔法,由於對與本文內容的理解不受影響,因此不做過多闡述,如感興趣請自行查看。

首先是來列舉下Angular中定義的三種通道的類型,三種通道負責不同的工作,分為渲染、事件和路由。

// DOM渲染通道nexport declare const RENDERER_2_CHANNEL = "v2.ng-Renderer";n// DOM事件通道nexport declare const EVENT_2_CHANNEL = "v2.ng-Events";n// 路由通道nexport declare const ROUTER_CHANNEL = "ng-Router";n

接下來具體講下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, MessageBusSerializerRenderStore類的注入,並對其初始化,如下:

this._messageBroker = messageBrokerFactory.createMessageBroker(RENDERER_2_CHANNEL);nbus.initChannel(EVENT_2_CHANNEL);nconst source = bus.from(EVENT_2_CHANNEL);nsource.subscribe({next: (message: any) => this._dispatchEvent(message)});n

從構造函數中能了解到,主要依賴注入類的作用。首先通過ClientMessageBrokerFactory創建了通道為RENDERER_2_CHANNEL的代理人,雖然還未具體解釋ClientMessageBroker類的作用,但從類命名中就可以了解到它的作用就是作為與UI線程通訊的中間代理人,在該類中負責向UI線程傳輸DOM節點渲染的工作,這個會在後續會詳細介紹。另外,通過自身的MessageBus創建了EVENT_2_CHANNEL通道,並且對信息源做了subscribe的訂閱操作,即當UI線程DOM事件觸發時,該MessageBus的Source會接收到信息,並觸發相應的_dispatchEvent函數操作,在WebWorker層中做相應的處理。

預知更多詳情,請看下回終解。


推薦閱讀:

2D圓形隨機分布
興趣部落的前端性能優化實踐概覽
精讀《2017前端性能優化備忘錄》
如何不擇手段提升scroll事件的性能
「每日一題」你是如何做性能優化的?

TAG:Angular? | 前端框架 | 前端性能优化 |