Rumah > Artikel > hujung hadapan web > Kaedah pelaksanaan pemuatan kitaran kemahiran JavaScript modules_javascript
"Kebergantungan bulat" bermaksud pelaksanaan skrip a bergantung pada skrip b, dan pelaksanaan skrip b bergantung pada skrip a.
// a.js var b = require('b'); // b.js var a = require('a');
Biasanya, "pemuatan gelung" menunjukkan kewujudan gandingan yang kuat Jika tidak dikendalikan dengan baik, ia juga boleh menyebabkan pemuatan rekursif, menjadikan program tidak dapat dilaksanakan, jadi ia harus dielakkan.
Tetapi sebenarnya, ini sukar untuk dielakkan, terutamanya untuk projek besar dengan kebergantungan yang kompleks. A mudah untuk bergantung pada b, b hingga c dan c bergantung pada a. Ini bermakna mekanisme pemuatan modul mesti mengambil kira situasi "pemuatan gelung".
Artikel ini memperkenalkan cara bahasa JavaScript mengendalikan "pemuatan gelung". Pada masa ini, dua format modul yang paling biasa, CommonJS dan ES6, mempunyai kaedah pemprosesan yang berbeza dan mengembalikan hasil yang berbeza.
1. Prinsip memuatkan modul CommonJS
Sebelum memperkenalkan cara ES6 mengendalikan "pemuatan gelung", mari kita perkenalkan prinsip pemuatan format modul CommonJS yang paling popular.
Modul CommonJS ialah fail skrip. Kali pertama arahan memerlukan memuatkan skrip, ia akan melaksanakan keseluruhan skrip dan kemudian menjana objek dalam ingatan.
{ id: '...', exports: { ... }, loaded: true, ... }
Dalam kod di atas, atribut id objek ialah nama modul, atribut eksport ialah setiap output antara muka oleh modul dan atribut yang dimuatkan ialah nilai Boolean, menunjukkan sama ada skrip modul telah dilaksanakan. Terdapat banyak atribut lain, tetapi ia ditinggalkan di sini. (Untuk pengenalan terperinci, sila rujuk "memerlukan() Tafsiran Kod Sumber".)
Apabila anda perlu menggunakan modul ini pada masa hadapan, anda akan mendapat nilai daripada atribut eksport. Walaupun arahan memerlukan dilaksanakan semula, modul tidak akan dilaksanakan lagi, tetapi nilai akan diambil daripada cache.
2. Pemuatan gelung modul CommonJS
Ciri penting modul CommonJS ialah pelaksanaan semasa memuatkan, iaitu, semua kod skrip akan dilaksanakan apabila diperlukan. Pendekatan CommonJS ialah apabila modul "dimuatkan gelung", hanya bahagian yang dilaksanakan akan menjadi output, dan bahagian yang tidak dilaksanakan tidak akan dikeluarkan.
Mari kita lihat contoh dalam dokumentasi rasmi. Kod fail skrip a.js adalah seperti berikut.
exports.done = false; var b = require('./b.js'); console.log('在 a.js 之中,b.done = %j', b.done); exports.done = true; console.log('a.js 执行完毕');
Dalam kod di atas, skrip a.js mula-mula mengeluarkan pembolehubah yang telah selesai, dan kemudian memuatkan fail skrip lain b.js. Ambil perhatian bahawa kod a.js berhenti di sini pada masa ini, menunggu b.js menyelesaikan pelaksanaan, dan kemudian meneruskan pelaksanaan.
Lihat kod b.js sekali lagi.
exports.done = false; var a = require('./a.js'); console.log('在 b.js 之中,a.done = %j', a.done); exports.done = true; console.log('b.js 执行完毕');
Dalam kod di atas, apabila b.js dilaksanakan ke baris kedua, a.js akan dimuatkan Pada masa ini, "pemuatan gelung" berlaku. Sistem akan mendapat nilai atribut eksport objek yang sepadan dengan modul a.js Namun, kerana a.js masih belum dilaksanakan, hanya bahagian yang dilaksanakan boleh diambil daripada atribut eksport, bukan nilai akhir.
Bahagian a.js yang dilaksanakan hanya mempunyai satu baris.
eksport.selesai = palsu;
Oleh itu, untuk b.js, ia hanya memasukkan satu pembolehubah yang dilakukan daripada a.js, dan nilainya adalah palsu.
Kemudian, b.js terus melaksanakan Apabila semua pelaksanaan selesai, hak pelaksanaan dikembalikan kepada a.js. Oleh itu, a.js terus melaksanakan sehingga pelaksanaan selesai. Kami menulis skrip main.js untuk mengesahkan proses ini.
var a = require('./a.js'); var b = require('./b.js'); console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
Laksanakan main.js dan hasilnya adalah seperti berikut.
$ node main.js 在 b.js 之中,a.done = false b.js 执行完毕 在 a.js 之中,b.done = true a.js 执行完毕 在 main.js 之中, a.done=true, b.done=true
Kod di atas membuktikan dua perkara. Pertama, dalam b.js, a.js belum dilaksanakan, hanya baris pertama telah dilaksanakan. Kedua, apabila main.js dilaksanakan ke baris kedua, b.js tidak akan dilaksanakan lagi, tetapi hasil pelaksanaan cache b.js akan menjadi output, iaitu baris keempatnya.
eksport.selesai = benar;
3. Pemuatan gelung modul ES6
Mekanisme pengendalian modul ES6 adalah berbeza daripada CommonJS Apabila ia menemui import arahan pemuatan modul, ia tidak akan melaksanakan modul, tetapi hanya menjana rujukan. Tunggu sehingga anda benar-benar perlu menggunakannya, kemudian dapatkan nilai dalam modul.
Oleh itu, modul ES6 adalah rujukan dinamik, tiada masalah nilai caching, dan pembolehubah dalam modul terikat pada modul di mana ia berada. Sila lihat contoh di bawah.
// m1.js export var foo = 'bar'; setTimeout(() => foo = 'baz', 500); // m2.js import {foo} from './m1.js'; console.log(foo); setTimeout(() => console.log(foo), 500);
Dalam kod di atas, pembolehubah foo m1.js adalah sama dengan bar apabila ia mula-mula dimuatkan Selepas 500 milisaat, ia menjadi sama dengan baz sekali lagi.
Mari kita lihat sama ada m2.js boleh membaca perubahan ini dengan betul.
$ babel-node m2.js bar baz
Kod di atas menunjukkan bahawa modul ES6 tidak menyimpan cache hasil yang sedang dijalankan, tetapi secara dinamik memperoleh nilai modul yang dimuatkan, dan pembolehubah sentiasa terikat pada modul di mana ia berada.
Ini menyebabkan ES6 mengendalikan "pemuatan gelung" secara asasnya berbeza daripada CommonJS. ES6 tidak peduli sama ada "pemuatan gelung" berlaku, ia hanya menjana rujukan kepada modul yang dimuatkan Pembangun perlu memastikan bahawa nilai boleh diperolehi apabila nilai sebenarnya diperoleh.
Sila lihat contoh berikut (dipetik daripada "Meneroka ES6" oleh Dr. Axel Rauschmayer).
// a.js import {bar} from './b.js'; export function foo() { bar(); console.log('执行完毕'); } foo(); // b.js import {foo} from './a.js'; export function bar() { if (Math.random() > 0.5) { foo(); } }
按照CommonJS规范,上面的代码是没法执行的。a先加载b,然后b又加载a,这时a还没有任何执行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。
但是,ES6可以执行上面的代码。
$ babel-node a.js
执行完毕
a.js之所以能够执行,原因就在于ES6加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。
我们再来看ES6模块加载器SystemJS给出的一个例子。
// even.js import { odd } from './odd' export var counter = 0; export function even(n) { counter++; return n == 0 || odd(n - 1); } // odd.js import { even } from './even'; export function odd(n) { return n != 0 && even(n - 1); }
上面代码中,even.js里面的函数foo有一个参数n,只要不等于0,就会减去1,传入加载的odd()。odd.js也会做类似操作。
运行上面这段代码,结果如下。
$ babel-node > import * as m from './even.js'; > m.even(10); true > m.counter 6 > m.even(20) true > m.counter 17
上面代码中,参数n从10变为0的过程中,foo()一共会执行6次,所以变量counter等于6。第二次调用even()时,参数n从20变为0,foo()一共会执行11次,加上前面的6次,所以变量counter等于17。
这个例子要是改写成CommonJS,就根本无法执行,会报错。
// even.js var odd = require('./odd'); var counter = 0; exports.counter = counter; exports.even = function(n) { counter++; return n == 0 || odd(n - 1); } // odd.js var even = require('./even').even; module.exports = function(n) { return n != 0 && even(n - 1); }
上面代码中,even.js加载odd.js,而odd.js又去加载even.js,形成"循环加载"。这时,执行引擎就会输出even.js已经执行的部分(不存在任何结果),所以在odd.js之中,变量even等于null,等到后面调用even(n-1)就会报错。
$ node > var m = require('./even'); > m.even(10) TypeError: even is not a function
[说明] 本文是我写的《ECMAScript 6入门》第20章《Module》中的一节。