автор:zhijsиз Thunder Front End
Оригинальный адрес:Модульный синтаксический анализ JavaScript
С постепенным развитием языка JavaScript приложения JavaScript эволюционировали от простой проверки формы до сложного взаимодействия с веб-сайтом и языковой поддержки для серверных, мобильных и ПК-клиентов. Область применения JavaScript становится все более и более обширной, инженерный код становится все более и более объемным, а управление кодом становится все более и более сложным, поэтому в сообществе появилась схема модульности JavaScript. схемы модуляризации, Постепенно он стал спецификацией языка JavaScript. Давайте обсудим тему модульности JavaScript. Эта статья в основном состоит из нескольких частей.
- Что такое модуль
- Почему модульный
- CommonJS для модульности JavaScript
- AMD модульности JavaScript
- Модульная командная строка JavaScript
- Модульный модуль JavaScript ES
- Суммировать
что такое модуль
Модуль, также известный как компонент, представляет собой набор операторов программы (то есть набор программных кодов и структур данных), которые могут быть индивидуально названы и независимо выполнять определенные функции. Он имеет две основные функции: внешние функции и внутренние функции. Внешние функции относятся к интерфейсу между модулем и внешней средой (то есть к тому, как другие модули или программы вызывают модуль, включая входные и выходные параметры, глобальные переменные, на которые ссылаются) и функции модуля, а внутренние функции относятся к особенности внутренней среды модуля (т. е. локальные данные и программный код для этого модуля). Короче говоря, модуль — это набор кода с независимой областью действия, который предоставляет внешнему миру определенный функциональный интерфейс.
Зачем нужна модульность
Сначала давайте вернемся в прошлое и посмотрим, как был написан исходный файл модуля JavaScript.
// add.js
function add(a, b) {
return a + b;
}
// decrease.js
function decrease(a, b) {
return a - b;
}
// formula.js
function square_difference(a, b) {
return add(a, b) * decrease(a, b);
}
Выше мы реализовали несколько функциональных функций в трех файлах JavaScript. Среди них третья функция function должна зависеть от функциональных функций первого и второго файлов JavaScript, поэтому, когда мы ее используем, мы обычно пишем ее так:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="add.js"></script>
<script src="decrease.js"></script>
<script src="formula.js"></script>
<!--使用-->
<script>
var result = square_difference(3, 4);
</script>
</body>
</html>
Такой вид управления вызовет следующие проблемы:
- Модули могут быть импортированы в неправильном порядке
- загрязняет глобальные переменные
- Зависимости между модулями не очевидны
Основываясь на вышеуказанных причинах, существует решение вышеуказанных проблем, то есть спецификация модульности JavaScript.В настоящее время существует четыре основных спецификации: CommonJS, AMD, CMD и модуль ES6.
CommonJS для модульности Javascript
Основное содержание спецификации CommonJS заключается в том, что один файл является модулем. Каждый модуль представляет собой отдельную область. Модуль должен экспортировать внешние переменные или интерфейсы через module.exports и импортировать вывод других модулей в текущую область модуля через require(). Давайте поговорим о модуляризации CommonJS в механизме NodeJs.
Как использовать
// 模块定义 add.js
module.eports.add = function(a, b) {
return a + b;
};
// 模块定义 decrease.js
module.exports.decrease = function(a, b) {
return a - b;
};
// formula.js,模块使用,利用 require() 方法加载模块,require 导出的即是 module.exports 的内容
const add = require("./add.js").add;
const decrease = require("./decrease.js").decrease;
module.exports.square_difference = function(a, b) {
return add(a, b) * decrease(a, b);
};
экспорт и модуль.экспорт
exports и module.exports — это переменные, указывающие на одно и то же: module.exports = exports = {}, так что вы также можете экспортировать такие модули
//add.js
exports.add = function(a, b) {
return a + b;
};
Но нельзя напрямую изменять точку экспорта, например:
// add.js
exports = function(a, b) {
return a + b;
};
// main.js
var add = require("./add.js");
Добавление, полученное в это время, является пустым объектом, потому что require импортирует содержимое module.exports соответствующего модуля.В приведенном выше коде, хотя в начале exports = module.exports, когда выполняется следующий код, на самом деле Экспорт точки в функцию, а содержимое module.exports не изменилось, поэтому экспорт этого модуля является пустым объектом.
exports = function(a, b) {
return a + b;
};
Механизм загрузки модулей CommonJS в NodeJs
Согласно следующемуМодуль CommonJS загружает исходный код в NodeJsПроанализировать механизм загрузки модулей в NodeJS.
Чтобы внедрить модуль (require) в NodeJs, вам необходимо пройти следующие 3 шага:
- анализ пути
- местоположение файла
- Скомпилировать и выполнить
Так же, как клиентские браузеры кэшируют статические файлы скриптов для повышения производительности, NodeJs кэширует импортированные модули, чтобы уменьшить нагрузку на вторичный импорт. Разница в том, что браузеры кешируют только файлы, а в NodeJ кешируются скомпилированные и исполняемые объекты.
Анализ пути + расположение файла
Его процесс показан на следующем рисунке:
компиляция модуля
После обнаружения файла он сначала проверит, есть ли у файла кеш, и если да, прочитает кеш напрямую, в противном случае он создаст новый объект модуля, который определяется следующим образом:
function Module(id, parent) {
this.id = id; // 模块的识别符,通常是带有绝对路径的模块文件名。
this.exports = {}; // 表示模块对外输出的值
this.parent = parent; // 返回一个对象,表示调用该模块的模块。
if (parent && parent.children) {
this.parent.children.push(this);
}
this.filename = null;
this.loaded = false; // 返回一个布尔值,表示模块是否已经完成加载。
this.childrent = []; // 返回一个数组,表示该模块要用到的其他模块。
}
Код действия запроса выглядит следующим образом:
Module.prototype.require = function(id) {
// 检查模块标识符
if (typeof id !== "string") {
throw new ERR_INVALID_ARG_TYPE("id", "string", id);
}
if (id === "") {
throw new ERR_INVALID_ARG_VALUE("id", id, "must be a non-empty string");
}
// 调用模块加载方法
return Module._load(id, this, /* isMain */ false);
};
Следующий шаг — разобрать путь к модулю, определить, есть ли кеш, а затем сгенерировать объект Module:
Module._load = function(request, parent, isMain) {
if (parent) {
debug("Module._load REQUEST %s parent: %s", request, parent.id);
}
// 解析文件名
var filename = Module._resolveFilename(request, parent, isMain);
var cachedModule = Module._cache[filename];
// 判断是否有缓存,有的话返回缓存对象的 exports
if (cachedModule) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
// 判断是否为原生核心模块,是的话从内存加载
if (NativeModule.nonInternalExists(filename)) {
debug("load native module %s", request);
return NativeModule.require(filename);
}
// 生成模块对象
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = ".";
}
// 缓存模块对象
Module._cache[filename] = module;
// 加载模块
tryModuleLoad(module, filename);
return module.exports;
};
Код для tryModuleLoad выглядит так:
function tryModuleLoad(module, filename) {
var threw = true;
try {
// 调用模块实例load方法
module.load(filename);
threw = false;
} finally {
if (threw) {
// 如果加载出错,则删除缓存
delete Module._cache[filename];
}
}
}
Объект модуля выполняет операцию загрузки module.load Код выглядит следующим образом:
Module.prototype.load = function(filename) {
debug("load %j for module %j", filename, this.id);
assert(!this.loaded);
this.filename = filename;
// 解析路径
this.paths = Module._nodeModulePaths(path.dirname(filename));
// 判断扩展名,并且默认为 .js 扩展
var extension = path.extname(filename) || ".js";
// 判断是否有对应格式文件的处理函数, 没有的话,扩展名改为 .js
if (!Module._extensions[extension]) extension = ".js";
// 调用相应的文件处理方法,并传入模块对象
Module._extensions[extension](this, filename);
this.loaded = true;
// 处理 ES Module
if (experimentalModules) {
if (asyncESM === undefined) lazyLoadESM();
const ESMLoader = asyncESM.ESMLoader;
const url = pathToFileURL(filename);
const urlString = `${url}`;
const exports = this.exports;
if (ESMLoader.moduleMap.has(urlString) !== true) {
ESMLoader.moduleMap.set(
urlString,
new ModuleJob(ESMLoader, url, async () => {
const ctx = createDynamicModule(["default"], url);
ctx.reflect.exports.default.set(exports);
return ctx;
})
);
} else {
const job = ESMLoader.moduleMap.get(urlString);
if (job.reflect) job.reflect.exports.default.set(exports);
}
}
};
Прочитайте модуль синхронно здесь, а затем выполните операцию компиляции:
Module._extensions[".js"] = function(module, filename) {
// 同步读取文件
var content = fs.readFileSync(filename, "utf8");
// 编译代码
module._compile(stripBOM(content), filename);
};
Процесс компиляции в основном выполняет следующие операции:
- Оберните код JavaScript в тело функции, чтобы изолировать область действия, например:
exports.add = (function(a, b) {
return a + b;
}
будет преобразован в
(
function(exports, require, modules, __filename, __dirname) {
exports.add = function(a, b) {
return a + b;
};
}
);
-
Выполните функцию, внедрите свойство экспорта объекта модуля, запросите глобальный метод и экземпляр объекта, __filename, __dirname, а затем выполните исходный код модуля.
-
Возвращает свойство экспорта объекта модуля.
AMD модульности JavaScript
AMD, определение асинхронного модуля, представляет собой механизм асинхронной загрузки модуля, который загружает модуль асинхронным способом, и загрузка модуля не влияет на работу следующего оператора. Все операторы, которые зависят от этого модуля, определены в функции обратного вызова, которая не будет выполняться до тех пор, пока не будут загружены зависимости.
AMD была создана для решения этих двух проблем:
- Асинхронная загрузка файлов JavaScript, чтобы избежать неотвечающих веб-страниц
- Управление зависимостями между модулями для облегчения написания кода и обслуживания
// 模块定义
define(id?: String, dependencies?: String[], factory: Function|Object);
id — это имя модуля и необязательный параметр.
dependencies указывает список модулей, от которых зависит.Это массив и необязательный параметр. Вывод каждого зависимого модуля будет передан на фабрику в качестве аргумента один раз. Если зависимости не указаны, по умолчанию используется ["require", "exports", "module"].
factory — последний параметр, он оборачивает конкретную реализацию модуля, это функция или объект. Если это функция, ее возвращаемое значение является выходным интерфейсом или значением модуля, а если это объект, то объект должен быть выходным значением модуля.
Например:
// 模块定义,add.js
define(function() {
let add = function(a, b) {
return a + b;
};
return add;
});
// 模块定义,decrease.js
define(function() {
let decrease = function(a, b) {
return a - b;
};
return decrease;
});
// 模块定义,square.js
define(["./add", "./decrease"], function(add, decrease) {
let square = function(a, b) {
return add(a, b) * decrease(a, b);
};
return square;
});
// 模块使用,主入口文件 main.js
require(["square"], function(math) {
console.log(square(6, 3));
});
Здесь для анализа используется RequireJS, реализующий спецификацию AMD, исходный код RequireJS более сложен, здесь анализируется только принцип асинхронной загрузки модулей. Во время загрузки модуля RequireJS вызывает следующие функции:
/**
*
* @param {Object} context the require context to find state.
* @param {String} moduleName the name of the module.
* @param {Object} url the URL to the module.
*/
req.load = function(context, moduleName, url) {
var config = (context && context.config) || {},
node;
// 判断是否为浏览器
if (isBrowser) {
// 根据模块名称和 url 创建一个 Script 标签
node = req.createNode(config, moduleName, url);
node.setAttribute("data-requirecontext", context.contextName);
node.setAttribute("data-requiremodule", moduleName);
// 对不同的浏览器 Script 标签事件监听做兼容处理
if (
node.attachEvent &&
!(
node.attachEvent.toString &&
node.attachEvent.toString().indexOf("[native code") < 0
) &&
!isOpera
) {
useInteractive = true;
node.attachEvent("onreadystatechange", context.onScriptLoad);
} else {
node.addEventListener("load", context.onScriptLoad, false);
node.addEventListener("error", context.onScriptError, false);
}
// 设置 Script 标签的 src 属性为模块路径
node.src = url;
if (config.onNodeCreated) {
config.onNodeCreated(node, config, moduleName, url);
}
currentlyAddingScript = node;
// 将 Script 标签插入到页面中
if (baseElement) {
head.insertBefore(node, baseElement);
} else {
head.appendChild(node);
}
currentlyAddingScript = null;
return node;
} else if (isWebWorker) {
try {
//In a web worker, use importScripts. This is not a very
//efficient use of importScripts, importScripts will block until
//its script is downloaded and evaluated. However, if web workers
//are in play, the expectation is that a build has been done so
//that only one script needs to be loaded anyway. This may need
//to be reevaluated if other use cases become common.
// Post a task to the event loop to work around a bug in WebKit
// where the worker gets garbage-collected after calling
// importScripts(): https://webkit.org/b/153317
setTimeout(function() {}, 0);
importScripts(url);
//Account for anonymous modules
context.completeLoad(moduleName);
} catch (e) {
context.onError(
makeError(
"importscripts",
"importScripts failed for " + moduleName + " at " + url,
e,
[moduleName]
)
);
}
}
};
// 创建异步 Script 标签
req.createNode = function(config, moduleName, url) {
var node = config.xhtml
? document.createElementNS("http://www.w3.org/1999/xhtml", "html:script")
: document.createElement("script");
node.type = config.scriptType || "text/javascript";
node.charset = "utf-8";
node.async = true;
return node;
};
Видно, что асинхронный тег Script создается в основном на основе URL-адреса модуля, а имя идентификатора модуля добавляется в data-requiremodule тега, а затем тег Script добавляется на html-страницу. При этом добавлен обработчик события load тега Script, при загрузке файла модуля будет срабатывать context.onScriptLoad. Мы добавляем точку останова в onScriptLoad, вы можете увидеть структуру страницы, как показано ниже:
Как видно из рисунка, в Html добавлен тег Script, что является принципом асинхронной загрузки модулей.
Модульная командная строка JavaScript
CMD (Common Module Definition) — это общее определение модуля. Реализация CMD на стороне браузера — это SeaJS. Как и RequireJS, принцип загрузки SeaJS заключается в динамическом создании асинхронных тегов Script. Разница между ними в основном заключается в способе написания зависимостей: AMD рекомендует загружать все зависимости с самого начала, а CMD рекомендует загружать зависимости только там, где они необходимы.
// ADM 在执行以下代码的时候,RequireJS 会首先分析依赖数组,然后依次加载,直到所有加载完毕再执行回到函数
define(["add", "decrease"], function(add, decrease) {
let result1 = add(9, 7);
let result2 = decrease(9, 7);
console.log(result1 * result2);
});
// CMD 在执行以下代码的时候, SeaJS 会首先用正则匹配出代码里面所有的 require 语句,拿到依赖,然后依次加载,加载完成再执行回调函数
define(function(require) {
let add = require("add");
let result1 = add(9, 7);
let add = require("decrease");
let result2 = decrease(9, 7);
console.log(result1 * result2);
});
Модульный модуль JavaScript ES
Модуль ES — это модульная функция, представленная в ECMAScript 6. Функция модуля в основном состоит из двух команд: экспорта и импорта. Команда экспорта используется для указания внешнего интерфейса модуля, а команда импорта используется для импорта функций, предоставляемых другими модулями.
Он используется следующим образом:
// 模块定义 add.js
export function add(a, b) {
return a + b;
}
// 模块使用 main.js
import { add } from "./add.js";
console.log(add(1, 2)); // 3
Ниже описаны еще несколько важных моментов.
экспортировать и экспортировать по умолчанию
В файле или модуле может быть несколько экспортов, и существует только один экспорт по умолчанию, экспорт подобен именованному экспорту, а умолчание аналогично экспорту переменной с именем по умолчанию. При этом при импорте для переменных экспорта необходимо использовать именованные объекты для их выполнения, а по умолчанию можно указывать имена переменных произвольно, например:
// a.js
export var a = 2;
export var b = 3 ;
// main.js 在导出的时候必须要用具名变量 a, b 且以解构的方式得到导出变量
import {a, b} from 'a.js' // √ a= 2, b = 3
import a from 'a.js' // x
// b.js export default 方式
const a = 3
export default a // 注意不能 export default const a = 3 ,因为这里 default 就相当于一个变量名
// 导出
import b form 'b.js' // √
import c form 'b.js' // √ 因为 b 模块导出的是 default,对于导出的default,可以用任意变量去承接
Процесс загрузки и экспорта модуля модуля ES
В качестве примера возьмем следующий код:
// counter.js
export let count = 5
// display.js
export function render() {
console.log('render')
}
// main.js
import { counter } from './counter.js';
import { render } from './display.js'
......// more code
В процессе загрузки модуля он в основном проходит следующие этапы:
Строительство
Этот процесс выполняет поиск, загрузку и преобразование файла в запись модуля. Так называемая запись модуля относится к синтаксическому дереву, в котором записан соответствующий модуль, информация о зависимостях и различные атрибуты и методы (здесь не очень понятно). Также в этом процессе кэшируются записи модуля.На следующем рисунке показана таблица записей модуля:
На следующем рисунке показана таблица записей кэша:
Создание экземпляра
Этот процесс откроет место для хранения в памяти (значение еще не заполнено в это время), а затем все переменные, которые экспортируют и импортируют модуль модуля, указывают на эту память.Этот процесс называется связыванием. Принципиальная схема его записи на экспорт выглядит следующим образом:
Затем идет импорт ссылок, принципиальная схема которого выглядит следующим образом:
Задание (Оценка)
Этот процесс выполнит код модуля и заполнит пространство памяти, открытое на предыдущем этапе, реальным значением.После этого процесса значение, связанное импортом, является реальным значением, экспортируемым экспортом.
В соответствии с описанным выше процессом мы можем знать. Экспорт и импорт модуля модуля ES фактически указывают на один и тот же фрагмент памяти, но следует отметить, что значение этой памяти не может быть изменено при импорте, а экспорт может.Схема выглядит следующим образом:
Суммировать
В этой статье в основном рассматриваются и разбираются текущие основные схемы модуляции JavaScript CommonJs, AMD, CMD, ES Module, а также проводится простой анализ наиболее репрезентативных модульных реализаций (NodeJs, RequireJS, SeaJS, ES6). Для модулей на стороне сервера, поскольку модули хранятся локально и загрузка модулей удобна, загрузка модулей обычно выполняется путем синхронного чтения файлов. Для браузера его модули обычно хранятся в удаленной сети, а загрузка модуля — очень трудоемкий процесс, поэтому для загрузки файла модуля обычно используется метод динамической асинхронной загрузки скрипта. Кроме того, модульные реализации JavaScript как на стороне клиента, так и на стороне сервера будут кэшировать модули, чтобы уменьшить нагрузку на вторичную загрузку.
Справочная статья:ES modules: A cartoon deep-dive