很开心再次遇见你,接着上回分解。

先把与通讯相关的类介绍完毕。

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 argthis._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,包括MessageBasedRenderer2SerializerRenderStoreMessageBus等之前介绍过的注入类,

还有一个关键的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对象,该对应用于PostMessageBusSinkPostMessageBusSource对象初始化,比如在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

解密Angular WebWorker Renderer (二)相关推荐

  1. 解密SVM系列(二):SVM的理论基础(转载)

    解密SVM系列(二):SVM的理论基础     原文博主讲解地太好了  收藏下 解密SVM系列(三):SMO算法原理与实战求解 支持向量机通俗导论(理解SVM的三层境界) 上节我们探讨了关于拉格朗日乘 ...

  2. Angular 学习(二):Angular 简介

    文章目录 一. Angular 介绍 AngularJS 特性 存在的问题 Angular 新特性 二. AngularJS 架构与 Angular 架构 AngularJS 架构 Angular 架 ...

  3. 看雪学院-解密入门教学(二)笔记

    解密入门教学(二)- 看雪学院 原作地址:http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458281884&idx=2&a ...

  4. Spring Security和Angular教程(二)登录页面

    Spring Security和Angular教程(二) 登录页面 在本节中,我们将继续讨论如何在"单页面应用程序"中使用带有Angular的Spring Security.在这里 ...

  5. angular学习笔记(二十五)-$http(3)-转换请求和响应格式

    本篇主要讲解$http(config)的config中的tranformRequest项和transformResponse项 1. transformRequest: $http({transfor ...

  6. angular路由模块(二)

    上一章写的是如何创建一个简单的路由,这一样我们来看看如何创建一个路由模块.angular的思想就是(模块,组件,子组件.....). 我们在src/app目录下创建一个跟路由模块app-routing ...

  7. html,vue, react,angular 前端实现二维码生成 ,二维码解析

    本文的背景 近期,由于项目开发的需求,需要前端实现图片二维码的解析. 由于需求的需要,这边调研了一下,发现很多人都有着类似的需求,网上给的解决方案也很多,但是感觉还是有些..... 又想到之前做过前端 ...

  8. Linux之zip加密压缩与解密解压(一百二十一)

    Linux之zip加密压缩与解密解压 1.zip加密压缩目录 # zip -rP Abc#123 tmp.zip test/2.unzip解密解压缩 # unzip -P Abc#123 tmp.zi ...

  9. 2017-08-10 前端日报

    2017-08-10 前端日报 精选 [译] 用 Node.js 搭建 API Gateway 探索 Service Worker 「生命周期」 JavaScript 中的匿名递归 deeplearn ...

最新文章

  1. ROS_Kinetic ubuntu 16.04
  2. Windows PowerShell in Action
  3. 新松机器人BG总裁高峰_新松与民航物流公司签署战略合作协议
  4. Android Open Accessory (AOA)
  5. python函数和方法的入参格式有哪些_Python函数的参数常见分类与用法实例详解
  6. 使用条件注释完成浏览器兼容
  7. Java:清空文件内容
  8. CNN结构:SPP-Net为CNNs添加空间尺度卷积-神经元层
  9. 34营销的三要素:真实诚信、诱饵引入、合理宣传
  10. 非功能性需求_更好的开卡,来聊聊非功能性需求
  11. 等宽字体与非等宽字体_我最喜欢的等宽字体
  12. 2022年python库大全
  13. itextsharp php,详解C#使用iTextSharp添加PDF水印的代码案例
  14. matlab 积分后带int,matlab int 积分
  15. SAP 工单报工批次确定自动带出批次并拆分
  16. 3DMAX建模入门:美国队长的盾牌图文教程,过程炒鸡详细(下)
  17. 微信群被封怎么办?微信群如何防封?一招解决永不封群!
  18. 秀米图文排版UEditor插件示例 新增自定义按钮没有显示 以及与neditor的适配
  19. 【源码分享】响应式风景旅游网页设计-HTML+CSS+JavaScript
  20. Java游戏项目之大鱼吃小鱼

热门文章

  1. Excel之【保护工作表】功能(工具----保护) ------可以防止修改格式,删除行。只能在里面填写数据。
  2. 【Hive】性能调优 - EXPLAIN
  3. mysql连接报java.math.BigInteger cannot be cast to java.lang.Long异常
  4. el-upload进度条无效,on-progress无效问题解决方案
  5. 如何git-cherry-pick仅更改某些文件?
  6. 在Ruby on Rails中对nil v。空v。空白的简要解释
  7. Django可扩展吗? [关闭]
  8. win11如何开启GUEST账户 windows11开启GUEST账户的设置方法
  9. win11资源管理器卡顿怎么办 Windows11解决资源管理器卡顿的步骤方法
  10. 华为电脑c盘哪些文件可以删除,c盘可以删除哪些文件