我的從源碼理解JIT模式下angular編譯AppModule的過程

承接上文。筆者以前將一個angular項目的啓動過程分爲了兩步: 建立平臺獲得 PlatformRef ,以及執行平臺引用提供的方法編譯根模塊 AppModule 。本文就將着眼於建立好的平臺,從angular的茫茫源代碼中看看整個AppModule的編譯過程。html

編譯的起點

從外界使用的 bootstrapModule 方法入手。首先angular把皮球踢給了私有方法 _bootstrapModuleWithZone ,而後皮球又踢給了 compileModuleAsync 方法,這個方法比較調皮,直接進入是一個抽象方法,說明它有一個繼承而且在前面做爲服務被注入了,它就是!在建立爸爸平臺 platformCoreDynamic 時注入的編譯器工廠中提供的 createCompiler 方法返回的編譯器實例,也就是 JitCompiler 。進入 JitCompiler 果不其然找到了 compileModuleAsync 方法的實現。bootstrap

// 啓動過程當中就是用這個異步編譯模塊
compileModuleAsync<T>(moduleType: Type<T>): Promise<NgModuleFactory<T>> {
    return Promise.resolve(this._compileModuleAndComponents(moduleType, false));
}

壞消息是皮球如今踢給了私有方法 _compileModuleAndComponents :promise

// 這個就是最底層編譯模塊的方法
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);
}

表面上代碼量還不算大,不過裏面還踢了幾回皮球,並且存在遞歸,工做量很是恐怖,所作的事情以下:ide

  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 展開的整個模塊樹,樹上的全部指令和管道的配置,以及全部的服務。this

_compileComponents 編譯組件

  1. 遍歷模塊元數據,須要拿到全部的模板,包括: 組件的模板、入口組件的模板、組件的入口組件的模板(原來組件也有入口組件),最終拿到了全部涉及的模板,放在 templates 中
  2. 執行 _compileTemplate 編譯模板
    • 處理內聯樣式和外聯樣式,最終組合到 componentStylesheet
    • 處理管道和模板 獲得模板片斷、訪問器、用到的管道(編譯時會傳入全部引入的管道,而後如今會留下使用到的管道)
    • 如今拿到了三樣東西: 編譯好的模板、用到的管道、編譯好的樣式表
    • 最後一步筆者沒有看明白,只知道目的是生成一個組件工廠方法,並從中取出 viewClass 和 rendererType 用來給組建元數據賦值

沒有看明白的最後幾行代碼:code

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 方法:component

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節點的訪問器
相關文章
相關標籤/搜索