rollup原理

344 阅读13分钟

准备工作:

magic-string是一个增强版字符串处理的工具

var MagicString = require('magic-string');
var magicString = new MagicString('export var name = "zhufeng"');
//返回magicString的克隆,删除原始字符串开头和结尾字符之前的所有内容
console.log(magicString.snip(0, 6).toString());
//从开始到结束删除字符(原始字符串,而不是生成的字符串)
console.log(magicString.remove(0,7).toString());
//使用MagicString.Bundle可以联合多个源代码
let bundleString = new MagicString.Bundle();
bundleString.addSource({
  content: 'var a = 1;',
  separator: '\n'
})
bundleString.addSource({
  content: 'var b = 2;',
  separator: '\n'
})
console.log(bundleString.toString());

AST深度优先遍历

const acorn = require('acorn');
//在rollup里和webpack是一样,都是通过acorn把源代码转成抽象语法树
const source = `import $ from 'jquery';`;
const walk = require('./ast/walk');
//把源代码转成抽象语法树
const ast = acorn.parse(source, {
    locations: true, ranges: true, sourceType: 'module', ecmaVersion: 8
});
let indent = 0;
const padding = () => " ".repeat(indent);
//ast.body是一个数组,放置的是根语句
ast.body.forEach(statement => {
    walk(statement, {
        enter(node, parent) {
            if (node.type) {
                console.log(padding() + node.type + ' enter');
                indent += 2;
            }
        },
        leave(node, parent) {
            if (node.type) {
                indent -= 2;
                console.log(padding() + node.type + ' leave');
            }
        }
    })
});

./ast/walk:

function walk(astNode, { enter, leave }) {
    visit(astNode, null, enter, leave);
}

function visit(astNode, parent, enter, leave) {
    if (enter) {
        enter.call(null, astNode, parent);
    }
    const childKeys = Object.keys(astNode).filter(key => typeof astNode[key] === 'object');
    childKeys.forEach(childKey => {
        let value = astNode[childKey];
        if (Array.isArray(value)) {
            value.forEach(child => visit(child, astNode, enter, leave));
        } else if (value && value.type) {
            visit(value, astNode, enter, leave)
        }
    });
    if (leave) {
        leave.call(null, astNode, parent);
    }
}

module.exports = walk;

作用域的描述

let Scope = require('./ast/scope');
var a = 1;
function one() {
    var b = 2;
    function two() {
        var c = 3;
        console.log(a, b, c);
    }
    two();
}
one();


const globalScope = new Scope({ scopeName: '全局', parentScope: null, variableNames: ["a"] });
const oneScope = new Scope({ scopeName: 'oneScope', parentScope: globalScope, variableNames: ["b"] });
const twoScope = new Scope({ scopeName: 'twoScope', parentScope: oneScope, variableNames: ["c"] });

let aScope = twoScope.findDefiningScope('a');
console.log(aScope.scopeName);
let bScope = twoScope.findDefiningScope('b');
console.log(bScope.scopeName);
let cScope = twoScope.findDefiningScope('c');
console.log(cScope.scopeName);
let dScope = twoScope.findDefiningScope('d');
console.log(dScope);

./ast/scope:

class Scope {
    constructor(options) {
        this.scopeName = options.scopeName;//作用域的名字,没什么用
        this.parentScope = options.parentScope;//父作用域,它用来构建作用域链
        this.variableNames = options.variableNames || [];//此作用域内部的定义的变量
    }
    /**
     * 向当前的作用域内添加个变量name
     * @param {*} name 
     */
    add(variableName) {
        this.variableNames.push(variableName);
    }
    /**
     * 查找某个变量是在哪个作用域中定义的
     * 原理是延着作用域链向上查找,一直查到根作用域为止,如果查不到返回null
     */
    findDefiningScope(variableName) {
        //如果当前的作用域内包含name变量
        if (this.variableNames.includes(variableName)) {
            return this;//返回当前作用域 
        }
        if (this.parentScope) {
            return this.parentScope.findDefiningScope(variableName);
        }
        return null;
    }
}
module.exports = Scope;

基本流程实现:

src/main.js

import { ageA } from './ageA.js';
import { ageB } from './ageB.js';
import { ageC } from './ageC.js';
console.log(ageA, ageB, ageC);
const path = require('path');
const rollup = require('./lib/rollup');
//入口模块的绝对路径
let entry = path.resolve(__dirname, 'src/main.js');
rollup(entry, 'bundle.js');

./lib/rollup:

const Bundle = require('./bundle');
/**
 * 从入口出发进行编译 ,输出文件
 * @param {*} entry 
 * @param {*} filename 
 */
function rollup(entry, filename) {
    const bundle = new Bundle({ entry });
    bundle.build(filename);
}
module.exports = rollup

./bundle:

const fs = require('fs');
const path = require('path');
const Module = require('./module');
const MagicString = require('magic-string')
const { keys, hasOwnProperty } = require('./utils')
const walk = require('./ast/walk');
class Bundle {
    constructor(options) {
        //这样写可以兼容没有添加.js后缀或者传递的路径是一个相对路径的情况
        this.entryPath = path.resolve(options.entry.replace(/.js$/, '') + '.js');
    }
    build(filename) {
        let entryModule = this.fetchModule(this.entryPath);
        //获取入口模块所有的语句节点
        this.statements = entryModule.expandAllStatements();
        this.deConflict();
        //根据这些语句节点生成新成源代码
        const { code } = this.generate();
        //写入目标文件就可以
        fs.writeFileSync(filename, code);
    }
    fetchModule(importee) {
        let route;
        if (route) {
            let code = fs.readFileSync(route, 'utf8');
            const module = new Module({
                code,//模块源代码
                path: route,//模块的绝对路径
                bundle: this//bundle全局只有一份
            });
            return module;
        }
    }
    generate() {
        let bundleString = new MagicString.Bundle();
        this.statements.forEach(statement => {
            bundleString.addSource({
                content: source,
                separator: '\n'
            });
        });
        return { code: bundleString.toString() };
    }
}
module.exports = Bundle;

./module

const MagicString = require('magic-string');
const { parse } = require('acorn');
const analyse = require('./ast/analyse');

class Module {
    constructor({ code, path, bundle }) {
        this.code = new MagicString(code, { filename: path });//源代码
        this.path = path;//当前模块的绝对路径
        this.bundle = bundle;//当前的bundle
        this.ast = parse(code, { ecmaVersion: 8, sourceType: 'module' });//把源代码转成抽象语法树
        
        this.analyse();//开始进行语法树的分析
    }
    analyse() {
        //获取定义的变量和读取的变量
        analyse(this.ast, this.code, this);
    }
    expandAllStatements() {
        let allStatements = [];
        //循环所有的body语句,
        this.ast.body.forEach(statement => {
            allStatements.push(statement);
        });
        return allStatements;
    }
}
module.exports = Module;

./ast/analyse:

const Scope = require('./scope');
const walk = require('./walk');
const { hasOwnProperty } = require('../utils');
/**
 * 对当前的语法进行解析
 * @param {*} ast 语法树
 * @param {*} code 源代码
 * @param {*} module 模块
 */
function analyse(ast, magicString, module) {
    //遍历所有的语法一级语句或者说顶级语句
    ast.body.forEach(statement => {
      	// 给statement语法树节点添加属性
        Object.defineProperties(statement, {
            //_source 就是这个语法树的节点在源码中对应那一部分节点源代码
            _source: { value: magicString.snip(statement.start, statement.end) }
        });
    });
}
module.exports = analyse;

scope hoist简介:

举例:

function funcA() {
    var a = 'a';
    return a;
}
function funcB() {
    let a = funcA();
    console.log(a);
}


// 上述代码可以转换为:

/* function funcA() {
    var a = 'a';
    return a;
} */
//scope hoisting
function funcB() {
    var a = 'a';
    console.log(a);
}

// 即funcA可以不要

tree shaking简介:

举例,以下源码:

import {name, age} from './msg'

function say () {
	console.log('hello', name);
}
say()

./msg

export var name = 'zhufeng';
export var age = 13;

经过tree shaking之后

\

  1. export var age = 13; 未使用到的导出语句消失 了
  2. import语句也消失 了
  3. 用到的导出变量 export var name = 'zhufeng';里的export 也消失了

原理实现:

大约包含如下工作:

1、寻找import和export的变量,并暂存到Module的import和export属性中

2、构建整个作用域链,在此过程中,还给statement添加了如下属性:

_module: { value: module },

//_source 就是这个语法树的节点在源码中对应那一部分节点源代码

_source: { value: magicString.snip(statement.start, statement.end) },

//当前节点是否已经 被 包含在结果中了

_include: { value: false, writable: true },

//当前statement节点声明了哪些变量

_defines: { value: {} },

//当前statement节点依赖读取了哪些变量

_dependsOn: { value: {} },

//这是存放着当前statement对应的修改语句, 这条语句修改了什么变量

_modifies: { value: {} },

3、找到每个语句中用到的变量,再找到变量定义(分为在当前模块内定义的变量和import导入的变量)合并在一起

4、如果导入的模块中有对变量进行修改,则需要把修改的语句也要包含进来(比较难理解)

Module构造函数需要做如下改造,主要增加一些存放顶级代码块的变量定义,例如:

import

export

顶级变量定义

顶级函数定义

class Module {
    constructor({ code, path, bundle }) {
        this.code = new MagicString(code, { filename: path });//源代码
        this.path = path;//当前模块的绝对路径
        this.bundle = bundle;//当前的bundle
        this.ast = parse(code, { ecmaVersion: 8, sourceType: 'module' });//把源代码转成抽象语法树
+        this.imports = {};//记录当前模块从哪些模块导入了哪些变量
+        this.exports = {};//记录当前模块向外导出了哪些变量
+        this.definitions = {};//记录变量是在哪个语句节点中定义的
      	this.analyse();

接下来就要在analyse方法中通过遍历ast.body来往imports、exports、definitions中添加值

处理ImportDeclaration

    analyse() {
        this.ast.body.forEach(statement => {
            //找出this.imports
            if (statement.type === 'ImportDeclaration') {
                let source = statement.source.value;// ./msg
                let specifiers = statement.specifiers;
                specifiers.forEach(specifier => {
                    let importName = specifier.imported.name;//导入的变量,也就是在导入的模块里叫什么名字
                    let localName = specifier.local.name;//本地变量,在当前模块叫什么
                    //this.imports['name'] = {localName:'name',source:'./msg',importName:'name'};
                    //为了方便记忆,我把格式统一一下
                    this.imports[localName] = { localName, source, importName };
                });
            }

处理ExportNamedDeclaration

    analyse() {
        this.ast.body.forEach(statement => {
            //找出this.imports
            if (statement.type === 'ImportDeclaration') {
                //...
            }
            //找出this.exports
            if (statement.type === 'ExportNamedDeclaration') {
                let declaration = statement.declaration;
                if (declaration.type === 'VariableDeclaration') {
                    let declarations = declaration.declarations;
                    declarations.forEach(declaration => {
                        let localName = declaration.id.name;//当前模块内声明的变量名
                        let exportName = localName;
                        //记录导出了哪个变量,这个变量是通过哪个声明语名声明的
                        this.exports[exportName] = { localName, exportName, declaration };
                    });
                }
            }
        });

构建作用域链,记录哪个变量是在哪个语句中定义的,这个工作在Module.analyse成员方法中,分析import和export的后面,通过analyse工具方法来实现的:

    analyse() {
        this.ast.body.forEach(statement => {
            //找出this.imports
            if (statement.type === 'ImportDeclaration') {
                //...
            }
            //找出this.exports
            if (statement.type === 'ExportNamedDeclaration') {
                //...
            }
        });
      	analyse(this.ast, this.code, this);

在analyse里面,我们每遇到一个函数定义,就要创建一个作用域对象,并分析在这个作用域下,有哪些变量和函数定义,分析出来之后加到这个作用域当中(代码体现在walk的enter回调中),很自然的,我们需要一个变量来跟踪遍历语法树的过程中创建的当前作用域对象

currentScope就充当这个角色,它初始化的值是顶级作用域,或叫全局作用域

const Scope = require('./scope');
const walk = require('./walk');
const { hasOwnProperty } = require('../utils');
/**
 * 对当前的语法进行解析
 * @param {*} ast 语法树
 * @param {*} code 源代码
 * @param {*} module 模块
 */
function analyse(ast, magicString, module) {
    let currentScope = new Scope({ name: '全局作用域' });//相当于模块内的顶级作用域,也就是最外层的作用域
    //遍历所有的语法一级语句或者说顶级语句
    ast.body.forEach(statement => {
        //把变量添加到当前的作用域内
        function addToScope(name, isBlockDeclaration) {
            let added = currentScope.add(name, isBlockDeclaration);
            //判断当前scope是不是顶级作用域,如果是顶级的话就往 statement挂一个变量,表示它声明一个顶级变量
            if (!currentScope.parent || !added) {
                //在当前的语句中添加一个定义的变量
                statement._defines[name] = true;
            }
        }
        //给statement语法树节点添加属性
        Object.defineProperties(statement, {
            _module: { value: module },
            //_source 就是这个语法树的节点在源码中对应那一部分节点源代码
            _source: { value: magicString.snip(statement.start, statement.end) },
            //当前节点是否已经 被 包含在结果中了
            _include: { value: false, writable: true },
            //当前statement节点声明了哪些变量
            _defines: { value: {} },
            //当前statement节点依赖读取了哪些变量
            _dependsOn: { value: {} },
            //这是存放着当前statement对应的修改语句, 这条语句修改了什么变量
            _modifies: { value: {} },
        });
        walk(statement, {
            enter(node) {
                let newScope;
                switch (node.type) {
                    //如果当前的节点是函数声明的话
                    case 'FunctionDeclaration':
                        //把函数的名称添加到当前的作用域,也就是全局作用域里
                        addToScope(node.id.name, false);
                        let names = node.params.map(param => param.name);
                        newScope = new Scope({ name: node.id.name, parent: currentScope, names, block: false });
                        break;
                    case 'VariableDeclaration':
                        node.declarations.forEach(declaration => {
                            if (node.kind === 'let' || node.kind === 'const') {
                                addToScope(declaration.id.name, true);//这是一个块级声明 const let
                            } else {
                                addToScope(declaration.id.name, false);//var
                            }
                        });
                        break;
                    case 'BlockStatement':
                        newScope = new Scope({
                            parent: currentScope,
                            block: true
                        });
                        break;
                }
                //如果有值说明当前的节点创建了新的作用域
                if (newScope) {
                    Object.defineProperty(node, '_scope', { value: newScope });
                    currentScope = newScope;
                }
            },
            leave(node) {
              	// 遍历完离开的时候回到父级作用域
                if (hasOwnProperty(node, '_scope')) {
                    currentScope = currentScope.parent;
                }
            }
        });
    });

在构建完作用域链之后,找到当前模块内声明的哪些变量之后

还需要找出当前模块内使用到了哪些变量,这部分放在了analyse的最后面:

function analyse(ast, magicString, module) {
    let currentScope = new Scope({ name: '全局作用域' });//相当于模块内的顶级作用域,也就是最外层的作用域
    //遍历所有的语法一级语句或者说顶级语句
    ast.body.forEach(statement => {
        //把变量添加到当前的作用域内
        function addToScope(name, isBlockDeclaration) {
            // ...
        }
        //给statement语法树节点添加属性
        Object.defineProperties(statement, {
            _module: { value: module },
            //_source 就是这个语法树的节点在源码中对应那一部分节点源代码
            _source: { value: magicString.snip(statement.start, statement.end) },
            //当前节点是否已经 被 包含在结果中了
            _include: { value: false, writable: true },
            //当前statement节点声明了哪些变量
            _defines: { value: {} },
            //当前statement节点依赖读取了哪些变量
            _dependsOn: { value: {} },
            //这是存放着当前statement对应的修改语句, 这条语句修改了什么变量
            _modifies: { value: {} },
        });
        walk(statement, {
            enter(node) {
                // ...
            },
            leave(node) {
                // ...
            }
        });
    });
    //在构建完作用域链之后,找到当前模块内声明的哪些变量之后
    //还需要找出当前模块内使用到了哪些变量 
    ast.body.forEach(statement => {
        function checkForReads(node) {
            if (node.type === 'Identifier') {
                statement._dependsOn[node.name] = true;
            }
        }
        function checkForWrites(node) {
            function addNode(node) {//TODO
                if (node.type === 'Identifier') {
                    statement._modifies[node.name] = true;
                }
            }
            if (node.type === 'AssignmentExpression') {
                addNode(node.left);
            } else if (node.type === 'UpdateExpression') {
                addNode(node.argument);
            }
        }
        walk(statement, {
            enter(node) {
                checkForReads(node);//检查当前节点读取了哪些变量
                checkForWrites(node);//检查当前节点修改了哪些变量
            }
        });
    });
}

以这段代码为例:

import { name, age } from './msg'
function say(hi) {
	console.log(hi, name);
}
say('aaa');

在ast中,读取变量的情况,例如say方法里的hi参数,name变量,以及say的调用,这些statement的节点type都是Identifier,即标识符,这些都会放在_dependsOn里面

在处理完ast树中的import、export,变量使用,函数调用后,我们要找到变量使用、调用方法的语句,然后将对应的变量和这些语句的映射存在definitions和modifications里面,供接下来expandAllStatements方法来用

    analyse() {
        this.ast.body.forEach(statement => {
            //找出this.imports
            if (statement.type === 'ImportDeclaration') {
                //...
            }
            //找出this.exports
            if (statement.type === 'ExportNamedDeclaration') {
                //...
            }
        });
      	analyse(this.ast, this.code, this);
        this.ast.body.forEach(statement => {
            //statement._defines可以从语句获取变量名
            //可以从变量名叫定义这个变量的语句
            Object.keys(statement._defines).forEach(name => {
                //this.definitions['say']=function say(hi){}
                this.definitions[name] = statement;
            });
            //这里存放的是当前语句更新到的所有的变量
            Object.keys(statement._modifies).forEach(name => {
                //this.definitions['say']=function say(hi){}
                if (!hasOwnProperty(this.modifications, name)) {
                    this.modifications[name] = [];
                }
                this.modifications[name].push(statement);
            });
        });

expandAllStatements做的事情是:找到变量定义的地方,将这些变量定义和对这些变量处理的语句拿过来合并到这里,举例来说:

index.js:

import {name, age} from './msg'

console.log('hello', name);

./msg

export var name = 'zhufeng';
export var age = 13;

index里面用到了name这个变量,name又是在msg里面定义的,所以打包export var name = 'zhufeng';会和console.log('hello', name);放在一起:

export var name = 'zhufeng';
console.log('hello', name);

然后再将第1行export var name = 'zhufeng';中的export去掉,形成最终的代码

expandAllStatements的调用在Bundle.build里面,构建完entryModule的地方:

实际上这里的fetchModule名字起的不合适,fetchModule方法不仅获取了模块,还对模块进行了语法树分析,并在语法树上添加了_source、_include、_defines等属性

class Bundle {
    constructor(options) {
        //这样写可以兼容没有添加.js后缀或者传递的路径是一个相对路径的情况
        this.entryPath = path.resolve(options.entry.replace(/.js$/, '') + '.js');
    }
    build(filename) {
        let entryModule = this.fetchModule(this.entryPath);
        //获取入口模块所有的语句节点
        this.statements = entryModule.expandAllStatements();

module.js:

expandAllStatements描述整个Module的代码块展开过程,内部会拿到this.ast.body中的每个statement表达式,挨个遍历,挨个遍历表达式的方法是expandStatement

expandStatement里面会通过statement._dependsOn取到当前的statement里面包含哪些标识符,或者说依赖哪些变量,拿到这些变量名后分别调用define方法找到这个变量名在哪里定义的

这个定义可能是普通的let、var的定义形式,也有可能是import导入的,因此在define方法中会根据这两种定义方式来判断

如果是普通的let、var,这样的定义形式,直接从this.definitions中去取出变量定义的语句,此处我们可以看到又判断了一下statement._include是否为true,其实我们在expandStatement方法中试图添加变量定义时如果进入这个方法的statement参数本身就是一个变量定义的statement,就会将其_include标识置为true,但在最终expandAllStatements方法中我们会把类型为VariableDeclaration和FunctionDeclaration的statement语句也都屏蔽掉,所以能走到expandStatement,进而再走到define方法中的语句都是非定义类型的语句,这一类语句中用到的变量,第一次进入define的时候,statement._include都会是false,都会继续走expandStatement找到其定义,把它们定义的statement放到一个数组中返回出来

接下来再讨论import形式变量的引入,对于这种的,我们要先通过模块中的imports,即Module.imports找到它是在哪个import语句中定义的,再找这个import是从那个module导入的,再从这个module的export中找到对应变量的定义拿到这里合并起来用

\

class Module {
  	// ...
    expandAllStatements() {
        let allStatements = [];
        //循环所有的body语句,
        this.ast.body.forEach(statement => {
            //如果是导入语句的话直接忽略 ,不会放在结果 里
            if (statement.type === 'ImportDeclaration') return;
            let statements = this.expandStatement(statement);
            allStatements.push(...statements);
        });
        return allStatements;
    }
    expandStatement(statement) {
        //第二进来的是在msg模块的 var age = 13;语句
        statement._include = true;
        let result = [];
        //获得这个语句依赖或者说使用到了哪些变量
        //var age = 13;
        const depends = Object.keys(statement._dependsOn);
        depends.forEach(dependName => {
            //找到这个依赖的变量对应的变量定义语句
            let definition = this.define(dependName);
            result.push(...definition);
        });
        result.push(statement);
        return result;
    }
  	//返回此变量对应的定义语句
    define(name) {
        //判断这个变量是外部导入的,还是模块内声明的
        if (hasOwnProperty(this.imports, name)) {
            //localName name2 importName name source home
            const { localName, importName, source } = this.imports[name];
            //获取依赖的模块 source依赖的模块名 this.path=当前模块的绝对路径
            let importModule = this.bundle.fetchModule(source, this.path);
            //externalLocalName=localName=hname
            let { localName: externalLocalName } = importModule.exports[importName];
            return importModule.define(externalLocalName);
            //说明是模块自己声明的
        } else {
            //获取本模块内的变量声明语句,如果此语句没有包含过的话,递归添加到结果 里
            let statement = this.definitions[name];
            if (statement) {
                if (statement._include) {
                    return [];
                } else {
                    return this.expandStatement(statement);
                }
            } else if (SYSTEMS.includes(name)) {//console log
                return [];
            } else {
                throw new Error(`ReferenceError: ${name} is not defined in current module`);
            }
        }
    }
}

最后再处理一下Bundle.generate:

class Bundle {
    constructor(options) {
        // ...
    }
    build(filename) {
        // ...
    }
    fetchModule(importee) {
        // ...
    }
    generate() {
        let bundleString = new MagicString.Bundle();
        this.statements.forEach(statement => {
            const source = statement._source.clone();
            if (/^Export/.test(statement.type)) {
                source.remove(statement.start, statement.declaration.start);
            }
            bundleString.addSource({
                content: source,
                separator: '\n'
            });
        });
        return { code: bundleString.toString() };
    }
}

对于声明了但没有使用的变量、函数都不应该打包到最终的代码块中,例如:

import { name, age } from './msg'
var a = 1;
function say(hi) {
	console.log(hi, name);
}
say('aaa');

这个里面的var a = 1定义的a变量在后面并没有用到,但是也被打包到最终代码中了,这是因为在Bundle.build -> Module.expandAllStatements中我们只排除了ImportDeclaration类型的表达式,而在接下来的Module.expandStatement里对于每个语句我们都会将其加入result数组中,这个result数组中的每一项都会在最后的generate方法里生成到文件中去,所以我们这里要把变量、函数定义这些种类的statement在Module.expandAllStatements中排除掉,而让真正的使用变量的statement走到Module.expandStatement里面,再在Module.expandStatement里去寻找对应的依赖,放到result中

    expandAllStatements() {
        let allStatements = [];
        //循环所有的body语句,
        this.ast.body.forEach(statement => {
            //如果是导入语句的话直接忽略 ,不会放在结果 里
            if (statement.type === 'ImportDeclaration') return;
            if (statement.type === 'VariableDeclaration') return;
            if (statement.type === 'FunctionDeclaration') return;
            let statements = this.expandStatement(statement);
            allStatements.push(...statements);
        });
        return allStatements;
    }

包含修改语句

这里的修改语句其实特指entryModule导入的模块中的变量修改语句,例如:

main.js:

import {name, age} from './msg'
console.log(age)

./msg

export var name = 'zhufeng';
export var age = 13;
age = 10;
age += 1;
age++;

注意:在./msg里面,我们对age做了一定修改:

age = 10;

age += 1;

age++;

但在最终生成的代码中,我们没能把这些修改语句打包进去,因为我们并没有分析依赖的Module里面变量的修改这种情况,我们需要将Module下的expandStatement方法做如下改动:

class Module {
		// ...
    expandStatement(statement) {
        //第二进来的是在msg模块的 var age = 13;语句
        statement._include = true;
        let result = [];
        //获得这个语句依赖或者说使用到了哪些变量
        //var age = 13;
        //A阶段
        const depends = Object.keys(statement._dependsOn);
        depends.forEach(dependName => {
            //找到这个依赖的变量对应的变量定义语句
            let definition = this.define(dependName);
            result.push(...definition);
        });
        //B阶段 
        result.push(statement);
        //再找定义的变量的修改语句
        // 对于main来说,这个defines是空的
        //C阶段
        const defines = Object.keys(statement._defines);
        defines.forEach(name => {//age
            //找到当前模块内对这个变量的修改句 找age变量的修改语句
            const modifications = hasOwnProperty(this.modifications, name) && this.modifications[name];
            if (modifications) {
                modifications.forEach(statement => {
                    if (!statement._include) {
                        let statements = this.expandStatement(statement);
                        result.push(...statements)
                    }
                });
            }
        });
        return result;
    }
}

这个执行过程比较复杂,我们详细分析一下:

1、从main.js的console.log(age)这行代码,走到expandStatement方法里开始看起,这个表达式的_dependsOn(即statement._dependsOn)为['console', 'log', 'age'],console和log就不必看了

2、'age'这个依赖进入this.define方法之后,由于它是import进来的,所以走到了

    define(name) {
        //判断这个变量是外部导入的,还是模块内声明的
        if (hasOwnProperty(this.imports, name)) {
            //localName name2 importName name source home
            const { localName, importName, source } = this.imports[name];
            //获取依赖的模块 source依赖的模块名 this.path=当前模块的绝对路径
            let importModule = this.bundle.fetchModule(source, this.path);
            //externalLocalName=localName=hname
            let { localName: externalLocalName } = importModule.exports[importName];
            return importModule.define(externalLocalName);

3、接下来,就会调用this.bundle.fetchModule(source, this.path); fetchModule内部,会依次走new Module -> this.analyse等等初始化这个模块

4、初始化完成后,我们就可以从这个模块的exports中取到./msg里export出来的age了,即:

export var age = 13;

5、接下来再走到importModule.define(externalLocalName);,即msg这个模块的define方法,在msg模块中,age是通过var定义的:

export var age = 13

因此,会走到else这个分支:

    define(name) {
        //判断这个变量是外部导入的,还是模块内声明的
        if (hasOwnProperty(this.imports, name)) {
						// ...
        } else {
            //获取本模块内的变量声明语句,如果此语句没有包含过的话,递归添加到结果 里
            let statement = this.definitions[name];
            if (statement) {
                if (statement._include) {
                    return [];
                } else {
                    return this.expandStatement(statement);
                }
            } else if (SYSTEMS.includes(name)) {//console log
                return [];
            } else {
                throw new Error(`ReferenceError: ${name} is not defined in current module`);
            }
        }
    }

6、接下来再调用this.expandStatement(statement),其参数statement就是定义age的statement,在这里会寻找statement._dependsOn(空数组),再将它本身添加到result中(result.push(statement)),最后再找模块中对age的modifications,最终把result返回回去,此时result里应该放着:

var age = 13;

age = 10;

age += 1;

age++;

7、这个result返回到哪里了呢?先是返回到msg模块的define方法,define方法再将它返回到main模块的expandStatement中,在expandStatement中,main模块的A阶段执行完毕

继续执行B阶段,将该statement本身放到result中去:result.push(statement);

\

对于如下不太符合规范的写法

if (true) {
	var name = 11;
}
console.log(name);

我们在Scope类中处理作用域的时候,对上面这种写法会做如下处理:

class Scope {
    constructor(options) {
        this.name = options.name;//作用域的名字,没什么用
        this.parent = options.parent;//父作用域,它用来构建作用域链
        this.names = options.names || [];//此作用域内部的定义的变量
        this.isBlockScope = !!options.block;//当前作用域是否是块级作用域
    }
    /**
     * 向当前的作用域内添加个变量name
     * @param {*} name 
     */
    add(name, isBlockDeclaration) {
        //如果这个变量不是一个块级声明,并且当前作用域是一个块级作用域的话 
        //当前是一个块级作用域,声明的变量是var
        if (!isBlockDeclaration && this.isBlockScope) {
            //不添加到自己的作用域,而是添加到父作用域
            this.parent.add(name, isBlockDeclaration);
            return false;
        } else {
            this.names.push(name);
            return true;
        }
    }

解决变量名冲突

main.js:

import { ageA } from './ageA.js';
import { ageB } from './ageB.js';

console.log(ageA, ageB);

./ageA.js

var age = '年龄';
export var ageA = age + '1'

./ageB.js

var age = '年龄';
export var ageB = age + '2'

在main中,import了ageA ageB两个模块,每个模块中都有age定义,二者在合并时,就会把这个变量定义都拿过来合并到一起,就会有冲突

实现这个功能,我们要改造一下Module类:

首先我们需要添加canonicalNames属性

class Module {
    constructor({ code, path, bundle }) {
        this.code = new MagicString(code, { filename: path });//源代码
        this.path = path;//当前模块的绝对路径
        this.bundle = bundle;//当前的bundle
        this.ast = parse(code, { ecmaVersion: 8, sourceType: 'module' });//把源代码转成抽象语法树
        this.imports = {};//记录当前模块从哪些模块导入了哪些变量
        this.exports = {};//记录当前模块向外导出了哪些变量
        this.definitions = {};//记录变量是在哪个语句节点中定义的
        this.modifications = {};//记录修改变量的语句
        this.canonicalNames = {};//这里放置着所有的变量名重命名后的结果

再添加一个rename方法专门用来替换变量名:

class Module {
    constructor({ code, path, bundle }) {
        // ...
    }
    rename(name, replacement) {
        //name是原来的变量名 replacement替换后的变量名
        this.canonicalNames[name] = replacement;
    }

然后在Bundle中添加deConflict方法:

class Bundle {
    constructor(options) {
        //这样写可以兼容没有添加.js后缀或者传递的路径是一个相对路径的情况
        this.entryPath = path.resolve(options.entry.replace(/.js$/, '') + '.js');
    }
    build(filename) {
        let entryModule = this.fetchModule(this.entryPath);
        //获取入口模块所有的语句节点
        this.statements = entryModule.expandAllStatements();
        this.deConflict();
        //根据这些语句节点生成新成源代码
        const { code } = this.generate();
        //写入目标文件就可以
        fs.writeFileSync(filename, code);
    }
    deConflict() {
        const defines = {};//定义的变量
        const conflict = {};//变量我冲突的变量
        this.statements.forEach(statement => {
            keys(statement._defines).forEach(name => {
                if ((hasOwnProperty(defines, name))) {
                    conflict[name] = true;
                } else {
                    defines[name] = [];
                }
                defines[name].push(statement._module);
            });
        });
        keys(conflict).forEach(name => {
            const modules = defines[name];
            modules.pop();//弹出最后一个,最后一个模块不需要重命名
            //modules.shift();//弹出第一个,第一个模块不需要重命名
            modules.forEach((module, index) => {
                module.rename(name, `${name}$${modules.length - index}`);
            });
        });

    }

这里面主要的逻辑是遍历statements中各条语句,找到每个statement中定义的变量,然后构建一个 变量名-对应模块的映射关系,放到defines对象中

在我们的案例中statements结构如下:

var age = '年龄';
var ageA = age + '1'
var age = '年龄';
var ageB = age + '2'
var age = '年龄';
var ageC = age + '3'
console.log(ageA, ageB, ageC);

这7行代码,它们的_defines属性分别是:

{ age: true }
{ ageA:true, age: true }
{ age: true }
{ ageB:true, age: true }
{ age: true }
{ ageC:true, age: true }
{}

通过遍历statements,我们构建出defines和conflict两个结构:

defines:

{
	age: [ageA对应的模块, ageB对应的模块, ageC对应的模块],
  ageA: [ageA对应的模块],
  ageB: [ageB对应的模块],
  ageC: [ageC对应的模块]
}

conflict:

{
	age: true
}

接下来再拿到变量名有冲突的地方,本案例中就是age变量,在各自的模块中重新定义一个唯一的名字,在每个模块中canonicalNames会存储着冲突的名字和改名之后名字的映射

在本案例中,经过此番处理,ageA ageB ageC 3个模块中,canonicalNames的结构就会变成:

ageA:

{
	age: age2
}

ageB:

{
	age: age1
}

ageC:

{
	age: age
}

最后在generate方法中生成代码的时候,如果遇到了冲突的变量名,将其替换即可:

class Bundle {
  	// ...
		generate() {
        let bundleString = new MagicString.Bundle();
        this.statements.forEach(statement => {
            let replacements = {};
            keys(statement._dependsOn)//用到的变量
                .concat(keys(statement._defines))//定义的变量
                .forEach(name => {
                    const canonicalName = statement._module.getCanonicalName(name);
                    if (name !== canonicalName) {
                        replacements[name] = canonicalName;
                    }
                });
            const source = statement._source.clone();
            if (/^Export/.test(statement.type)) {
                source.remove(statement.start, statement.declaration.start);
            }
            replaceIdentifiers(statement, source, replacements);
            //把每个astNode语法树节点代码添加到bundleString
            bundleString.addSource({
                content: source,
                separator: '\n'
            });
        });
        return { code: bundleString.toString() };
    }