承接上文。笔者之前将一个angular项目的启动过程分为了两步: 创建平台得到 PlatformRef ,以及执行平台引用提供的方法编译根模块 AppModule 。本文就将着眼于创建好的平台,从angular的茫茫源代码中看看整个AppModule的编译过程。

编译的起点

从外界使用的 bootstrapModule 方法入手。首先angular把皮球踢给了私有方法 _bootstrapModuleWithZone ,然后皮球又踢给了 compileModuleAsync 方法,这个方法比较调皮,直接进入是一个抽象方法,说明它有一个继承并且在前面作为服务被注入了,它就是!在创建爸爸平台 platformCoreDynamic 时注入的编译器工厂中提供的 createCompiler 方法返回的编译器实例,也就是 JitCompiler 。进入 JitCompiler 果不其然找到了 compileModuleAsync 方法的实现。

// 启动过程中就是用这个异步编译模块
compileModuleAsync<T>(moduleType: Type<T>): Promise<NgModuleFactory<T>> {
return Promise.resolve(this._compileModuleAndComponents(moduleType, false));
}

坏消息是皮球现在踢给了私有方法 _compileModuleAndComponents :

// 这个就是最底层编译模块的方法
private _compileModuleAndComponents<T>(
moduleType: Type<T>,
isSync: boolean
): SyncAsync<NgModuleFactory<T>> {
// 相当于 把第一个参数的结果作为第二个表达式的参数
// 但在这里只是保证两个参数顺序执行
// 最后做了三件事 _loadModules _compileComponents _compileModule
return SyncAsync.then(this._loadModules(moduleType, isSync), () => {
// 加载好模块后编译组件
this._compileComponents(moduleType, null);
// 编译模块并返回
return this._compileModule(moduleType);
});
}

稍微感受一下这个 SyncAsync 对象,很厉害:

export const SyncAsync = {
// 断言同步 也就是说 如果是个Promise 那就抛出 不是的话直接返回
assertSync: <T>(value: SyncAsync<T>): T => {
if (isPromise(value)) {
throw new Error(`Illegal state: value cannot be a promise`);
}
return value;
},
// 除了适配普通Promise外还能处理非Promise的情况 将value作为参数传入cb
then: <T, R>(
value: SyncAsync<T>,
cb: (value: T) => R | Promise<R>| SyncAsync<R>
): SyncAsync<R> => {
// 如果是Promise则继续这个promise(用cb接上),否则的话直接执行cb 还是相当于接上 强行接上不是Promise的情况
return isPromise(value) ? value.then(cb) : cb(value);
},
all: <T>(syncAsyncValues: SyncAsync<T>[]): SyncAsync<T[]> => {
// 只要里面有Promise 那就执行 Promise.all(并行执行这些Promise) 一个都没有嘛那就直接返回
return syncAsyncValues.some(isPromise) ? Promise.all(syncAsyncValues) : syncAsyncValues as T[];
}
};

现在看来,模块编译的重头戏就在这三个方法里面了:

_loadModules 负责加载模块元数据
_compileComponents 负责编译组件
_compileModule 负责编译模块

_loadModules 加载模块

加载过程是这样的:

private _loadModules(mainModule: any, isSync: boolean): SyncAsync<any> {
const loading: Promise<any>[] = []; // 最终会把所有任务放到这里 用SyncAsync 串起来执行
// 拿到模块元数据 里面包含了所有模块 以及模块涉及的服务 指令 管道
const mainNgModule = this._metadataResolver.getNgModuleMetadata(mainModule) !;
// 遍历元数据中的模块
this._filterJitIdentifiers(mainNgModule.transitiveModule.modules).forEach((nestedNgModule) => {
// getNgModuleMetadata only returns null if the value passed in is not an NgModule
// 递归获取模块元数据的万恶之源 getNgModuleMetadata
const moduleMeta = this._metadataResolver.getNgModuleMetadata(nestedNgModule) !;
// 遍历这些模块的元数据拿到其声明组件的元数据
this._filterJitIdentifiers(moduleMeta.declaredDirectives).forEach((ref) => {
const promise =
this._metadataResolver.loadDirectiveMetadata(moduleMeta.type.reference, ref, isSync); // 这货会把指令元数据(包括组件)放到缓存 返回的是null
if (promise) {
loading.push(promise);
}
});
// 遍历这些模块元数据拿到其声明管道的元数据
this._filterJitIdentifiers(moduleMeta.declaredPipes)
.forEach((ref) => this._metadataResolver.getOrLoadPipeMetadata(ref));
});
return SyncAsync.all(loading);
}

表面上代码量还不算大,不过里面还踢了几次皮球,而且存在递归,工作量非常恐怖,所做的事情如下:

  1. 执行 getNgModuleMetadata 拿到模块元数据

    • 执行 _ngModuleResolver.resolve 拿到模块本身的注解信息(即 @NgModule({}) 中的配置信息)
    • 遍历注解中的 imports 部分 将模块所有引入的元数据添加到 importedModules,并收集了里面的服务 providers (这一步会递归调用 getNgModuleMetadata 来获得模块元数据,保证除了懒加载模块外所有与根模块关联的元数据都遍历到)
    • 遍历注解中的 exports 部分 将所有Ng类(模块、组件、指令) 元数据收集到 exportedModules
    • 新建一个 transitiveModule 用来放所有导入、导出的Ng类的元数据 据注释说是写法要改所以单独拷贝一份
    • 遍历注解中的 declaration 部分,将指令元数据放到 declaredDirectives,管道元数据放到 declaredPipes,同时都添加到 transitiveModule 中
    • 遍历所有无模块包裹的指令和管道,如果已经存在于 transitiveModule 中说明是由模块自己声明的,(引入的必定有模块包裹),若不存在于 transitiveModule 中则抛出模块既未导入又未声明的错误
    • 接下来处理 providers 部分(在 imports和exports 后面处理,保证自己本身注入的服务的优先级)
    • 处理剩下的配置相关的几个东西: entryComponents bootstrap schemas
    • 缓存收集好的元数据并返回
  2. 执行 loadDirectiveMetadata 加载模块元数据中所有组件的元数据
    • 进一步遍历上面得到的 transitiveModule ,加载所有指令的元数据
  3. 执行 getOrLoadPipeMetadata 加载模块元数据中所有管道的元数据
    • 进一步遍历上面得到的 transitiveModule ,加载所有管道的元数据

至此所有与AppModule关联的模块的所有元数据都已经加载进了缓存中,包括了从 AppModule 展开的整个模块树,树上的所有指令和管道的配置,以及所有的服务。

_compileComponents 编译组件

  1. 遍历模块元数据,需要拿到所有的模板,包括: 组件的模板、入口组件的模板、组件的入口组件的模板(原来组件也有入口组件),最终拿到了所有涉及的模板,放在 templates 中
  2. 执行 _compileTemplate 编译模板
    • 处理内联样式和外联样式,最终组合到 componentStylesheet
    • 处理管道和模板 得到模板片段、访问器、用到的管道(编译时会传入所有引入的管道,然后现在会留下使用到的管道)
    • 现在拿到了三样东西: 编译好的模板、用到的管道、编译好的样式表
    • 最后一步笔者没有看明白,只知道目的是生成一个组件工厂方法,并从中取出 viewClass 和 rendererType 用来给组建元数据赋值

没有看明白的最后几行代码:

let evalResult: any;
if (!this._compilerConfig.useJit) {
evalResult = interpretStatements(outputContext.statements);
} else {
evalResult = jitStatements( // 拼接得到具体工厂方法
templateJitUrl(template.ngModule.type, template.compMeta), outputContext.statements);
}
const viewClass = evalResult[compileResult.viewClassVar];
const rendererType = evalResult[compileResult.rendererTypeVar];
// 到这一步 模板、用到的管道、样式表都已经处理好了 还创建了应该是组件的工厂方法
// 但是最后这个compile 好像把这里得到的 rendererType 一一赋值到了组件元数据中 细节待研究
template.compiled(viewClass, rendererType);

不明觉厉的 jitStatements 方法:

function evalExpression(
sourceUrl: string,
ctx: EmitterVisitorContext,
vars: {[key: string]: any}
): any {
let fnBody = `${ctx.toSource()}\n//# sourceURL=${sourceUrl}`;
const fnArgNames: string[] = [];
const fnArgValues: any[] = [];
for (const argName in vars) { // 遍历添加变量
fnArgNames.push(argName);
fnArgValues.push(vars[argName]);
}
if (isDevMode()) {
// using `new Function(...)` generates a header, 1 line of no arguments, 2 lines otherwise
// E.g. ```
// function anonymous(a,b,c
// /**/) { ... }```
// We don't want to hard code this fact, so we auto detect it via an empty function first.
const emptyFn = new Function(...fnArgNames.concat('return null;')).toString();
const headerLines = emptyFn.slice(0, emptyFn.indexOf('return null;')).split('\n').length - 1;
fnBody += `\n${ctx.toSourceMapGenerator(sourceUrl, sourceUrl, headerLines).toJsComment()}`;
}
return new Function(...fnArgNames.concat(fnBody))(...fnArgValues);
}
// 路径 语句 好像是组合了一个新的function
// 看来是用来造工厂方法的
export function jitStatements(sourceUrl: string, statements: o.Statement[]): {[key: string]: any} {
const converter = new JitEmitterVisitor(); // 新的发射访问器
const ctx = EmitterVisitorContext.createRoot(); // 新的发射访问器上下文
converter.visitAllStatements(statements, ctx); // 访问所有语句
converter.createReturnStmt(ctx); // 创建返回语句
return evalExpression(sourceUrl, ctx, converter.getArgs()); // 路径 上下文 参数 得到一个动态创建的方法
}

Anyway, all in all, whatever, 现在已经编译好了组件,放在了缓存里面。

_compileModule 编译模块

前面执行 _loadModules 仅是加载了整个模块树的元数据,现在要正式编译它们了:

private _compileModule<T>(moduleType: Type<T>): NgModuleFactory<T> {
// 从缓存拿到模块工厂
let ngModuleFactory = this._compiledNgModuleCache.get(moduleType) !;
if (!ngModuleFactory) { // 缓存中没有
const moduleMeta = this._metadataResolver.getNgModuleMetadata(moduleType) !; // 重新拿到模块元数据
// Always provide a bound Compiler
const extraProviders = [this._metadataResolver.getProviderMetadata(new ProviderMeta(
Compiler, {useFactory: () => new ModuleBoundCompiler(this, moduleMeta.type.reference)}))];
const outputCtx = createOutputContext(); // 创建输出上下文
const compileResult = this._ngModuleCompiler.compile(outputCtx, moduleMeta, extraProviders); // 执行编译
if (!this._compilerConfig.useJit) {
ngModuleFactory =
interpretStatements(outputCtx.statements)[compileResult.ngModuleFactoryVar];
} else {
// -----------------
ngModuleFactory = jitStatements( // 传入路径和语句 得到一个object key为字符串 value为 生成的工厂方法
ngModuleJitUrl(moduleMeta), outputCtx.statements, )[compileResult.ngModuleFactoryVar]; // 从这个object中拿到 key为模块变量的值 看来是造了一个工厂方法
}
this._compiledNgModuleCache.set(moduleMeta.type.reference, ngModuleFactory); // 设置工厂方法的缓存
}
return ngModuleFactory;
}

看上去还算比较明了,做的事情如下:

  1. 尝试从缓存直接拿到模块工厂
  2. 无缓存则新建一个服务商和输出上下文准备编译模块
  3. 执行 _ngModuleCompiler.compile 进行编译,过程很复杂待研究,但结果很简单,仅返回包含一个字符串值得类 NgModuleCompileResult ,看来是又把编译结果放缓存了
  4. 使用上一步的编译结果,动态创建出一个模块的工厂方法,使用的还是那个 jitStatements 方法
  5. 返回这个模块工厂方法 也就是编译的结果了

得到 模块工厂方法之后

得到模块的工厂方法之后,就跟之前平台的创建过程连接上了,使用这个工厂创建出模块引用,并注入一个NgZone以完成整个模块的启动。

比较扫兴的地方有这些:

  1. 组件和模块的工厂方法的创建细节笔者还没有摸着头脑
  2. 对于angular如何处理组件模板和模块编译的底层细节也还不明朗
  3. 截至目前还没看到真正在操作DOM的影子,顶多才开始处理组件的模板

已知的情报有这些:

  1. angular框架内部大量使用了依赖注入,大部分工作都被封装成了一个类(服务的本质就是一个类),并到处注入,甚至在编译模块时模块本身的元数据也会作为服务注入使用
  2. angular框架内部在处理元数据时大量使用了缓存,用来应对层层依赖的模块树(必然会出现很多重复的模块、指令、服务、管道的声明)
  3. angular框架底层对许多原生操作(包括ES6新特性)都实现了一层抽象,包括 Reflect 和 DOM节点的访问器

个人从源码理解JIT模式下angular编译AppModule的过程的更多相关文章

  1. [nginx] nginx源码分析--proxy模式下nginx的自动重定向auto_redirect

    描述 我们配置了一个proxy模式下的nginx, upstream backend-test { server ; } server { listen ; location = /nginx/hww ...

  2. JVM(一) OpenJDK1.8源码在Ubuntu16.04下的编译

    笔者最近在学习周志明老师编写的<深入理解Java虚拟机>一书,书中第一章的实战部分就是"自己编译JDK",不过书中提到的是OpenJDK 7的编译.由于现在Java开发 ...

  3. Pytorch学习之源码理解:pytorch/examples/mnists

    Pytorch学习之源码理解:pytorch/examples/mnists from __future__ import print_function import argparse import ...

  4. .NET Core 3.0之深入源码理解Startup的注册及运行

    原文:.NET Core 3.0之深入源码理解Startup的注册及运行   写在前面 开发.NET Core应用,直接映入眼帘的就是Startup类和Program类,它们是.NET Core应用程 ...

  5. 鸿蒙内核源码分析(工作模式篇) | CPU是韦小宝,七个老婆 | 百篇博客分析OpenHarmony源码 | v36.04

    百篇博客系列篇.本篇为: v36.xx 鸿蒙内核源码分析(工作模式篇) | CPU是韦小宝,七个老婆 | 51.c.h .o 硬件架构相关篇为: v22.xx 鸿蒙内核源码分析(汇编基础篇) | CP ...

  6. QTimer源码分析(以Windows下实现为例)

    QTimer源码分析(以Windows下实现为例) 分类: Qt2011-04-13 21:32 5026人阅读 评论(0) 收藏 举报 windowstimerqtoptimizationcallb ...

  7. Caffe源码理解2:SyncedMemory CPU和GPU间的数据同步

    目录 写在前面 成员变量的含义及作用 构造与析构 内存同步管理 参考 博客:blog.shinelee.me | 博客园 | CSDN 写在前面 在Caffe源码理解1中介绍了Blob类,其中的数据成 ...

  8. 基于SpringBoot的Environment源码理解实现分散配置

    前提 org.springframework.core.env.Environment是当前应用运行环境的公开接口,主要包括应用程序运行环境的两个关键方面:配置文件(profiles)和属性.Envi ...

  9. 深入源码理解Spring整合MyBatis原理

    写在前面 聊一聊MyBatis的核心概念.Spring相关的核心内容,主要结合源码理解Spring是如何整合MyBatis的.(结合右侧目录了解吧) MyBatis相关核心概念粗略回顾 SqlSess ...

随机推荐

  1. Google研究人员宣布完成全球首例SHA-1哈希碰撞!

    2004年的国际密码讨论年会(CRYPTO)尾声,我国密码学家王小云及其研究同事展示了MD5.SHA-0及其他相关杂凑函数的杂凑碰撞并给出了实例.时隔13年之后,来自Google的研究人员宣布完成第一 ...

  2. [知了堂学习笔记]_JSON数据操作第1讲(初识JSON)

    一.认识JSON 什么是JSON? JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式..它基于 ECMAScript (w3c制定的js规 ...

  3. jq获取图片的原始尺寸,自适应布局

    原理: each()遍历,width().height()获取宽高, load() 注意: 由于页面加载完了,但图片不一定加载完了,所以直接通过 $("img").width(), ...

  4. 源码编译安装bind

    author:JevonWei 版权声明:原创作品 编译bind 准备阶段: 下载bind软件包,然后传输到系统中 https://www.isc.org/downloads/ 安装开发包组 yum ...

  5. JavaScript实现隔行换颜色

    <!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title>& ...

  6. Project 2:传奇汉诺塔

    汉诺塔简介:汉诺塔问题是源于印度一个古老传说的益智玩具.大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘.大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在 ...

  7. 字符串和转为Data类型前后几天

    以防忘记:SimpleDateFormat 可以设置字符串的格式 package com.apploft.util.lang;import java.text.SimpleDateFormat;imp ...

  8. webservice Dome--一个webservice的简单小实例

    1.理解:webservice就是为了实现不同服务器上不同应用程序的之间的通讯 2.让我们一步一步的来做一个webservice的简单应用 1)新建一个空的web应用程序,在程序上右键,新建项目,选择 ...

  9. JSON与String之间互转

    一,String转json 这个JSON.parse()与eval()都可以实现,但是它们是有区别的, JSON.parse对json字符串要求比eval()更为严格,key名称(例如name)全部必 ...

  10. 团队作业1 团队展示&选题

    团队展示&选题 Coding项目地址:https://git.coding.net/wjunren/running.git 一.团队展示 1.队名:Runing Guys 2.队员: 组长:骆 ...