承接上文。笔者之前将一个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. dbUtils的基本使用

    dbUtils是对JDBC的完全封装,只需要导入连接池,添加sql语句,dbUtils就可以返回各种封装形式的结果集. 如果纯手动的话,在DAO层使用JDBC查询一个语句的话,需要以下几步: 1创建C ...

  2. 浏览器F12进行Web程序调试

    转自http://www.cnblogs.com/yougewe/p/5152700.html 引语:如今的整个Web开发行业甚至说整个软件开发行业,已经相当成熟,基本上已经很少找不到没有前人做过的东 ...

  3. 使用Coding.net+Hexo+node.js+git来搭建个人博客

    使用Coding.net来搭建基于Hexo的博客 一.准备工作 什么是Coding.net Coding可以说,就是国产的Github,但是,有一个功能使它似乎超越了GitHub-那就是 Web ID ...

  4. Linux 查看进程

    1.ps 显示进程信息,参数可省略 -aux 以BSD风格显示进程常用 -efH 以SystemV风格显示进程 -e , -A 显示所有进程 a 显示终端上所有用户的进程 x 显示无终端进程 u显示详 ...

  5. Ext.grid.EditorGridPanel分页刷新

    store.reload(); var start = grid.getBottomToolbar().cursor;//获取当前页开始条数 上面获取当前页第一条记录的方法有时候说未定义,我现在使用下 ...

  6. Entity Framework Core 2.0 中使用LIKE 操作符

    Entity Framework Core 2.0 中使用LIKE 操作符 不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译 ...

  7. [js高手之路]javascript腾讯面试题学习封装一个简易的异步队列

    这道js的面试题,是这样的,页面上有一个按钮,一个ul,点击按钮的时候,每隔1秒钟向ul的后面追加一个li, 一共追加10个,li的内容从0开始技术( 0, 1, 2, ....9 ),首先我们用闭包 ...

  8. c语言中的文件格式化读写函数fscanf和fprintf函数

    很多时候我们需要写入数据到文件中时都觉得很困扰,因为格式乱七八槽的,可读性太差了,于是我们就想有没有什么函数可以格式化的从文件中输入和输出呢,还真有.下面我将讲解一下fscanf和fprintf的强大 ...

  9. 【小白成长撸】--多项式求圆周率PI

    /*程序的版权和版本声明部分: *Copyright(c) 2016,电子科技大学本科生 *All rights reserved. *文件名:多项式求PI *程序作用:计算圆周率PI *作者:Amo ...

  10. 关于SpringMVC项目报错:java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/xxxx.xml]

    关于SpringMVC项目报错:java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/xxxx ...