很開心再次遇見你,接著上回分解。
先把與通訊相關的類介紹完畢。
與WebWorkerRendererFactory2類對應的就是WebWorkerRenderer2類,該類從類結構中就可以看出包含了各種對DOM節點的操作函式,基本覆蓋原生JS的DOM操作函式。特別注意,該類裡面的操作函式並不是真正地操作DOM節點,而是在WebWorker執行緒中的模擬,最後還是以訊息的形式傳送給UI主執行緒中呼叫Renderer2進行操作,後續會講到。舉例說一下其中的createElement函式:
createElement(name: string, namespace?: string): any {
const node = this._rendererFactory.allocateNode();
this.callUIWithRenderer('createElement', [
new FnArg(name),
new FnArg(namespace),
new FnArg(node, SerializerTypes.RENDER_STORE_OBJECT),
]);
return node;
}
複製程式碼
其函式體中的_rendererFactory成員變數是一個WebWorkerRendererFactory2類的例項(在建構函式中傳入),呼叫了allocateNode方法,建立一個包含事件處理的類,再通過RenderStore生成唯一Id,並儲存。然後呼叫callUIWithRenderer函式,如下:
private callUIWithRenderer(fnName: string, fnArgs: FnArg[] = []) {
// always pass the renderer as the first arg
this._rendererFactory.callUI(fnName, [this.asFnArg, ...fnArgs]);
}
複製程式碼
其函式體中的_rendererFactory成員變數是一個WebWorkerRendererFactory2類,呼叫了callUI方法處理DOM相關的操作函式,包含fnName(方法名)和fnArgns(引數),其中asFnArg是一個預設引數(FnArg類的一個例項),並指定了序列化的型別,因此會被存入RenderStore,定義如下:
private asFnArg = new FnArg(this, SerializerTypes.RENDER_STORE_OBJECT);
複製程式碼
那麼callUI方法是怎麼處理的呢?我們來看一下:
callUI(fnName: string, fnArgs: FnArg[]) {
const args = new UiArguments(fnName, fnArgs);
this._messageBroker.runOnService(args, null);
}
複製程式碼
從函式體中,可以看出呼叫了ClientMessageBroker的runOnService方法,通過該方法向UI執行緒傳送渲染指令(通過Sink的emit方法)並處理反饋資訊,這裡先做個簡單的介紹,後續會詳細介紹。
小小地總結下,WebWorkerRenderer2類定義了在WebWorker執行緒中模擬操作DOM節點的方法,並且發出指令向UI執行緒傳送資訊。
與WebWorkerRenderer2類對應的是MessageBasedRenderer2類,前者在WebWorker執行緒中工作,後者在UI執行緒中工作。同樣的,是MessageBasedRenderer2類中也定義了豐富的DOM操作方法(與WebWorkerRenderer2類對應),這些方法才是真正意義上操作DOM的方法,通過呼叫Renderer2中的相關方法。同時,DOM的事件觸發後會通過MessageBus的Sink傳送給WebWorker執行緒中的WebWorkerRendererFactory2類做處理。
WebWorkerRenderer2類既然有個ClientMessageBroker類來作中間代理人,負責傳遞資訊,那麼,MessageBasedRenderer2類也需要一個代理人來接頭,這就是ServiceMessageBrokerFactory類。它負責註冊WebWorkerRenderer2類中DOM操作函式,並接收從ClientMessageBroker傳過來的渲染指令,然後觸發對應的方法,執行結束後,在反饋給ClientMessageBroker代理人(成功還是失敗)。
枯燥無聊的基本概念都介紹到了,接下來該幹正事了。
統觀全域性
我們看圖說話。
圖中只介紹了兩個通道的(RENDERER_2_CHANNEL通道和EVENT_2_CHANNEL通道)的通訊流程,還差一個ROUTER_CHANNEL通道沒有提及,其通過過程和RENDERER_2_CHANNEL通道類似,這裡就不作進一步介紹了,有興趣的請閱讀原始碼。
先來介紹一下初始化的部分(圖中的藍色部分),從左到右來介紹。首先左側MessageBasedRenderer2類初始化的時候建立ServiceMessageBroker型別作為自己的通訊代理人,同時初始化兩條通道,建立PostMessageBusSink和Source為通訊做準備(其中事件通道中只用到了Sink),並對Source訂閱事件。右側的WebWorker執行緒中的初始化操作類似,用到的類不同而已。
在啟動時(圖中的黃色部分),MessageBasedRenderer2會在start函式中呼叫registerMethod方法去向ServiceMessageBroker註冊DOM操作相關的方法,儲存在_methods中,等待觸發。
接下來,就是正式執行渲染環節了,避免錯亂,更新下圖,如下:
圖中顯示了3種顏色,代表了3個執行緒間通訊的過程,從綠色的開始說。
在WebWorker執行緒中,Angular引擎首先會根據頁面佈局拆分為細化的DOM節點操作,並執行渲染操作。通過callUI呼叫Broker中的runOnService方法,並儲存在_pending容器(存的是什麼東西?)中,同時使用Sink向UI執行緒的Source傳送訊息,包含id、方法名和引數。UI執行緒的代理人接收到資訊後,觸發相應的訂閱事件_handleMessage方法,該方法就去_mehtods中找MessageBassedRenderer2在啟動時候註冊的對應方法並執行,具體過程就是呼叫Renderer2中相應的DOM操作方法。
等DOM操作結束,就進入了藍色標示的過程,其實是個反饋的過程。通過Sink向WebWorker執行緒傳送訊息,訊息內容如下:
{
'type': 'result',
'value': this._serializer.serialize(result, type),
'id': id,
}
複製程式碼
其中type型別是‘result’(其實還可能是'error',本文未指出),WebWorker執行緒收到訊息後,會觸發在初始化時候定義的訂閱事件,執行_handleMessage方法,操作_pengding容器,根據id獲取對應條目執行(執行的是什麼?)並且刪除,這樣這個藍色過程就結束了。
那麼_pending容器裡面存的是什麼?執行的又是什麼?從字面理解是應該是用於儲存'待解決'的事務,這也回答了,怎麼處理執行緒間併發這個問題?在此解答一下,先上相關程式碼:
interface PromiseCompleter {
resolve: (result: any) => void;
reject: (err: any) => void;
}
// ...
let completer: PromiseCompleter = undefined !;
let promise = new Promise((resolve, reject) => { completer = {resolve, reject}; });
let id = this._generateMessageId(args.method);
// 儲存
this._pending.set(id, completer);
// catch和then
promise.catch((err) => {...});
promise = promise.then((v: any) => this._serializer ? this._serializer.deserialize(v, returnType) : v);
// ... 反饋時
if (message.type === 'result') {
this._pending.get(id) !.resolve(message.value);
} else {
this._pending.get(id) !.reject(message.value);
}
this._pending.delete(id);
// ...
複製程式碼
_pending容器裡面存了id和與之對應的Promise物件的 {resolve, reject},並且預先定義好then方法(這裡是做了反序列化操作,並沒有其餘操作),當WebWorker執行緒接收到type為'result' or 'error'的訊息時,並對應執行resolve或者reject方法,以此釋放Promise。相信你已經明白了,執行緒間併發問題就是用過Promise方法來完成同步。
最後來到紫色的過程,UI執行緒中DOM節點繫結的事件觸發後,通過Sink通過事件通道向WebWorker執行緒的Source傳送訊息,WebWorker執行緒收到訊息後,觸發相應的訂閱方法,這裡不像渲染通道一樣,有反饋過程。可能的原因時,與事件相關的方法(大部分是在對DOM節點操作),還是通過渲染通道(通過綠色和藍色的過程)通知給UI執行緒。
這樣整個WebWoker Renderer的執行緒間通訊的部分就介紹完畢了。
回顧下一開始提出的三個問題:
- 通訊資訊如何序列化與反序列化?記憶體資料如何共享? 答:通過RenderStore類。
- 如何打破Webworker執行緒不能操作DOM節點的侷限? 答:通過RENDERER_2_CHANNEL通道。
- 如何處理併發?答:通過操作反饋與Promise機制。
還沒完
我猜你一定想知道Angular中是怎麼啟動WebWorker執行緒並執行渲染操作?如何開啟UI執行緒中的start方法?來來來,慢慢絮叨。
我們從如何將傳統的Angular專案(基於platformBrowserDynamic的JIT專案或者基於platformBrowser的AOT專案)轉換成基於platformWorkerAppDynamic的WebWorker專案,可以參考本文一開始提供的DEMO專案或者參考文章《Angular with Web Workers: Step by step》。基於現有的WebWorker專案,我們來講解一下,啟動過程。
首先會呼叫@angular/platform-webworker中的bootstrapWorkerUi方法啟動一個WebWorkerLoader,傳入的是一個WebPack打包輸出的webworker.bundle.js(可在webpack.config.js輸出),基於的檔案就是一個platformWorkerAppDynamic的啟動檔案,其餘的配置與傳統項無異,由此可見改動成本還是比較小的。
主要還是來看一下bootstrapWorkerUi方法做了些什麼?
export function bootstrapWorkerUi(
workerScriptUri: string, customProviders: Provider[] = []): Promise<PlatformRef> {
// For now, just creates the worker ui platform...
const platform = platformWorkerUi([
{provide: WORKER_SCRIPT, useValue: workerScriptUri},
...customProviders,
]);
return Promise.resolve(platform);
}
複製程式碼
從程式碼中看出主要是建立一個platformWorkerUi物件,講loader檔案地址傳入。
這麼關鍵的一個platformWorkerUi我們簡單來將想,該物件通過createPlatformFactory(Angular/core中的方法)建立,並傳入一組Provider,包括MessageBasedRenderer2,Serializer,RenderStore,MessageBus等之前介紹過的注入類,
還有一個關鍵的WebWorkerInstance,申明如下:
@Injectable()
export class WebWorkerInstance {
public worker: Worker;
public bus: MessageBus;
/** @internal */
public init(worker: Worker, bus: MessageBus) {
this.worker = worker;
this.bus = bus;
}
}
複製程式碼
作為WebWorker的一個例項,包含Woker物件的例項,和MessageBus的例項,那麼什麼時候呼叫的init方法初始化呢?接著往下看,
Provider中還有一個關鍵init時需要的注入類,描述如下:
{
provide: PLATFORM_INITIALIZER,
useFactory: initWebWorkerRenderPlatform,
multi: true,
deps: [Injector]
}
複製程式碼
裡面使用了initWebWorkerRenderPlatform方法,提取梳理出關鍵的步驟:
const webWorker: Worker = new Worker(url);
const sink = new PostMessageBusSink(webWorker);
const source = new PostMessageBusSource(webWorker);
const bus = new PostMessageBus(sink, source);
WebWorkerInstance.init(webWorker, bus);
// initialize message services after the bus has been created
const services = injector.get(WORKER_UI_STARTABLE_MESSAGING_SERVICE);
// 這裡的 WORKER_UI_STARTABLE_MESSAGING_SERVICE 在應用中歸根到底其實就是呼叫的MessageBasedRenderer2類
zone.runGuarded(() => { services.forEach((svc: any) => { svc.start(); }); });
複製程式碼
根據url路徑建立Worder物件,該對應用於PostMessageBusSink和PostMessageBusSource物件初始化,比如在PostMessageBusSource初始化中會對Worker物件addEventListener監聽'message'事件。然後使用sink和source例項化PostMessageBus類,再呼叫WebWorkerInstance物件的init方法。最後,將注入的MessageBasedRenderer2類自動呼叫start方法。
總結
說一下自己的感受,不要為了用WebWorker執行緒而用,還是要集合多方面因素來考慮,比如執行緒間通訊時間,開啟執行緒的效能消耗等,畢竟WebWorker提出的初衷是為了那些計算密集型的操作,被Angular框架使用到渲染中,是一個有突破的創新,但目前並不能支援所有專案的轉換,還不穩定(@experimental),請謹慎使用。但相信隨著瀏覽器的發展,為了極致化使用者體驗,WebWorker的渲染勢必會被各大主流前端框架考慮在內。
至此,Angular WebWorker Renderer的前前後後的原始碼解析就解密完了,肯定有諸多解析不到位的地方,歡迎留言吐槽。
最後的最後,歡迎加入我們的隊伍charway@qq.com。
參考
Angular Platform-Webworker 原始碼
Angular Web Worker - Building Super Responsive UI
Using Web Workers for more responsive apps
Angular with Web Workers: Step by step