前言:你們好,我叫邵威儒,你們都喜歡喊我小邵,學的金融專業卻憑藉興趣愛好入了程序猿的坑,從大學買的第一本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上供全局訪問,不過不推薦。
複製代碼