• .39-浅析webpack源码之parser.parse


      因为换了个工作,所以博客停了一段时间。

      这是上个月留下来的坑,webpack的源码已经不太想看了,又臭又长,恶心的要死,想去看node的源码……总之先补完这个

      上一节完成了babel-loader对JS文件字符串的转换,最后返回后进入如下代码:

    // NormalModule.js
    build(options, compilation, resolver, fs, callback) {
        // ...
    
        return this.doBuild(options, compilation, resolver, fs, (err) => {
            // ...
    
            // 进入这里
            try {
                this.parser.parse(this._source.source(), {
                    current: this,
                    module: this,
                    compilation: compilation,
                    options: options
                });
            } catch (e) {
                const source = this._source.source();
                const error = new ModuleParseError(this, source, e);
                this.markModuleAsErrored(error);
                return callback();
            }
            return callback();
        });
    }

      在看这个parse方法之前,需要过一下参数,首先是这个source方法。

      这个_source并不是转换后的字符串,而是进行一层封装的对象,source是其原型方法,源码如下:

    class OriginalSource extends Source {
        // value => babel-loader转换后的字符串
        // name => 'D:\workspace\node_modules\babel-loader\lib\index.js!D:\workspace\doc\input.js'
        constructor(value, name) {
            super();
            this._value = value;
            this._name = name;
        }
    
        source() {
            return this._value;
        }
    
        //...
    }

      有病啊!还是返回了转换后的字符串。

    parser.parse

      这个parser的parse方法有两部分,如下:

    parse(source, initialState) {
        let ast;
        const comments = [];
        // 这一部分负责解析源代码字符串
        for (let i = 0, len = POSSIBLE_AST_OPTIONS.length; i < len; i++) {
            // ...
        }
        // 这里再次进行尝试
        if (!ast) {
            // ...
        }
        if (!ast || typeof ast !== "object")
            throw new Error("Source couldn't be parsed");
    
        // code...
        // 这里传入parse后的ast触发parse的事件流
        if (this.applyPluginsBailResult("program", ast, comments) === undefined) {
            this.prewalkStatements(ast.body);
            this.walkStatements(ast.body);
        }
        // code...
        return state;
    }

      先不用管这里POSSIBLE_AST_OPTIONS是啥,总之这里做了两件事情

    1、对返回的字符串再做一次parse

    2、将得到的ast作为参数触发program事件流

      一个一个来,首先是parse代码块,如下:

    /*
        const POSSIBLE_AST_OPTIONS = [
        {
            ranges: true,
            locations: true,
            ecmaVersion: ECMA_VERSION,
            sourceType: "module",
            plugins: {
                dynamicImport: true
            }
        }, 
        {
            ranges: true,
            locations: true,
            ecmaVersion: ECMA_VERSION,
            sourceType: "script",
            plugins: {
                dynamicImport: true
            }
        }
    ];
    */
    // 抽象语法树
    let ast;
    // 注释数组
    const comments = [];
    for (let i = 0, len = POSSIBLE_AST_OPTIONS.length; i < len; i++) {
        if (!ast) {
            try {
                comments.length = 0;
                POSSIBLE_AST_OPTIONS[i].onComment = comments;
                // 传入JS字符串与本地默认配置参数
                ast = acorn.parse(source, POSSIBLE_AST_OPTIONS[i]);
            } catch (e) {
                // ignore the error
            }
        }
    }

      这里引入了别的模块acorn来解析字符串,负责将JS字符串解析成抽象语法树。

      这里不关心解析的过程,假设解析完成,简单看一下一个JS文件源码与解析后的树结构:

    源码

    const t = require('./module.js');
    t.fn();

    抽象语法树

    {
        "type": "Program",
        "start": 0,
        "end": 41,
        "body": [{
            "type": "VariableDeclaration",
            "start": 0,
            "end": 33,
            "declarations": [{
                "type": "VariableDeclarator",
                "start": 6,
                "end": 32,
                "id": { "type": "Identifier", "start": 6, "end": 7, "name": "t" },
                "init": {
                    "type": "CallExpression",
                    "start": 10,
                    "end": 32,
                    "callee": {
                        "type": "Identifier",
                        "start": 10,
                        "end": 17,
                        "name": "require"
                    },
                    "arguments": [{
                        "type": "Literal",
                        "start": 18,
                        "end": 31,
                        "value": "./module.js",
                        "raw": "'./module.js'"
                    }]
                }
            }],
            "kind": "const"
        }, {
            "type": "ExpressionStatement",
            "start": 34,
            "end": 41,
            "expression": {
                "type": "CallExpression",
                "start": 34,
                "end": 40,
                "callee": {
                    "type": "MemberExpression",
                    "start": 34,
                    "end": 38,
                    "object": { "type": "Identifier", "start": 34, "end": 35, "name": "t" },
                    "property": { "type": "Identifier", "start": 36, "end": 38, "name": "fn" },
                    "computed": false
                },
                "arguments": []
            }
        }],
        "sourceType": "script"
    }

      这里涉及到一个抽象语法树的规则,详情可见https://github.com/estree/estree

      接下来会调用Parser上的program事件流,定义地点如下:

    // WebpackOptionsApply.js
    compiler.apply(
        // 1
        new HarmonyModulesPlugin(options.module),
        // 2
        new UseStrictPlugin(),
    );

      地方不好找,总之一个一个过:

    HarmonyModulesPlugin

    // HarmonyModulesPlugin.js => HarmonyDetectionParserPlugin.js
    parser.plugin("program", (ast) => {
        // 这里对Import/Export的表达式进行检索
        const isHarmony = ast.body.some(statement => {
            return /^(Import|Export).*Declaration$/.test(statement.type);
        });
        if(isHarmony) {
            const module = parser.state.module;
            const dep = new HarmonyCompatibilityDependency(module);
            dep.loc = {
                start: {
                    line: -1,
                    column: 0
                },
                end: {
                    line: -1,
                    column: 0
                },
                index: -2
            };
            // 如果存在就对该模块进行特殊标记处理
            module.addDependency(dep);
            module.meta.harmonyModule = true;
            module.strict = true;
            module.exportsArgument = "__webpack_exports__";
        }
    });

      这里的正则可以参考https://github.com/estree/estree/blob/master/es2015.md的Modules部分说明,简单讲就是检索JS中是否出现过Import * from *、Export default *等等。

      如果存在会对该模块进行标记。

    UseStrictPlugin

    // UseStrictPlugin.js
    parser.plugin("program", (ast) => {
        const firstNode = ast.body[0];
        // 检测头部是否有'use strict'字符串
        if(firstNode &&
            firstNode.type === "ExpressionStatement" &&
            firstNode.expression.type === "Literal" &&
            firstNode.expression.value === "use strict") {
            // Remove "use strict" expression. It will be added later by the renderer again.
            // This is necessary in order to not break the strict mode when webpack prepends code.
            // @see https://github.com/webpack/webpack/issues/1970
            const dep = new ConstDependency("", firstNode.range);
            dep.loc = firstNode.loc;
            parserInstance.state.current.addDependency(dep);
            parserInstance.state.module.strict = true;
        }
    });

      这个就比较简单了,判断JS是否是严格模式,然后做个标记。

      事件流走完,parse方法也就调用完毕,接下来调用build方法的callback,一路回到了Compilation类的buildModule方法。

    // Compilation.js
    buildModule(module, optional, origin, dependencies, thisCallback) {
        this.applyPlugins1("build-module", module);
        if(module.building) return module.building.push(thisCallback);
        const building = module.building = [thisCallback];
    
        function callback(err) {
            module.building = undefined;
            building.forEach(cb => cb(err));
        }
        module.build(this.options, this, this.resolvers.normal, this.inputFileSystem, (error) => {
            // 处理错误与警告
            const errors = module.errors;
            for(let indexError = 0; indexError < errors.length; indexError++) {
                // ...
            }
            const warnings = module.warnings;
            for(let indexWarning = 0; indexWarning < warnings.length; indexWarning++) {
                // ...
            }
            module.dependencies.sort(Dependency.compare);
            // 事件流不存在
            if(error) {
                this.applyPlugins2("failed-module", module, error);
                return callback(error);
            }
            this.applyPlugins1("succeed-module", module);
            return callback();
        });

      这里对模块解析后的警告与错误进行处理,根据是否有错误走两个不同的事件流,然后触发callback。

      这里神神秘秘的搞个callback函数,还forEach,往上面一看,傻了吧唧的就是强行给外部callback参数弄成数组,实际上就是调用了thisCallback。

      

      现在总算摸清了callback的套路,就跟this一样,谁调用就找谁,于是这个callback回到了Compilation的_addModuleChain函数的尾部:

    // Compilation.js
    _addModuleChain(context, dependency, onModule, callback) {
        // ...
    
        this.semaphore.acquire(() => {
            moduleFactory.create({
                // ...
            }, (err, module) => {
                // ...
                // 从这里出来
                this.buildModule(module, false, null, null, (err) => {
                    if(err) {
                        this.semaphore.release();
                        return errorAndCallback(err);
                    }
                    // 这属性就是个计时器
                    // 计算从读取模块内容到构建完模块的时间
                    if(this.profile) {
                        const afterBuilding = Date.now();
                        module.profile.building = afterBuilding - afterFactory;
                    }
    
                    moduleReady.call(this);
                });
    
                function moduleReady() {
                    this.semaphore.release();
                    // 跳入下一个阶段
                    this.processModuleDependencies(module, err => {
                        if(err) {
                            return callback(err);
                        }
    
                        return callback(null, module);
                    });
                }
            });
        });
    }

      至此,模块的构建基本完成,先到这里吧……

  • 相关阅读:
    TVM性能评估分析(七)
    TVM性能评估分析(六)
    理论三:里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?
    理论二:如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?
    (转)浅谈 SOLID 原则的具体使用
    老一辈的革命先烈告诉我,凡事打好提前量
    理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?
    实战二(下):如何利用面向对象设计和编程开发接口鉴权功能?
    实战二(上):如何对接口鉴权这样一个功能开发做面向对象分析?
    12 | 实战一(下):如何利用基于充血模型的DDD开发一个虚拟钱包系统?
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/8515179.html
Copyright © 2020-2023  润新知