В чем именно разница между модулем CommonJS и ES6?

Node.js JavaScript
В чем именно разница между модулем CommonJS и ES6?

Как разработчик внешнего интерфейса, вы когда-нибудь задумывались, почему вы можете использовать его прямо в своем коде?requireМетод загружает модуль, почему Node знает, какой файл выбрать в качестве записи при загрузке стороннего пакета, и часто спрашивают, почему модуль ES6 имеет эффект [тип ссылки] при экспорте базовых типов данных?

С этими вопросами и любопытством, я надеюсь, что чтение этой статьи может развеять ваши сомнения.

Спецификация CommonJS

До ES6 ECMAScript не предоставлял возможности организации кода. В то время он обычно основывался на IIFE для достижения «модульности». С другой стороны, исходная спецификация модуля на стороне браузера не подходила для крупномасштабных приложений. Так раноСпецификация CommonJS, целью которого является предоставление общего способа организации модулей для определения модулей.

Определение и использование модуля

В Commonjs файл — это модуль. Определите модуль для экспорта черезexportsилиmodule.exportsПросто смонтируйте его.

exports.count = 1;

Импорт модуля также прост,requireПолучить соответствующий модульexportsобъект.

const counter = require('./counter');
console.log(counter.count);

CommonJSМодули в основном состоят из нативных модулейmoduleДля этого некоторые свойства этого класса очень полезны для понимания механизма модуля.

Module {
  id: '.', // 如果是 mainModule id 固定为 '.',如果不是则为模块绝对路径
  exports: {}, // 模块最终 exports
  filename: '/absolute/path/to/entry.js', // 当前模块的绝对路径
  loaded: false, // 模块是否已加载完毕
  children: [], // 被该模块引用的模块
  parent: '', // 第一个引用该模块的模块
  paths: [ // 模块的搜索路径
   '/absolute/path/to/node_modules',
   '/absolute/path/node_modules',
   '/absolute/node_modules',
   '/node_modules'
  ]
}

Требуется прийти от?

При написании модулей CommonJS мы используемrequireчтобы загрузить модуль, используйтеexportsсделать вывод модуля, а такжеmodule,__filename, __dirnameЭти переменные, почему их не нужно вводить?

vm.runInThisContextпреобразовать строку вFunctionСформируйте область, чтобы избежать глобального загрязнения.

let wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};

const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

Поэтому в модуле CommonJS вы можете напрямую обращаться к этим методам и переменным, не требуя их.

в параметреmoduleтекущего модуляmoduleПример (хотя код модуля на данный момент не скомпилирован и не выполнен),exportsдаmodule.exportsпсевдонимы, которые в итогеrequireкогда выходmodule.exportsценность .requireПоследний звонок такжеModule._loadметод.__filename,__dirnameЭто абсолютный путь и путь к текущей папке текущего модуля в системе соответственно.

процесс поиска модуля

Разработчикам очень просто использовать require, но на самом деле для того, чтобы учесть разные способы написания, разные типы модулей,node_modulesПроцесс поиска таких модулей, как пакеты, немного громоздкий.

Во-первых, при создании объекта модуля будет свойство paths, значение которого вычисляется из текущего пути к файлу, из текущего каталога в корневой каталог системы.node_modules. можно распечатать в модулеmodule.pathsпосмотри.

[ 
  '/Users/evan/Desktop/demo/node_modules',
  '/Users/evan/Desktop/node_modules',
  '/Users/evan/node_modules',
  '/Users/node_modules',
  '/node_modules'
]

В дополнение к этому также просматривается глобальный путь (если он существует)

[
  execPath/../../lib/node_modules, // 当前 node 执行文件相对路径下的 lib/node_modules
  NODE_PATH, // 全局变量 NODE_PATH
  HOME/.node_modules, // HOME 目录下的 .node_module
  HOME/.node_libraries' // HOME 目录下的 .node-libraries
]

Согласно официальной документацииНайти процессЭто уже достаточно подробно, здесь дан только общий процесс.

从 Y 路径运行 require(X)

1. 如果 X 是内置模块(比如 require('http'))
  a. 返回该模块。
  b. 不再继续执行。

2. 如果 X 是以 '/' 开头、
   a. 设置 Y 为 '/'

3. 如果 X 是以 './' 或 '/' 或 '../' 开头
   a. 依次尝试加载文件,如果找到则不再执行
      - (Y + X)
      - (Y + X).js
      - (Y + X).json
      - (Y + X).node
   b. 依次尝试加载目录,如果找到则不再执行
      - (Y + X + package.json 中的 main 字段).js
      - (Y + X + package.json 中的 main 字段).json
      - (Y + X + package.json 中的 main 字段).node
  c. 抛出 "not found"
4. 遍历 module paths 查找,如果找到则不再执行
5. 抛出 "not found"

Процесс поиска модуля заменит программную ссылку реальным путем в системе, например.lib/foo/node_moduels/barмягкая ссылка наlib/bar,barв упаковкеrequire('quux'), и, наконец, запуститьfooмодуль,require('quux')Путь поискаlib/bar/node_moduels/quuxвместоlib/foo/node_moduels/quux.

Связано с загрузкой модуля

MainModule

при бегеnode index.js, Node вызывает статический метод класса Module_load(process.argv[1])Загрузите этот модуль, отметьте его как основной модуль и назначьте егоprocess.mainModuleа такжеrequire.main, вы можете использовать эти два поля, чтобы определить, является ли текущий модуль основным илиrequireВойдите.

CommonJSНормой является загрузка модулей синхронно и блокно во время выполнения кода, встречаяrequire(X)Он остановится и будет ждать загрузки нового модуля, прежде чем продолжить выполнение следующего кода.

Несмотря на то, что он является синхронным обструктивным, этот шаг на самом деле очень быстрый, и браузер блокируется, анализируется, выполняется.jsФайлы — это не класс, и чтение файлов с жесткого диска происходит намного быстрее, чем сетевые запросы.

Кэширование и циклические ссылки

Поиск файлового модуля занимает много времени. Если вам нужно каждый раз заново проходить поиск по папке, производительность будет низкой, а в фактической разработке модуль может содержатьпобочный эффекткод, например, выполняемый на верхнем уровне модуляaddEventListener, могут возникнуть проблемы, если процесс требуется, повторяется несколько раз.

CommonJSКэширование может решить проблему повторного поиска и повторного выполнения. В процессе загрузки модуля абсолютный путь модуля будет использоваться какkey, moduleобъектvalueзаписыватьcache. При чтении модуля приоритет будет определять, находится ли он уже в кеше, если да, возвращаться напрямуюmodule.exports; Если нет, то войдет в процесс поиска модуля, а потом напишет после нахождения модуляcache.

// a.js
module.exports = {
    foo: 1,
};

// main.js
const a1 = require('./a.js');
a1.foo = 2;

const a2 = require('./a.js');

console.log(a2.foo); // 2
console.log(a1 === a2); // true

В приведенном выше примере,require a.jsи изменитьfooсвойства, затем сноваrequire a.jsможно увидеть дваждыrequireРезультат тот же.

Кэш модуля можно распечататьrequire.cacheсмотреть.

{ 
    '/Users/evan/Desktop/demo/main.js': 
       Module {
         id: '.',
         exports: {},
         parent: null,
         filename: '/Users/evan/Desktop/demo/main.js',
         loaded: false,
         children: [ [Object] ],
         paths: 
          [ '/Users/evan/Desktop/demo/node_modules',
            '/Users/evan/Desktop/node_modules',
            '/Users/evan/node_modules',
            '/Users/node_modules',
            '/node_modules'
          ]
       },
  '/Users/evan/Desktop/demo/a.js': 
       Module {
         id: '/Users/evan/Desktop/demo/a.js',
         exports: { foo: 1 },
         parent: 
          Module {
            id: '.',
            exports: {},
            parent: null,
            filename: '/Users/evan/Desktop/demo/main.js',
            loaded: false,
            children: [Array],
            paths: [Array] },
         filename: '/Users/evan/Desktop/demo/a.js',
         loaded: true,
         children: [],
         paths: 
          [ '/Users/evan/Desktop/demo/node_modules',
            '/Users/evan/Desktop/node_modules',
            '/Users/evan/node_modules',
            '/Users/node_modules',
            '/node_modules' ] } }

Кэширование также решает проблему циклических ссылок. Например, для модуля а теперь требуется модуль b, а для модуля b требуется модуль а.

// main.js
const a = require('./a');
console.log('in main, a.a1 = %j, a.a2 = %j', a.a1, a.a2);

// a.js
exports.a1 = true;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.a2 = true;

// b.js
const a = require('./a.js');
console.log('in b, a.a1 = %j, a.a2 = %j', a.a1, a.a2);

Результат выполнения программы следующий:

in b, a.a1 = true, a.a2 = undefined
in main, a.a1 = true, a.a2 = true

На самом деле экземпляр Module был создан и записан в кеш до выполнения кода модуля а. В это время код еще не выполнен, а экспортыпустой объект.

'/Users/evan/Desktop/module/a.js': 
   Module {
     exports: {},
     //...
  }
}

кодexports.a1 = true;отредактированоmodule.exportsВверхa1дляtrue, В этот моментa2Код еще не выполнен.

'/Users/evan/Desktop/module/a.js': 
   Module {
     exports: {
      a1: true
    }
     //...
  }
}

Войтиbмодуль,require a.jsКогда обнаруживается, что он уже существует в кэше, получаемaна модулеexports. Распечататьa1, a2соответственноtrue,а такжеundefined.

закончитьсяbмодуль, продолжитьaоставшийся код модуля,exports.a2 = true;еще разexportsобъект добавленa2свойства, в настоящее времяmodule aизexportобъектa1, a2обеtrue.

exports: { 
  a1: true,
  a2: true
}

назад сноваmainмодуль, из-заrequire('./a.js')получить этоmodule a exportСсылка на объект, на этот раз печатаетa1, a2все дляtrue.

резюме:

CommonJSПроцесс загрузки модуля представляет собой синхронную загрузку с блокировкой, которая записывается до запуска кода модуля.cache, один и тот же модуль вызывается несколько разrequireбудет выполняться только один раз, повторяетсяrequireполучить то же самоеexportsЦитировать.

Стоит отметить: cache keyИспользуется абсолютное положение модуля в системе, т.к.Разница в том, где вызывается модуль, идентичныйrequire('foo')Код не гарантирует, что будет возвращена единственная ссылка на объект. Я случайно сталкивался с этим раньше,require('яйцо-ядро') дважды, но они не равны.

Модули ES6

ES6Модули — более привычный способ для студентов, изучающих фронтенд-разработку.import, exportКлючевые слова для ввода и вывода модуля.ES6Вместо использования замыканий и инкапсуляции функций для модуляризации модульность обеспечивается на синтаксическом уровне.

ES6Модуль не существуетrequire, module.exports, __filenameи другие переменные,CommonJSтакже нельзя использовать вimport. Две спецификации несовместимы, обычно пишутся в будние дни.ES6Код модуля в конечном итоге пройдетBabel, Typescriptинструменты перерабатываются вCommonJSкод.

использоватьNodeРоднойES6модуль долженjsСуффикс файла изменен наmjs,илиpackage.jsonПоле «тип» изменено на «модуль», что сообщаетNodeиспользоватьES ModuleЗагрузите модуль в виде .

Процесс загрузки модуля ES6

Процесс загрузки модулей ES6 делится на три этапа:

1. Найдите, загрузите, проанализируйте и соберите все экземпляры модуля.

Модули ES6 найдут все модули в соответствии с отношением модулей до запуска программы, сгенерируют ациклический граф отношений и создадут все экземпляры модуля.Этот метод, естественно, позволяет избежать проблемы циклических ссылок.Конечно, существуют также кеши загрузки модулей.Если вы повторно импортировать один и тот же модуль, код будет выполнен только один раз.

2. Освободите место в памяти для экспортируемого содержимого (значение экспорта еще не записано). Затем сделайте так, чтобы импорт и экспорт указывали на эти пространства в памяти, этот процесс также называется связыванием.

Работа, проделанная на этом шаге, равнаliving binding import export, с помощью следующих примеров, чтобы помочь понять.

// counter.js
let count = 1;

function increment () {
  count++;
}

module.exports = {
  count,
  increment
}

// main.js
const counter = require('counter.cjs');

counter.increment();
console.log(counter.count); // 1

надCommonJSРезультаты выполнения примера хорошо понятны, модифицируйтеcount++Модификация — это базовая переменная типа данных в модуле, которая не изменится.exports.count, поэтому результат печати считает, что1.

// counter.mjs
export let count = 1;

export function increment () {
  count++;
}

// main.mjs
import { increment, count } from './counter.mjs'

increment();
console.log(count); // 2

Используйте результатыES6написание модуля, когдаexportКогда переменная изменяется, это повлияетimportрезультат. Реализация этой функции естьliving binding, вы можете временно игнорировать то, как конкретная спецификация реализована внизу, но знайтеliving bindingЛучше понять, чем то, что онлайн-статья описывает как «модули ES6 выводят ссылки на значения».

БлижеES6модульныйCommonJSКод может быть следующим:

exports.counter = 1;

exports.increment = function () {
    exports.counter++;
}

3. Запустите код модуля, чтобы заполнить фактическое значение переменной в пространстве, сгенерированном на втором шаге.

На третьем шаге будет заполнено значение обхода в глубину на основе ациклического графа, сгенерированного на первом шаге, если процесс еще не посетил пространство, инициализация завершена, он выдаст исключение.

// a.mjs
export const a1 = true;
import * as b from './b.mjs';
export const a2 = true;

// b.mjs
import { a1, a2 } from './a.mjs'
console.log(a1, a2);

Приведенный выше пример вызовет исключение во время выполнения.ReferenceError: Cannot access 'a1' before initialization. Если изменить наimport * as a from 'a.mjs'можно увидетьaмодульexport

// b.mjs
import * as a from './a.mjs'
console.log(a);

Выход{ a1: <uninitialized>, a2: <uninitialized> }Видно, что модуль ES6 зарезервировал место для экспортируемой переменной, но еще не присвоил ей значение. здесь иCommonJSРазные,CommonJSвот знатьa1дляtrue, a2дляundefined

В дополнение к этому мы также можем получить некоторые модули ES6 иCommonJSТочка разницы:

  • CommonJSПеременные могут использоваться для запроса во время выполнения, например.require(path.join('xxxx', 'xxx.js')), в то время как статическийimportграмматика (и动态 import,вернутьPromise) не будет работать, потому что модули ES6 разрешают все модули перед выполнением кода.

  • requireзавершитexportsимпорт объекта,importможно толькоimportЧасть необходимого контента, поэтому используйтеTree ShakingВы должны использовать нотацию модуля ES6.
  • importдругого модуля нетexportпеременная, перед выполнением кода будет сообщено об ошибке, иCommonJSОшибка сообщается, когда модуль работает.

Почему я могу смешивать и писать в обычном развитии?

упомянутый ранееES6модули иCommonJSМодули очень разные и не могут быть смешаны напрямую. Это отличается от производительности в процессе разработки, потому что модули ES6, написанные в процессе разработки, в конечном итоге будут обработаны инструментами упаковки вCommonJSмодули, чтобы быть совместимыми с большим количеством сред, а также с текущим общим сообществомCommonJSСлияние модулей.

В процессе преобразования может возникнуть некоторая путаница, например:

__esModuleчто это такое? Для чего это?

При использовании инструментов преобразования для обработки модулей ES6 вы часто видите, что они появляются после упаковки.__esModuleсвойство, буквально обозначая его какES6 Module. Эта переменная предназначена для облегчения обработки при обращении к модулям.

Например, в модулях ES6export defaultв превращении вCommonJSбудет установлен наexports['default']Горит во время работыrequire('./a.js')Когда вы не можете читать напрямуюdefaultна значение, чтобы и ES6import a from './a.js'поведение последовательное, основанное на__esModuleОбработка приговора.

// a.js
export default 1;

// main.js
import a from './a';

console.log(a);

после преобразования

// a.js
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = 1;

// main.js
'use strict';

var _a = require('./a');

var _a2 = _interopRequireDefault(_a);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

console.log(_a2.default);

aмодульexport defualtбудет преобразован вexports.default = 1;, который также обычно используется при разработке фронтенд-проектов.requireЗачем тебе еще нужно.defaultчтобы получить целевое значение.

Затем при бегеimport a from './a.js'час,es moduleОжидается возвращениеexportСодержание. Инструмент преобразует код в_interopRequireDefaultпакет, по которому можно судить о том,esModule, если да, вернуться напрямую, если даcommonjsЕсли модуль завернут в слой{default: obj}, когда значение a будет окончательно получено, оно также будет заменено на_a1.default.

Ссылки по теме