node
是前端工程化不可或缺的工具,其内部使用commonJS
规范的模块化解决方案。虽然esModule
是未来的趋势,但时代更替还需一段时间,我们先来了解一下commonJS
内部的实现原理。
假设我们在a
文件里要使用b
文件的某个变量,一般会这样做。
// a文件
const b = require('./b.js')
console.log(b.someData)
复制代码
// b文件
const someData = 'im b'
module.exports = { someData }
复制代码
思考一下这两个问题
require
和module
是哪来的,是全局上挂的吗require
一个文件地址,就能取到那个文件导出的exports
require
可以分为两部分,加载器
和解析器
。我们主要讲解析器
,加载器
主要做的就是获取内容字符串,这块儿一笔带过。
我们可以用fs.readFile
或fs.readFileSync
导入一个js文件,获取该文件内容的字符串。在node
提供的require
里第一步也是要获取内容字符串,但内部肯定要更复杂。
const fs = require('fs')
const b = fs.readFileSync('./b.js', 'utf-8')
// 没有第二个参数,会得到一个buffer对象,我们要操作字符串,所以要传入字符编码
console.log(b)
/**
* const someData = 'im b'
* module.exports = { someData }
*/
复制代码
接着就要对得到的内容字符串进行操作,不同于node
的require
, 我们的这个require
接收内容字符串为第二个参数,跳过加载器,就当内容已经加载好了。这里要做一个缓存,如果要导入的模块已经存在于缓存中,则直接从缓存中取出。然后创建一个Module
的实例。
const moduleCache = new Map()
function require(pathName, source){
if(moduleCache.has(pathName)){
return moduleCache.get(pathName)
}
const module = new Module(pathName, source)
const exports = module.compile()
// 设置缓存
moduleCache.set(pathName, exports)
return exports
}
复制代码
再次说明一下,这些代码只是为了说明原理,不代表真正实现。现在应该能隐约感觉到module.compile
里会把内容字符串执行吧。好,接着开始写Module
类。constructor
没啥好说的,存了一下pathName
和source
, 声明了要导出的exports
, 并赋值空对象。
class Module{
constructor(pathName, source){
this.pathName = pathName
this.source = source
this.exports = {}
}
...
}
复制代码
在compile
里,首先我们要把内容字符串用一个函数包一下。
class Module{
...
compile(){
const iife = this.getIIFE()
...
}
getIIFE(){
return `function(module,exports,require){${this.source}}`
}
}
复制代码
哦,原来内容字符串被一个匿名函数包裹,而这个匿名函数的形参就有module
, 这就回答了开头两个问题中的第一个,module
是从哪来的。
接着调用createSandbox
, 把这个匿名函数字符串传进去。这个字符串是不是有点像抽干了水份的三体人 😄
class Module{
...
compile(){
const iife = this.getIIFE()
const sandboxFunc = this.createSandbox(iife)
...
}
...
复制代码
现在我们需要一个沙盒环境,这个沙盒环境要满足2个条件:
iife
在执行过程中遇到未定义变量,要禁止它沿着作用域链向上查找。iife
在执行过程中遇到未定义变量,则在这个对象上查找。那我们看看createSandbox
里具体要怎么做。好家伙,第一行就涉及了两个冷门知识点。使用new
调用创建一个函数实例,嗯,正常不会这么干。通过这种形式创建的函数在调用时,查找变量会直接在全局上找。相当于是在全局上定义了这个函数。
而使用了with
语句后,查找变量时会先在传入with
语句的对象上找,没找到再沿着作用域连逐层找到全局。相当于在作用域链的底部加了一个节点。
使用这两个基本没人用的特性,我们是不是能做一个沙箱满足上面的两个条件了。嗯 还不行,还差关键一步。
class Module{
...
createSandbox(iife){
const func = new Function('sandbox', `with(sandbox){return ${iife}`)
...
}
复制代码
现在做到了最开始在传入with
的对象上找,接着在全局上找。而我们想要的是只在传入with
的对象上找,没有也不往上找了,在顺手做个白名单功能,在白名单上的允许在全局上找。要做到这点就要请出强大的proxy
了。
class Module{
...
createSandbox(iife){
const func = new Function('sandbox', `with(sandbox){return ${iife}`)
return function(obj, whiteList){
const proxyedObj = new Proxy(obj, {
has(target, key){
if(!whiteList.includes(key)){
return true
}
return false
},
get(target, key, receiver){
if(key === Symbol.unscopables){
return void 0
}
return Reflect.get(target, key, receiver)
}
})
return func(proxyedObj)
}
}
}
复制代码
with
底层是通过in
操作符来判断要查找的变量是否在传入的对象上的。而proxy
的has
捕获器就是专门捕获in
操作的。
在has
捕获器里我们看到,当要查找的变量不在白名单上时返回了true, 这代表在传入with
的对象上查找啥,这个对象都会说:“有! 有”,即使这个对象上不存在。这样就把查找行为结束在了这一层。
而要查找的变量在白名单上时,这个对象就说:“这个真没有,去全局找吧”。哪怕这个对象上存在。
get
捕获器就好理解了,代理读取属性的行为。Symbol.unscopables
和Reflect.get
又是两个知识点,由于跟主内容关系不大,网上又有很多相关资料,这里就不再赘述了。
这函数一层套一层的,跟俄罗斯套娃似的,我们回到compile里捋一捋。
class Module{
...
compile(){
const iife = this.getIIFE()
const sandboxFunc = this.createSandbox(iife)
...
}
...
复制代码
sandboxFunc
是个函数,长这样。
const sandboxFunc = function(obj){
with(obj){
return function(module, exports, require){
//文件b的内容字符串
const someData = 'im b'
module.exports = { someData }
}
}
}
复制代码
调用sandboxFunc
得到内部函数,最后调用内部函数,返回exports
.
class Module{
...
compile(){
const iife = this.getIIFE()
const sandboxFunc = this.createSandbox(iife)
const coreFunc = sandboxFunc({}, ['console']) // 传入一个空对象,供with使用。console添加到白名单。
coreFunc.call(null, this, this.exports, require)
return this.exports
}
...
复制代码
回到require
里,调用compile
返回了exports
, 存到缓存中并且return出去。到这里就能回答第二个问题,为啥require
一个文件能得到这个文件里的exports
了。
const moduleCache = new Map()
function require(pathName, source){
if(moduleCache.has(pathName)){
return moduleCache.get(pathName)
}
const module = new Module(pathName, source)
const exports = module.compile()
// 设置缓存
moduleCache.set(pathName, exports)
return exports
}
复制代码
跑一下试试,可以看到模块正常导出了。
const source = `
const someData = 'im b'
module.exports = {someData}
`
console.log(require('./b.js', source))
// {someData:'im b'}
复制代码
如果了解过ATS
, 或者看过webpack的一些loader
,plugin
的源码,那这篇文章对你来说应该比较easy. 这中间穿插的一些小知识比如with
, new Function()
, Symbol.unscopables
, Reflect
等还是比较有趣,值得了解的。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。