javascript
es6 依赖循环_探索 JavaScript 中的依赖管理及循环依赖
我們通常會把項目中使用的第三方依賴寫在 package.json 文件里,然后使用 npm 、cnpm 或者 yarn 這些流行的依賴管理工具來幫我們管理這些依賴。但是它們是如何管理這些依賴的、它們之間有什么區別,如果出現了循環依賴應該怎么解決。
在回答上面幾個問題之前,先讓我們了解下語義化版本規則。
語義化版本
使用第三方依賴時,通常需要指定依賴的版本范圍,比如
"dependencies": {
"antd": "3.1.2",
"react": "~16.0.1",
"redux": "^3.7.2",
"lodash": "*"
}
上面的 package.json 文件表明,項目中使用的 antd 的版本號是 3.1.2,但是 3.1.1 和 3.1.2、3.0.1、2.1.1 之間有什么不同呢。語義化版本規則規定,版本格式為:主版本號.次版本號.修訂號,并且版本號的遞增規則如下:主版本號:當你做了不兼容的 API 修改
次版本號:當你做了向下兼容的功能性新增
修訂號:當你做了向下兼容的問題修正
主版本號的更新通常意味著大的修改和更新,升級主版本后可能會使你的程序報錯,因此升級主版本號需謹慎,但是這往往也會帶來更好的性能和體驗。次版本號的更新則通常意味著新增了某些特性,比如 antd 的版本從 3.1.1 升級到 3.1.2,之前的 Select 組件不支持搜索功能,升級之后支持了搜索。修訂號的更新則往往意味著進行了一些 bug 修復。因此次版本號和修訂號應該保持更新,這樣能讓你之前的代碼不會報錯還能獲取到最新的功能特性。
但是,往往我們不會指定依賴的具體版本,而是指定版本范圍,比如上面的 package.json 文件里的 react、redux 以及 lodash,這三個依賴分別使用了三個符號來表明依賴的版本范圍。語義化版本范圍規定:~:只升級修訂號
^:升級次版本號和修訂號
*:升級到最新版本
因此,上面的 package.json 文件安裝的依賴版本范圍如下:react@~16.0.1:>=react@16.0.1 && < react@16.1.0
redux@^3.7.2:>=redux@3.7.2 && < redux@4.0.0
lodash@*:lodash@latest
語義化版本規則定義了一種理想的版本號更新規則,希望所有的依賴更新都能遵循這個規則,但是往往會有許多依賴不是嚴格遵循這些規定的。因此,如何管理好這些依賴,尤其是這些依賴的版本就顯得尤為重要,否則一不小心就會陷入因依賴版本不一致導致的各種問題中。
依賴管理
在項目開發中,通常會使用 npm、yarn 或者 cnpm 來管理項目中的依賴,下面我們就來看看它們是如何幫助我們管理這些依賴的。
在項目開發中,通常會使用 npm、yarn 或者 cnpm 來管理項目中的依賴,下面我們就來看看它們是如何幫助我們管理這些依賴的。
npm
npm 發展到今天,可以說經歷過三個重大的版本變化。
npm v1
最早的 npm 版本在管理依賴時使用了一種很簡單的方式。我們稱之為嵌套模式。比如,在你的項目中有如下的依賴。
"dependencies": {
A: "1.0.0",
C: "1.0.0",
D: "1.0.0"
}
這些模塊都依賴 B 模塊,而且依賴的 B模塊的版本還不同。
A@1.0.0 -> B@1.0.0
C@1.0.1 -> B@2.0.0
D@1.0.0 -> B@1.0.0
通過執行 npm install 命令,npm v1 生成的 node_modules目錄如下:
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── D@1.0.0
└── node_modules
└── B@1.0.0
很明顯,每個模塊下面都會有一個 node_modules 目錄存放該模塊的直接依賴。模塊的依賴下面還會存在一個 node_modules 目錄來存放模塊的依賴的依賴。很明顯這種依賴管理簡單明了,但存在很大的問題,除了 node_modules 目錄長度的嵌套過深之外,還會造成相同的依賴存儲多份的問題,比如上面的 B@1.0.0 就存放了兩份,這明顯也是一種浪費。于是在 npm v3 發布后,npm 的依賴管理做出了重大的改變。
npm v3
對于同樣的上述依賴,使用 npm v3 執行 npm install 命令后生成的 node_modules 目錄如下:
node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
├── D@1.0.0
顯而易見,npm v3 使用了一種扁平的模式,把項目中使用的所有的模塊和模塊的依賴都放在了 node_modules 目錄下的頂層,遇到版本沖突的時候才會在模塊下的 node_modules 目錄下存放該模塊需要用到的依賴。之所以能這么實現是基于包搜索機制的。包搜索機制是指當你在項目中直接 require('A') 時,首先會在當前路徑下搜索 node_modules 目錄中是否存在該依賴,如果不存在則往上查找也就是繼續查找該路徑的上一層目錄下的 node_modules。正因為此,npm v3 才能把之前的嵌套結構拍平,把所有的依賴都放在項目根目錄的 node_modules,這樣就避免了 node_modules 目錄嵌套過深的問題。此外,npm v3 還會解析模塊的依賴的多個版本為一個版本,比如 A依賴 B@^1.0.1,D 依賴 B@^1.0.2,則只會有一個 B@1.0.2 的版本存在。雖然 npm v3 解決了這兩個問題,但是此時的 npm 仍然存在諸多問題,被人詬病最多的應該就是它的不確定性了。
npm v5
什么是確定性。在 JavaScript 包管理的背景下,確定性是指在給定的 package.json 和 lock 文件下始終能得到一致的 node_modules 目錄結構。簡單點說就是無論在何種環境下執行 npm install 都能得到相同的 node_modules 目錄結構。npm v5 正是為解決這個問題而產生的,npm v5 生成的 node_modules 目錄和 v3 是一致的,區別是 v5 會默認生成一個 package-lock.json 文件,來保證安裝的依賴的確定性。比如,對于如下的一個 package.json 文件
"dependencies": {
"redux": "^3.7.2"
}
對應的 package-lock.json 文件內容如下:
{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"lodash": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
},
"lodash-es": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz",
"integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc="
},
"loose-envify": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
"requires": {
"js-tokens": "3.0.2"
}
},
"redux": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
"requires": {
"lodash": "4.17.4",
"lodash-es": "4.17.4",
"loose-envify": "1.3.1",
"symbol-observable": "1.1.0"
}
},
"symbol-observable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.1.0.tgz",
"integrity": "sha512-dQoid9tqQ+uotGhuTKEY11X4xhyYePVnqGSoSm3OGKh2E8LZ6RPULp1uXTctk33IeERlrRJYoVSBglsL05F5Uw=="
}
}
}
不難看出,package-lock.json 文件里記錄了安裝的每一個依賴的確定版本,這樣在下次安裝時就能通過這個文件來安裝一樣的依賴了。
yarn
yarn 是在 2016.10.11 開源的,yarn 的出現是為了解決 npm v3 中的存在的一些問題,那時 npm v5 還沒發布。yarn 被定義為快速、安全、可靠的依賴管理。快速:全局緩存、并行下載、離線模式
安全:安裝包被執行前校驗其完整性
可靠:lockfile文件、確定性算法
yarn 生成的 node_modules 目錄結構和 npm v5 是相同的,同時默認生成一個 yarn.lock 文件。對于上面的例子,只安裝 redux 的依賴生成的 yarn.lock 文件內容如下:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
js-tokens@^3.0.0:
version "3.0.2"
resolved "http://registry.npm.alibaba-inc.com/js-tokens/download/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
lodash-es@^4.2.1:
version "4.17.4"
resolved "http://registry.npm.alibaba-inc.com/lodash-es/download/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"
lodash@^4.2.1:
version "4.17.4"
resolved "http://registry.npm.alibaba-inc.com/lodash/download/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
loose-envify@^1.1.0:
version "1.3.1"
resolved "http://registry.npm.alibaba-inc.com/loose-envify/download/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
dependencies:
js-tokens "^3.0.0"
redux@^3.7.2:
version "3.7.2"
resolved "http://registry.npm.alibaba-inc.com/redux/download/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
dependencies:
lodash "^4.2.1"
lodash-es "^4.2.1"
loose-envify "^1.1.0"
symbol-observable "^1.0.3"
symbol-observable@^1.0.3:
version "1.1.0"
resolved "http://registry.npm.alibaba-inc.com/symbol-observable/download/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32"
不難看出,yarn.lock 文件和 npm v5 生成的 package-lock.json 文件有如下幾點不同:文件格式不同,npm v5 使用的是 json 格式,yarn 使用的是一種自定義格式
package-lock.json 文件里記錄的依賴的版本都是確定的,不會出現語義化版本范圍符號(~ ^ *),而 yarn.lock 文件里仍然會出現語義化版本范圍符號
package-lock.json 文件內容更豐富,npm v5 只需要 package.lock 文件就可以確定 node_modules 目錄結構,而 yarn 卻需要同時依賴 package.json 和 yarn.lock 兩個文件才能確定 node_modules 目錄結構
關于為什么會有這些不同、yarn 的確定性算法以及和 npm v5 的區別,yarn 官方的一篇文章詳細介紹了這幾點。由于篇幅有限,這里就不再贅述,感興趣的可以移步到我的翻譯文章 Yarn 確定性去看。
yarn 的出現除了帶來安裝速度的提升以外,最大的貢獻是通過 lock 文件來保證安裝依賴的確定性,保證相同的 package.json 文件,在何種環境何種機器上安裝依賴都會得到相同的結果也就是相同的 node_modules 目錄結構。這在很大程度上避免了一些“在我電腦上是正常的,在其他機器上失敗”的 bug。但是在使用 yarn 做依賴管理時,仍然需要注意以下3點。不要手動修改 yarn.lock 文件
yarn.lock 文件應該提交到版本控制的倉庫里
升級依賴時,使用yarn upgrade命令,避免手動修改 package.json 和 yarn.lock 文件。
cnpm
cnpm 在國內的用戶應該還是蠻多的,尤其是對于有搭建私有倉庫需求的人來說。cnpm 在安裝依賴時使用的是 npminstall,簡單來說, cnpm 使用鏈接 link 的安裝方式,最大限度地提高了安裝速度,生成的 node_modules 目錄采用的是和 npm 不一樣的布局。 用 cnpm 裝的包都是在 node_modules 文件夾下以 版本號 @包名 命名,然后再做軟鏈接到只以包名命名的文件夾上。同樣的例子,使用 cnpm 只安裝 redux 依賴時生成的 node_modules 目錄結構如下:
cnpm 和 npm 以及 yarn 之間最大的區別就在于生成的 node_modules 目錄結構不同,這在某些場景下可能會引發一些問題。此外也不會生成 lock 文件,這就導致在安裝確定性方面會比 npm 和 yarn 稍遜一籌。但是 cnpm 使用的 link 安裝方式還是很好的,既節省了磁盤空間,也保持了 node_modules 的目錄結構清晰,可以說是在嵌套模式和扁平模式之間找到了一個平衡。
npm、yarn 和 cnpm 均提供了很好的依賴管理來幫助我們管理項目中使用到的各種依賴以及版本,但是如果依賴出現了循環調用也就是循環依賴應該怎么解決呢?
循環依賴
循環依賴指的是,a 模塊的執行依賴 b 模塊,而 b 模塊的執行又依賴 a 模塊。循環依賴可能導致遞歸加載,處理不好的話可能使得程序無法執行。探討循環依賴之前,先讓我們了解一下 JavaScript 中的模塊規范。因為,不同的規范在處理循環依賴時的做法是不同的。
目前,通行的 JavaScript 規范可以分為三種,CommonJS 、 AMD 和 ES6。
模塊規范
CommonJS
從2009年 node.js 出現以來,CommonJS 模塊系統逐漸深入人心。CommonJS 的一個模塊就是一個腳本文件,通過 require 命令來加載這個模塊,并使用模塊暴漏出的接口。加載時執行是 CommonJS 模塊的重要特性,即腳本代碼在 require 的時候就會執行模塊中的代碼。這個特性在服務端是沒問題的,但如果引入一個模塊就要等待它執行完才能執行后面的代碼,這在瀏覽器端就會有很大的問題了。因此出現了 AMD 規范,以支持瀏覽器環境。
AMD
AMD 是 “Asynchronous Module Definition” 的縮寫,意思就是“異步模塊定義”。它采用異步加載方式加載模塊,模塊的加載不影響它后面語句的運行。所有依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成之后,這個回調函數才會運行。最有代表性的實現則是 requirejs。
ES6
不同于 CommonJS 和 AMD 的模塊加載方案,ES6 在 JavaScript 語言層面上實現了模塊功能。它的設計思想是,盡量的靜態化,使得編譯時就能確定模塊的依賴關系。在遇到模塊加載命令 import 時,不會去執行模塊,而是只生成一個引用。等到真的需要用到時,再到模塊里面去取值。這是和 CommonJS 模塊規范的最大不同。
CommonJS 中循環依賴的解法
請看下面的例子:
a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
b.js
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
main.js
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done=%j, b.done=%j', a.done, b.done);
在這個例子中,a 模塊調用 b 模塊,b 模塊又需要調用 a 模塊,這就使得 a 和 b 之間形成了循環依賴,但是當我們執行 node main.js 時代碼卻沒有陷入無限循環調用當中,而是輸出了如下內容:
$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done=true, b.done=true
為什么程序沒有報錯,而是輸出如上的內容呢?這是因為 CommonJs 模塊的兩個特性。第一,加載時執行;第二,已加載的模塊會進行緩存,不會重復加載。下面讓我們分析下程序的執行過程:main.js 執行,輸出 main starting
main.js 加載 a.js,執行 a.js 并輸出 a starting,導出 done = false
a.js 加載 b.js,執行 b.js 并輸出 b starting,導出 done = false
b.js 加載 a.js,由于之前 a.js 已加載過一次因此不會重復加載,緩存中 a.js 導出的 done = false,因此,b.js 輸出 in b, a.done = false
b.js 導出 done = true,并輸出 b done
b.js 執行完畢,執行權交回給 a.js,執行 a.js,并輸出 in a, b.done = true
a.js 導出 done = true,并輸出 a done
a.js 執行完畢,執行權交回給 main.js,main.js 加載 b.js,由于之前 b.js 已加載過一次,不會重復執行
main.js 輸出 in main, a.done=true, b.done=true
從上面的執行過程中,我們可以看到,在 CommonJS 規范中,當遇到 require() 語句時,會執行 require 模塊中的代碼,并緩存執行的結果,當下次再次加載時不會重復執行,而是直接取緩存的結果。正因為此,出現循環依賴時才不會出現無限循環調用的情況。雖然這種模塊加載機制可以避免出現循環依賴時報錯的情況,但稍不注意就很可能使得代碼并不是像我們想象的那樣去執行。因此在寫代碼時還是需要仔細的規劃,以保證循環模塊的依賴能正確工作(官方原文:Careful planning is required to allow cyclic module dependencies to work correctly within an application)。
除了仔細的規劃還有什么辦法可以避免出現循環依賴嗎?一個不太優雅的方法是在循環依賴的每個模塊中先寫 exports 語句,再寫 require 語句,利用 CommonJS 的緩存機制,在 require() 其他模塊之前先把自身要導出的內容導出,這樣就能保證其他模塊在使用時可以取到正確的值。比如:
A.js
exports.done = true;
let B = require('./B');
console.log(B.done)
B.js
exports.done = true;
let A = require('./A');
console.log(A.done)
這種寫法簡單明了,缺點是要改變每個模塊的寫法,而且大部分同學都習慣了在文件開頭先寫 require 語句。
個人經驗來看,在寫代碼中只要我們注意一下循環依賴的問題就可以了,大部分同學在寫 node.js 中應該很少碰到需要手動去處理循環依賴的問題,更甚的是很可能大部分同學都沒想過這個問題。
ES6 中循環依賴的解法
要想知道 ES6 中循環依賴的解法就必須先了解 ES6 的模塊加載機制。我們都知道 ES6 使用 export命令來規定模塊的對外接口,使用 import 命令來加載模塊。那么在遇到 import 和 export 時發生了什么呢?ES6 的模塊加載機制可以概括為四個字一靜一動。一靜:import 靜態執行
一動:export 動態綁定
import 靜態執行是指,import 命令會被 JavaScript 引擎靜態分析,優先于模塊內的其他內容執行。
export 動態綁定是指,export 命令輸出的接口,與其對應的值是動態綁定關系,通過該接口可以實時取到模塊內部的值。
讓我們看下面一個例子:
foo.js
console.log('foo is running');
import {bar} from './bar'
console.log('bar = %j', bar);
setTimeout(() => console.log('bar = %j after 500 ms', bar), 500);
console.log('foo is finished');
bar.js
console.log('bar is running');
export let bar = false;
setTimeout(() => bar = true, 500);
console.log('bar is finished');
執行 node foo.js 時會輸出如下內容:
bar is running
bar is finished
foo is running
bar = false
foo is finished
bar = true after 500 ms
是不是和你想的不一樣呢?當我們執行 node foo.js 時第一行輸出的不是 foo.js 的第一個 console 語句,而是先輸出了 bar.js 里的 console 語句。這就是因為 import 命令是在編譯階段執行,在代碼運行之前先被 JavaScript 引擎靜態分析,所以優先于 foo.js 自身內容執行。同時我們也看到 500 毫秒之后也可以取到 bar 更新后的值也說明了 export 命令輸出的接口與其對應的值是動態綁定關系。這樣的設計使得程序在編譯時就能確定模塊的依賴關系,這是和 CommonJS 模塊規范的最大不同。還有一點需要注意的是,由于 import 是靜態執行,所以 import 具有提升效果即 import 命令的位置并不影響程序的輸出。
在我們了解了 ES6 的模塊加載機制之后來讓我們來看一下 ES6 是怎么處理循環依賴的。修改一下上面的例子:
foo.js
console.log('foo is running');
import {bar} from './bar'
console.log('bar = %j', bar);
setTimeout(() => console.log('bar = %j after 500 ms', bar), 500);
export let foo = false;
console.log('foo is finished');
bar.js
console.log('bar is running');
import {foo} from './foo';
console.log('foo = %j', foo)
export let bar = false;
setTimeout(() => bar = true, 500);
console.log('bar is finished');
執行 node foo.js 時會輸出如下內容:
bar is running
foo = undefined
bar is finished
foo is running
bar = false
foo is finished
bar = true after 500 ms
foo.js 和 bar.js 形成了循環依賴,但是程序卻沒有因陷入循環調用報錯而是執行正常,這是為什么呢?還是因為 import 是在編譯階段執行的,這樣就使得程序在編譯時就能確定模塊的依賴關系,一旦發現循環依賴,ES6 本身就不會再去執行依賴的那個模塊了,所以程序可以正常結束。這也說明了 ES6 本身就支持循環依賴,保證程序不會因為循環依賴陷入無限調用。雖然如此,但是我們仍然要盡量避免程序中出現循環依賴,因為可能會發生一些讓你迷惑的情況。注意到上面的輸出,在 bar.js 中輸出的 foo = undefined,如果沒注意到循環依賴會讓你覺得明明在 foo.js 中 export foo = false,為什么在 bar.js 中卻是 undefined 呢,這就是循環依賴帶來的困惑。在一些復雜大型項目中,你是很難用肉眼發現循環依賴的,而這會給排查異常帶來極大的困難。對于使用 webpack 進行項目構建的項目,推薦使用 webpack 插件 circular-dependency-plugin 來幫助你檢測項目中存在的所有循環依賴,盡早發現潛在的循環依賴可能會免去未來很大的麻煩。
小結
講了那么多,希望此文能幫助你更好的了解 JavaScript 中的依賴管理,并且處理好項目中的循環依賴問題。
總結
以上是生活随笔為你收集整理的es6 依赖循环_探索 JavaScript 中的依赖管理及循环依赖的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: esp8266 lcd 天气_8266W
- 下一篇: mongodb数据库扩展名_MongoD