原来,所谓的模块化也就那么回事(mini-commonJS)
大家好,我是苏先生,一名热爱钻研、乐于分享的前端工程师,跟大家分享一句我很喜欢的话:人活着,其实就是一种心态,你若觉得快乐,幸福便无处不在
github与好文
前言
犹记得在刚开始学习接触JavaScript时总是按照文件顺序去引入不同的js文件,那时候我就在想,这玩意儿也忒麻烦了吧。直到后来接触到cli工具后,才摆脱了这种无奈。最近活不算多(ps,我好像总是这么说😂),故花点时间简单的了解下
简单了解模块化标准
COMMONJS
-
定义全局函数 require(dependency),通过传入模块标识来引入其他依赖模块,执行的结果即为别的模块暴漏出来的 API。
-
如果被 require 函数引入的模块中也包含外部依赖,则依次加载这些依赖。
-
如果引入模块失败,那么 require 函数应该抛出一个异常。
-
模块通过变量 exports 来向外暴露 API,exports 只能是一个 Object 对象,暴露的 API 须作为该对象的属性。
AMD
使用define定义模块,require导入,代表库为requireJS,较老
CMD
使用define定义模块,use引用,代表库为seaJS,较老
UMD
有点像AMD+CMD
ESM
ecma官方支持的模块规范,是未来的方向。使用export导出模块,import引入,受现代浏览器原生支持。也是vite实现的基础
本节目标
模拟实现一个commonJS,搞清楚其大致实现逻辑
注意点
不能使用require和exports作为模块的方法命名
JavaScript代码实现
- 初始化
用来做一些初始化工作,比如唯一的模块名称、缓存、暴露的数据、源代码等,当读取一个文件之后,使用MyModule类存储文件信息
- load加载器
该函数用于加载对应的文件,其实说白了就是利用node的文件读取操作将文件读取过来
但是通过node读入的文件实际上是以字符串的形式返回的,这势必要有一个解析过程
同时,总不能啥都让引入吧,应当定义一个类似白名单的东西,比如只支持.js文件的导入
也不能每次都傻傻的去解析一次,像react、vue这种一年变动不了几次的文件,大可不必次次解析,所以增加一个缓存机制,当匹配到时直接取缓存即可。事实上,在代码中引入一个不以..或./开头的都是可被缓存的文件。至于怎么去决定什么时候要更新缓存,其实仁者见仁,我觉得,在缓存中将package.json文件缓存,并设置一个定时任务,比如每一个月读取一次package.json进行对比
- 读取文件
根据传入的文件名称进行查找,如果是'./'开头则直接取当前文件路径拼上文件名,如果是'react'这样,则去查找node_modules等。这里就默认为当前文件下
- 编译
我很奇怪在文件中使用的require到底是哪里来的?global上的?貌似不是。通过阅读nodejs中相关的代码,我发现它将代码字符串通过自执行函数包了一层,也就是说它创建了一个闭包
同时引入了vm并执行了vm.runInThisContext函数,这实际上相当于是创建了一个沙盒环境
故
我们来模拟它!!!
首先
通过闭包允许我们向文件内注入一些api,如require函数
其次
我们拿到的是字符串,如果想要运行,需要使用eval或者new Function。eval会多次编译,且对于闭包和全局都具有访问权限。new Function则只编译一次,且不能访问闭包
最后
模块之间应当是封闭的、隔离的。但是闭包本身是允许通过作用域链向上访问直到全局的(使用new Function已经可以避免访问闭包),故我们需要通过with去劫持作用域,使所有的属性都首先从with劫持的对象(如obj)上尝试获取
可以看到,with是无法截断作用域链,其内部变量会首先在with上作一次for...in循环,找不到时返回false,当为false时,会跳出with向上继续查找。故需要通过Proxy进行下代理
故,我们的编译过程如下
测试
- 用例一
在当前文件下定义a.js文件,内容如下
引入并执行
- 用例二
在当前文件创建b.js并在a.js文件内引入
引入a并执行
如果本文对您有用,希望能得到您的点赞和收藏
订阅专栏,每周更新1-2篇类型体操,每月1-3篇vue3源码解析,等你哟😎
转载自:https://juejin.cn/post/7262721189646794812