解密 Angular WebWorker Renderer (一)

美團點評點餐發表於2017-08-08
解密Angular WebWorker Renderer (一)

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

先來做個對比

開發框架版本:Angular 4.x

專案地址Charway/angular-webworker-renderer-demo

對比物件:傳統的UI執行緒渲染和使用WebWorker執行緒渲染頁面

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

執行結果

首先是傳統的UI執行緒渲染效果:

解密 Angular WebWorker Renderer (一)

其次時使用WebWorker執行緒渲染效果:

解密 Angular WebWorker Renderer (一)

從動圖中很明顯可以看出,使用了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的情況下,效能的提升效果。

解密 Angular WebWorker Renderer (一)

(圖片來源: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節點的侷限?
  • 如何處理併發?

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

先感受一下

解密 Angular WebWorker Renderer (一)

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

介紹幾個基本的類

解密 Angular WebWorker Renderer (一)

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

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

解密 Angular WebWorker Renderer (一)

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

  • Object物件裡存的到底是什麼東西?
  • 難倒只能由WebWorker執行緒向UI執行緒單向地傳送同步RenderStore資料的指令?

不慌,我們接下去講。

解密 Angular WebWorker Renderer (一)

這個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,
}
複製程式碼

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

  • 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} {
    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型別是指哪些型別呢?

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

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

解密 Angular WebWorker Renderer (一)

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

再說說與通訊相關的類

解密 Angular WebWorker Renderer (一)

根據官方遠原始碼介紹,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抽象類的一個實現,類結構如下圖所示。

解密 Angular WebWorker Renderer (一)

該類的兩個公共成員變數分別是source(PostMessageBusSource型別,是MessageBusSource類的一實現類)和sink(PostMessageBusSink型別,是MessageBusSink的實現類),可以解釋為水源和水槽。可以這麼理解,資訊好比是水,可以通過水槽流出,也可以流入到水源中

類中的initChannel方法對這通道進行初始化,其中有2個的關鍵點:1)每個通道的例項最多隻能有三個不同的通道型別;2)Channel通道資訊初始化時候包含了一個EventEmitter類的例項物件,在Sink通道初始化的時候還會對其進行了訂閱操作,觸發後會執行相應的sendMessage操作,這個傳送資訊的方法的實現主要是通過該類的建構函式中傳入,後面會有所介紹。

另外需要介紹一下PostMessageBusSource類,該類在建構函式中會對Worker物件通過addEventListener方法監聽message事件,這個過程能監聽資訊接收的事件,並且做相應的資訊處理的操作,即通過EventEmitter類的emit方法來觸發相應的訂閱事件。

解密 Angular WebWorker Renderer (一)

首先介紹一下WebWorkerRendererFactory2類,從類名中可以解釋為WebWorker渲染工廠,在Angular中也被標為@Injectable()型別,其建構函式中依賴ClientMessageBrokerFactory, MessageBusSerializerRenderStore類的注入,並對其初始化,如下:

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層中做相應的處理。

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



相關文章