前言:你们好,我叫邵威儒,你们都喜欢喊我小邵,学的金融专业却凭借兴趣爱好入了程序猿的坑,从大学买的第一本vb和自学vb,我就与编程结下不解之缘,随后自学易语言写游戏辅助、交易软件,至今进入了前端领域,看到很多朋友都写文章分享,本身也弄一个玩玩,如下文章纯属我的理解,便于记录学习,确定有理解错误或理解不到位的地方,意在站在前辈的肩膀,分享我的对技术的通俗理解,共同成长!javascript
后续我会陆陆续续更新javascript方面,尽可能把javascript这个学习路径体系都写一下
包括前端所经常使用的es六、angular、react、vue、nodejs、koa、express、公众号等等
都会从浅到深,从入门开始逐步写,但愿能让你们有所收获,也但愿你们关注我~前端
文章列表:juejin.im/user/5a84f8…
小邵教你玩转nodejs系列:juejin.im/collection/…vue
Author: 邵威儒
Email: 166661688@qq.com
Wechat: 166661688
github: github.com/iamswr/java
十分抱歉,最近比较忙,临时须要把h5快速迁移到app,又不懂co swift android,基本天天都在踩坑当中,不过收获也很大,未来前端的趋势确定是大前端,无论是pc 移动m站仍是app 后端都须要懂一些,后面有时间,我也会把最近接触ios/android原生遇到的问题总结出来。node
在这篇文章中,你会明白模块化的发展,明白以sea.js为表明的cmd规范和以require.js为表明的amd规范的异同,剖析node.js使用的commonJs规范的源码以及手写实现简陋版的commonJs,最后你会明白模块化是怎样一个加载过程。react
咱们知道javascript最开始是面向过程的思惟编程,随着代码愈来愈庞大、复杂,在这种实际遇到的问题中,大佬们逐渐把面向对象、模块化的思想用在javascript当中。android
// 好比getCssAttr函数来获取Css属性,当咱们须要获取Css属性的时候能够直接调用该方法
function getCssAttr(obj, attr) {
if (obj.currentStyle) {
return obj.currentStyle[attr];
} else {
return window.getComputedStyle(obj, null)[attr];
}
}
// 好比toJSON函数可以把url的query转为JSON对象
function toJSON(str) {
var obj = {}, allArr = [], splitArr = [];
str = str.indexOf('?') >= 0 ? str.substr(1) : str;
allArr = str.split('&');
for (var i = 0; i < allArr.length; i++) {
splitArr = allArr[i].split('=');
obj[splitArr[0]] = splitArr[1];
}
return obj;
}
复制代码
这样getCssAttr函数和toJSON组成了模块,当须要使用的时候,直接调用便可,可是随着项目代码量愈来愈庞大和复杂,并且这种方式会对全局变量形成了污染。ios
let utils = new Object({
getCssAttr:function(){...},
toJSON:function(){...}
})
复制代码
当须要调用相应函数时,咱们经过对象调用便可,utils.getCssAttr()
、utils.toJSON()
,可是这样会存在一个问题,就是能够直接经过外部修改内部方法属性。git
utils.getCssAttr = null
复制代码
答案是能够的,咱们能够经过闭包的方式,使私有成员不暴露在外部。es6
let utils = (function(){
let getCssAttr = function(){...}
let toJSON = function(){...}
return {
getCssAttr,
toJSON
}
})()
复制代码
这样的话,外部就没法改变内部的私有成员了。
试想一下,若是一个项目,全部轮子都本身造,在如今追求敏捷开发的环境下,咱们有必要全部轮子都本身造吗?一些经常使用通用的功能,是否能够提取出来,供你们使用,提升开发效率?
正所谓,无规矩不成方圆,每一个程序猿的代码风格确定是有差别的,你写你的,我写个人,这样就很难流通了,可是若是你们都遵循一个规范编写代码,造成一个个模块,就显得很是重要了。
在这样的背景下,造成了两种规范,一种是以sea.js为表明的CMD规范,另一种是以require.js为表明的AMD规范。
这一点必定要明白,很是重要!
这一点必定要明白,很是重要!
这一点必定要明白,很是重要!
在node.js中是遵循commonJS规范的,在对模块的导入是同步的,为何这样说?由于在服务器中,模块都是存在本地的,即便要导入模块,也只是耗费了从硬盘读取模块的时间,并且可控。
可是在浏览器中,模块是须要经过网络请求获取的,若是是同步获取的话,那么网络请求的时间没办法保证,会形成浏览器假死的,可是异步的话,是不会阻塞主线程,因此无论是CMD仍是AMD,都是属于异步的,CMD和AMD都是属于异步加载模块,当所须要依赖的模块加载完毕后,才经过一个回调函数,写咱们所须要的业务逻辑。
延迟执行,依赖就近
,而AMD是提早执行,依赖前置
(require2.0开始能够改为延迟执行),怎么理解呢?看看下面代码// CMD
define(function(require,exports,module){
var a = require('./a')
a.run()
var b = require('./b')
b.eat()
})
// AMD
define(['./a','./b'],function(a,b){
a.run()
b.eat()
})
复制代码
上面CMD和AMD都是异步获取到这些模块,可是加载的时机是不一样的
CMD是使用的时候再进行加载
AMD则是执行回调函数以前就已经把模块加载了
这样的话会存在一个问题,就是在CMD执行的时候,require模块的时候,
由于要加载指定的模块,因此当执行到var a = require('./a')、var b = require('./b')
的时候,会稍微耗费多一些时间,也就是俗称的懒加载,因此CMD中执行
这个回调函数的时间会比AMD的快。
更正:表达不许确,应该是开始执行回调函数的时间,并不是执行回调函数的过程
可是在AMD中,是预加载,意思就是执行回调函数以前就把依赖的模块都加载完了,
因此AMD执行回调函数的时间会比CMD慢,可是由于已经预加载了,在AMD执行回
调函数内的业务逻辑会比CMD快。 CMD AMD 执行回调函数的时机 快 慢 执行回调函数内的业务 慢 快![]()
咱们分别建立两个文件useModule.js
、module.js
,而且打上断点。
// useModule.js
let utils = require('./module')
utils = require('./module')
utils.sayhello()
复制代码
// module.js
let utils = {
sayhello:function(){
console.log('hello swr')
}
}
module.exports = utils
复制代码
而后开始执行,咱们首先会进入commonJs的源码了
在最上面能够看出是一个闭包的形式(function(exports,require,module,__filename,__dirname))
,这里能够看出__dirname
和__filename
并不是是global
上的属性,而是每一个模块对应的路径。
并且咱们在模块当中this
并非指向global
的,而是指向module.exports
,至于为何会这样呢?下面会讲到。
在红框中,咱们能够看到require
函数,exports.requireDepth
能够暂时不用管,是一个引用深度的变量,接下来咱们往下看,return mod.require(path)
,这里的mod
就是每个文件、模块,而里面都有一个require
方法,接下来咱们看看mod.require
函数内部是怎么写的。
进来后,咱们会看到2个assert
断言,用来判断path
参数是否传递了,path
是否字符串类型等等。
return Module._load(path,this,false)
,path
为咱们传入的模块路径,this则是这个模块,false则不是主要模块,主要模块的意思是,若是a.js加载了b.js,那么a.js是主要模块,而b.js则是非主要模块。
接下来咱们看看Module._load
这个静态方法
var filename = Module._resolveFilename(request, parent, isMain)
,这里的目的是解析出一个绝对路径,咱们能够进去看看Module._resolveFilename
函数是怎么写的
Module._resolveFilename
函数也没什么好说的,就是判断各类状况,而后解析出一个绝对路径出来,咱们跳出这个函数,回到Module._load
中
而后咱们看到var cachedModule = Module._cache[filename]
,这是咱们加载模块的缓存机制,就是说咱们加载过一次模块后,会缓存到Module._cache这个对象中,而且是以filename
做为键名,由于路径是惟一的,因此以路径做为惟一标识,若是已经缓存过,则会直接返回这个缓存过的模块。
NativeModule.nonInternalExists(filename)
判断是否原生模块,是的话则直接返回模块。
通过上面两个判断,基本能够断定这个模块没被加载过,那么接下来看到var module = new Module(filename, parent)
,建立了一个模块,咱们看看Module
这个构造函数有什么内容
这里的id
,实际上就是filename
惟一路径,另一个很重要的是this.exports
,也就是未来用于暴露模块的。
咱们接着往下看,在建立一个实例后,接下来把这个实例存在缓存当中,Module._cache[filename] = module
而后执行tryModuleLoad(module, filename)
,这个函数很是重要,是用来加载模块的,咱们看看是怎么写的
这里有个module.load
,咱们再往里面看看是怎么写的
兜兜转转,终于来到最核心的地方了
this.paths = Module._nodeModulePaths(path.dirname(filename))
,咱们知道,咱们安装npm包时,node会由里到外一层层找node_modules
文件夹,而这一步,则是路径一层层丢进数组里,咱们能够看看this.paths
的数组
继续往下看,var extension = path.extname(filename) || '.js'
是获取后缀名,若是没有后缀名的话,暂时默认添加一个.js
后缀名。
继续往下看,if (!Module._extensions[extension]) extension = '.js'
是判断Module._extensions
这个对象,是否有这个属性,若是没有的话,则让这个后缀名为.js
继续往下看,Module._extensions[extension](this, filename)
,根据后缀名,执行对应的函数,那么咱们看一下Module._extensions
对象有哪几个函数
从这里咱们能够看到,Module._extensions
中有3个函数,分别是.js
、.json
、.node
函数,意思是根据不一样的后缀名,执行不一样的函数,来解析不一样的内容,咱们能够留意到读取文件都是用fs.readFileSync
同步读取,由于这些文件都是保存在服务器硬盘中,读取这些文件耗费时间很是短,因此采用了同步而不是异步
其中.json
最为简单,读取出文件后,再经过JSON.parse
把字符串转化为JSON
对象,而后把结果赋值给module.exports
接下来看看.js
,也是同样先读取出文件内容,而后经过module._compile
这个函数来解析.js
的内容,咱们看一下module._compile
函数怎么写的
var wrapper = Module.wrap(content)
这里对.js
文件的内容进行了一层处理,咱们能够看看Module.wrap
怎么写的
在这里能够看出,NativeModule.wrapper
数组中有两个数组成员,是否是看起来似曾相识?没错,这就是闭包的形式,而Module.wrap
中,是直接把js文件的内容,和这个闭包拼接成一段字符串,对,就是在这里,把一个个模块,套一层闭包!实际上拼接出来的是
// 字符串
"(function(exports,require,module,__filename,__dirname){ let utils = { sayhello:function(){ console.log('hello swr') } } })"
复制代码
咱们跳出来,回到Module.prototype._compile
看看,接下来看到var compiledWrapper = vm.runInThisContext(wrapper,{...})
,在nodejs中是经过vm这个虚拟机,执行字符串,并且这样的好处是使内部彻底是封闭的,不会被外在变量污染,而在前端的字符串模板则是经过new Function()
来执行字符串,达到不被外在变量污染
继续往下看,result = compiledWrapper.call(this.exports, this.exports, require, this,filename, dirname)
,其中compiledWrapper
就是咱们经过vm虚拟机执行的字符串后返回的闭包,并且经过call
来把这个模块中的this
指向更改成当前模块,而不是全局的global
,这里就是为何咱们在模块当中打印this
时,指向的是当前的module.exports
而不是global
,而后后面依次把相应的参数传递过去
最终一层层跳出后Module._load
中,最后是return module.exports
,也就是说咱们经过require
导入的模块,取的是module.exports
.js
、.json
、.node
做为后缀,而后经过fs.existsSync
来判断文件是否存在node_modules
.js
和.json
是怎么解析的
.js
是经过拼接字符串,造成一个闭包形式的字符串.json
则是经过JSON.parse
转为JSON
对象new Function()
来执行字符串this
指向的是this.exports
而不是global
call
把指针指向了this.exports
<script>
export default {
data(){
return{
name:"邵威儒"
}
}
}
// 在这外面取里面的name值,如何取呢?
</script>
复制代码
首先,咱们知道,.vue
文件在vue当中至关于一个模块,而模块的this
是指向于exports
,那么咱们能够打印出this
看看是什么
@舞动乾坤 大佬反馈,vue-cli3中打印this是undefined,我后面看看vue-cli3怎么处理的再更新
<script>
export default {
data(){
return{
name:"邵威儒"
}
}
}
// 在这外面取里面的name值,如何取呢?
console.log(this)
</script>
复制代码
打印出来是这样的
那么就是说this.a.data
则是data
函数了, 那么咱们执行this.a.data()
,返回了{name:"邵威儒"}
因此当咱们了解这个模块化的源码后,会为咱们工做当中解决问题,提供了思路的
commonJs其实在加载模块的时候,作了如下几个步骤
.js
、.json
、.node
做为后缀,而后经过fs.existsSync
来判断文件是否存在那么咱们根据这几个步骤,来手写一下源码~
// module.js
let utils = {
sayhello: function () {
console.log('hello swr')
}
}
console.log('执行了')
module.exports = utils
复制代码
首先写出解析一个绝对路径以及如文件没添加后缀,则添加.js
、.json
做为后缀,而后经过fs.existsSync
来判断文件是否存在( .. 每一个步骤我都会标识一、二、3…
// useModule.js
// 1.引入核心模块
let fs = require('fs')
let path = require('path')
// 3.声明一个Module构造函数
function Module(id) {
this.id = id
this.exports = {} // 未来暴露模块的内容
}
// 8.支持的后缀名类型
Module._extensions = {
".js":function(){},
".json":function(){}
}
// 5.解析出绝对路径,_resolveFilename是Module的静态方法
Module._resolveFilename = function (relativePath) {
// 6.返回一个路径
let p = path.resolve(__dirname,relativePath)
// 7.该路径是否存在文件,若是存在则直接返回
// 这种状况主要考虑用户自行添加了后缀名
// 如'./module.js'
let exists = fs.existsSync(p)
if(exists) return p
// 9.若是relativePath传入的如'./module',没有添加后缀
// 那么咱们给它添加后缀,而且判断添加后缀后是否存在该文件
let keys = Object.keys(Module._extensions)
let r = false
for(let val of keys){ // 这里用for循环,是当找到文件后能够直接break跳出循环
let realPath = p + val // 拼接后缀
let exists = fs.existsSync(realPath)
if(exists){
r = realPath
break
}
}
if(!r){ // 若是找不到文件,则抛出错误
throw new Error('file not exists')
}
return r
}
// 2.为了避免与require冲突,这个函数命名为req
// 传入一个参数p 路径
function req(p) {
// 10.由于Module._resolveFilename存在找不到文件
// 找不到文件时会抛出错误,因此咱们这里捕获错误
try {
// 4.经过Module._resolveFilename解析出一个绝对路径
let filename = Module._resolveFilename(p)
} catch (e) {
console.log(e)
}
}
// 导入模块,而且导入两次,主要是校验是否加载过一次后
// 在有缓存的状况下,会不会直接返回缓存的模块
// 为此特地在module.js中添加了console.log("执行了")
// 来看打印了几回
let utils = req('./module')
utils = req('./module')
utils.sayhello()
复制代码
而后到缓存中找该模块是否被加载过,若是没有加载过则new一个模块实例,把模块存到缓存当中,最后根据后缀名,加载这个模块( .. 每一个步骤我都会标识一、二、3…
// useModule.js
// 1.引入核心模块
let fs = require('fs')
let path = require('path')
// 3.声明一个Module构造函数
function Module(id) {
this.id = id
this.exports = {} // 未来暴露模块的内容
}
// * 21.由于处理js文件时,须要包裹一个闭包,咱们写一个数组
Module.wrapper = [
"(function(exports,require,module){",
"\n})"
]
// * 22.经过Module.wrap包裹成闭包的字符串形式
Module.wrap = function(script){
return Module.wrapper[0] + script + Module.wrapper[1]
}
// 8.支持的后缀名类型
Module._extensions = {
".js":function(module){ // * 20.其次看看js是如何处理的
let str = fs.readFileSync(module.id,'utf8')
// * 23.经过Module.wrap函数把内容包裹成闭包
let fnStr = Module.wrap(str)
// * 24.引入vm虚拟机来执行字符串
let vm = require('vm')
let fn = vm.runInThisContext(fnStr)
// 让产生的fn执行,而且把this指向更改成当前的module.exports
fn.call(this.exports,this.exports,req,module)
},
".json":function(module){ // * 18.首先看看json是如何处理的
let str = fs.readFileSync(module.id,'utf8')
// * 19.经过JSON.parse处理,而且赋值给module.exports
let json = JSON.parse(str)
module.exports = json
}
}
// * 15.加载
Module.prototype._load = function(filename){
// * 16.获取后缀名
let extension = path.extname(filename)
// * 17.根据不一样后缀名 执行不一样的方法
Module._extensions[extension](this)
}
// 5.解析出绝对路径,_resolveFilename是Module的静态方法
Module._resolveFilename = function (relativePath) {
// 6.返回一个路径
let p = path.resolve(__dirname,relativePath)
// 7.该路径是否存在文件,若是存在则直接返回
// 这种状况主要考虑用户自行添加了后缀名
// 如'./module.js'
let exists = fs.existsSync(p)
if(exists) return p
// 9.若是relativePath传入的如'./module',没有添加后缀
// 那么咱们给它添加后缀,而且判断添加后缀后是否存在该文件
let keys = Object.keys(Module._extensions)
let r = false
for(let val of keys){ // 这里用for循环,是当找到文件后能够直接break跳出循环
let realPath = p + val // 拼接后缀
let exists = fs.existsSync(realPath)
if(exists){
r = realPath
break
}
}
if(!r){ // 若是找不到文件,则抛出错误
throw new Error('file not exists')
}
return r
}
// * 11.缓存对象
Module._cache = {}
// 2.为了避免与require冲突,这个函数命名为req
// 传入一个参数p 路径
function req(p) {
// 10.由于Module._resolveFilename存在找不到文件
// 找不到文件时会抛出错误,因此咱们这里捕获错误
try {
// 4.经过Module._resolveFilename解析出一个绝对路径
let filename = Module._resolveFilename(p)
// * 12.判断是否有缓存,若是有缓存的话,则直接返回缓存
if(Module._cache[filename]){
// * 由于实例的exports才是最终暴露出的内容
return Module._cache[filename].exports
}
// * 13.new一个Module实例
let module = new Module(filename)
// * 14.加载这个模块
module._load(filename)
// * 25.把module存到缓存
Module._cache[filename] = module
// * 26.返回module.exprots
return module.exports
} catch (e) {
console.log(e)
}
}
// 导入模块,而且导入两次,主要是校验是否加载过一次后
// 在有缓存的状况下,会不会直接返回缓存的模块
// 为此特地在module.js中添加了console.log("执行了")
// 来看打印了几回
let utils = req('./module')
utils = req('./module')
utils.sayhello()
复制代码
这样咱们就完成了一个简陋版的commonJs,并且咱们屡次导入这个模块,只会打印出一次执行了
,说明了只要缓存中有的,就直接返回,而不是从新加载这个模块
这里建议你们一个步骤一个步骤去理解,尝试敲一下代码,这样感悟会更加深
// 从上面源码咱们能够看出,实际上
// exports = module.exports = {}
// 可是当咱们exports = {name:"邵威儒"}时,
// require出来却获取不到这个对象,这是由于咱们在上面源码中,
// req函数(即require)内部return出的是module.exports,而不是exports,
// 当咱们exports = { name:"邵威儒" }时,实际上这个exports指向了一个新的对象,
// 而不是module.exports
// 那么咱们的exports是否是多余的呢?确定不是多余的,咱们能够这样写
exports.name = "邵威儒"
// 这样写没有改变exports的指向,而是在exports指向的module.exports对象上新增了属性
// 那么何时用exports,何时用module.exports呢?
// 若是导出的东西是一个,那么能够用module.exports,若是导出多个属性能够用exports,
// 通常状况下是用module.exports
// 还有一种方式,就是把属性挂载到global上供全局访问,不过不推荐。
复制代码