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]);
我们知道这端代码完全没有可读性。。。
我们需要一步一步简化它,理解它。
- 去除自执行匿名函数结构:
// 整体调用结构是这样的
(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就是依赖了哪些其他包。
了解了打包后文件是如何执行的,那么接下来我们就需要了解一下是如何进行这个打包过程的。