browserify源码解析1——打包后文件解析

browserify就是一个js打包工具,使用方法见:browserify-github

要学习源码 我们首先要知道这个打包工具打包出了什么。

  • 安装:
npm i browserify -g
  • main.js:
var foo = require('./foo.js');
var bar = require('./lib/bar.js');
// npm 安装的包
var gamma = require('gamma');
var w = require('./w');

var elem = document.getElementById('result');
var x = foo(100) + bar('baz');
w.c();
elem.textContent = gamma(x);
  • foo.js:
var w = require('./w');
w.c();

module.exports = function (n) { return n * 111 }
  • 打包:browserify main.js > bundle.js
  • 我们打包好了bundle.js就直接丢在script了,故我们需要了解为什么打包成了这个样子就能正常执行了。
  • 打包结果:
(function () {
    function r(e, n, t) {
        function o(i, f) {
            if (!n[i]) {
                if (!e[i]) {
                    var c = "function" == typeof require && require;
                    if (!f && c) return c(i, !0);
                    if (u) return u(i, !0);
                    var a = new Error("Cannot find module '" + i + "'");
                    throw a.code = "MODULE_NOT_FOUND", a
                }
                var p = n[i] = {
                    exports: {}
                };
                e[i][0].call(p.exports, function (r) {
                    var n = e[i][1][r];
                    return o(n || r)
                }, p, p.exports, r, e, n, t)
            }
            return n[i].exports
        }
        for (var u = "function" == typeof require && require, i = 0; i < t.length; i++) o(t[i]);
        return o
    }
    return r
})()({
    1: [function (require, module, exports) {
        var w = require('./w');
        w.c();

        module.exports = function (n) {
            return n * 111
        }
    }, {
        "./w": 5
    }],
    2: [function (require, module, exports) {
        module.exports = function (n) {
            return 111
        }
    }, {}],
    3: [function (require, module, exports) {
        var foo = require('./foo.js');
        var bar = require('./lib/bar.js');
        var gamma = require('gamma');
        var w = require('./w');

        var elem = document.getElementById('result');
        var x = foo(100) + bar('baz');
        w.c();
        elem.textContent = gamma(x);
    }, {
        "./foo.js": 1,
        "./lib/bar.js": 2,
        "./w": 5,
        "gamma": 4
    }],
    4: [function (require, module, exports) {
        // transliterated from the python snippet here:
        // http://en.wikipedia.org/wiki/Lanczos_approximation
        // gamma包里的逻辑

        module.exports = function gamma(z) {
        };
    }, {}],
    5: [function (require, module, exports) {
        module.exports = {
            c() {
                console.log(1)
            }
        }
    }, {}]
}, {}, [3]);

我们知道这端代码完全没有可读性。。。
我们需要一步一步简化它,理解它。

  1. 去除自执行匿名函数结构:
// 整体调用结构是这样的
(function(){
  let r = function(e, n, t){};
  return r
})()({
  // 1 2 3 4 5
},{},[3])

第一个()执行后其实就是返回了r函数,故等价于

r({
  // 1 2 3 4 5
},{},[3])
// 我们可以将最后括号里的三个参数 按照r函数的参数命名

所以整体代码就可以改写成:

let e = {
    1: [function (require, module, exports) {
        var w = require('./w');
        w.c();

        module.exports = function (n) {
            return n * 111
        }
    }, {
        "./w": 5
    }],
    2: [function (require, module, exports) {
        module.exports = function (n) {
            return 111
        }
    }, {}],
    3: [function (require, module, exports) {
        var foo = require('./foo.js');
        var bar = require('./lib/bar.js');
        var gamma = require('gamma');
        var w = require('./w');

        var elem = document.getElementById('result');
        var x = foo(100) + bar('baz');
        w.c();
        elem.textContent = gamma(x);
    }, {
        "./foo.js": 1,
        "./lib/bar.js": 2,
        "./w": 5,
        "gamma": 4
    }],
    4: [function (require, module, exports) {
        // transliterated from the python snippet here:
        // http://en.wikipedia.org/wiki/Lanczos_approximation
        // gamma包里的逻辑

        module.exports = function gamma(z) {
        };
    }, {}],
    5: [function (require, module, exports) {
        module.exports = {
            c() {
                console.log(1)
            }
        }
    }, {}]
};

let n = {}, t = [3];
r(e,n,t);

这样发现有木有简单很多了,接下来就是了解r函数内部执行:

function r(e, n, t) {
        function o(i, f) {
            if (!n[i]) {
                if (!e[i]) {
                    var c = "function" == typeof require && require;
                    if (!f && c) return c(i, !0);
                    if (u) return u(i, !0);
                    var a = new Error("Cannot find module '" + i + "'");
                    throw a.code = "MODULE_NOT_FOUND", a
                }
                var p = n[i] = {
                    exports: {}
                };
                e[i][0].call(p.exports, function (r) {
                    var n = e[i][1][r];
                    return o(n || r)
                }, p, p.exports, r, e, n, t)
            }
            return n[i].exports
        }
        for (var u = "function" == typeof require && require, i = 0; i < t.length; i++) o(t[i]);
        return o
    }

o函数只是一个函数声明,整这些没有用的,简化下:

function r(e, n, t) {
        for (var u = "function" == typeof require && require, i = 0; i < t.length; i++) o(t[i]);
        return o
    }

require是进行环境判断,return o可以忽视调,其实最终r函数就是一个遍历执行:

function r(e, n, t) {
        for (i = 0; i < t.length; i++) o(t[i]);
 }
// 然后我们把上面命名好的t ---- [3]代入,最终就变成了
o(3);

然后这个时候大家会有疑问t是一个什么数组,其实t是一个入口文件下标的数组,我们可以看到我们命名的变量e就是各个模块的代码,里面下标为的3的代码就是我们执行的main.js。所以这个时候就是把入口文件代入执行了。

分析到这里最后就是解析o函数:

// 我们知道执行的是o(3),所以这里的i就是3 f就是undefined
function o(i, f) {
    // 然后n为我们命名的第二参数---空对象,所以第一个if为true
    if (!n[i]) { 
        // e为我们命名的第一个参数,e[3]就是我们入口文件的代码块,如果没有找到就抛错MODULE_NOT_FOUND。找到了这个if就为false
        if (!e[i]) {
            var c = "function" == typeof require && require;
            if (!f && c) return c(i, !0);
            if (u) return u(i, !0);
            var a = new Error("Cannot find module '" + i + "'");
            throw a.code = "MODULE_NOT_FOUND", a
        }
        var p = n[i] = {
            exports: {}
        };
        e[i][0].call(p.exports, function (r) {
            var n = e[i][1][r];
            return o(n || r)
        }, p, p.exports, r, e, n, t)
    }
    return n[i].exports
} 

最终就变成了

// i = 3
function o(i, f) {
  var p = n[i] = {
    exports: {}
  };
  e[i][0].call(p.exports, function (r) {
    var n = e[i][1][r];
    return o(n || r)
  }, p, p.exports, r, e, n, t)
  return n[i].exports
} 

这里难以理解的点估计就是call其实call就是普通的函数执行,只是把this替换成call函数的第一个参数,其他参数跟着代入而已,所以可以理解为执行了e[3][0]函数而已,e[3][0]其实就是我们的入口文件函数了:

let myMainFunc = function (require, module, exports) {
  var foo = require('./foo.js');
  var bar = require('./lib/bar.js');
  var gamma = require('gamma');
  var w = require('./w');

  var elem = document.getElementById('result');
  var x = foo(100) + bar('baz');
  w.c();
  elem.textContent = gamma(x);
}

// 那么 我们为r作为参数的函数命名下
e[i][0].call(p.exports, function (r) {
    var n = e[i][1][r];
    return o(n || r)
  }, p, p.exports, r, e, n, t)

let r = function (r) {
  var n = e[i][1][r];
  return o(n || r)
}

// 虽然call里有很多参数,但是其实我们函数只接收3个参数而已,故最终变成了:
myMainFunc(r, p, p.exports)

到这里不知道大家理解没有,其实包装了这么大一串的目的就是为了给我们的入口函数包装一段function(require, module, exports){}而已,并且使这个require方法在浏览器环境里能确确实实的执行。然现在就让我们来理解下这个require函数,就是我们传入的r函数:

// i = 3
let r = function (r) {
  var n = e[i][1][r];
  return o(n || r)
}

// e[3][0]就是我们的入口函数
// 那么 e[3][1]是什么? 我们回到我们命名的e变量看下
// e[3][1] = {
  "./foo.js": 1,
  "./lib/bar.js": 2,
  "./w": 5,
  "gamma": 4
}
// 其实这就是我们入口函数依赖的那些包的路径所对应在e变量里的下标

var w = require("./foo.js");为例,那么

var n = e[3][1]["./foo.js"];
// n w为1
return o(1)

返回了一个o(1)是不是很熟悉,之前入口文件是执行的o(3)。这里又递归执行了依赖的包foo.js,并把执行结果返回出来给o(3)继续执行。其实到了这里我们就应该明白整个打包后的文件是怎么执行的了:

  • 把我们入口文件本身,连同所有依赖的包用function(require, module, exports){}包装放在一个对象里。
  • 找到入口文件的下标用o函数执行
  • 入口文件每执行到require时,其实就是找到require文件的下标并用o函数执行返回exports的结果。

其实我们require的文件也可能require了其他文件,所以o函数是一直递归执行返回结果的,e对象的每一个属性值数组下标0就是该模块本身,下标1就是依赖了哪些其他包。

了解了打包后文件是如何执行的,那么接下来我们就需要了解一下是如何进行这个打包过程的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容