предисловие
Эта статья следуетОчки знаний по фронтенд-интервью (1)После второго собеседования анализ знаний. В первом анализе точек знаний интервью было дано 19 ответов из 174 вопросов интервью, и в этой статье будет продолжен анализ некоторых ответов. Поскольку длина анализа ответов становится все длиннее и длиннее, анализ ответов на этот список вопросов интервью должен быть сделан вСерия знаний о предварительном собеседовании, что может помочь учащимся, не читавшим этот цикл статей, прочитать по порядку по порядковому номеру:
- Очки знаний по фронтенд-интервью (1): Анализ ответов на 19 вопросов интервью, таких как 1 ~ 5, 7 ~ 8, 10 ~ 15, 18, 20 ~ 21, 24, 29 ~ 30 и т. д.
- Точки знаний о предварительном собеседовании (2): анализ ответов на 2 вопроса интервью, таких как 6 и 9.
6. Кратко опишите процесс компиляции Babel?
Babel — это транспилятор исходного кода, основная функция которого заключается в преобразовании синтаксиса высокой версии JavaScript (например, ES6) в синтаксис более низкой версии (например, ES5), чтобы он мог адаптироваться к совместимости с браузерами.
Напоминание: если целевой язык языка высокого уровня или прикладного языка (например, языка компьютерного дизайна для искусственного интеллекта) преобразование является не языком ассемблера конкретного компьютера, а другим языком программирования высокого уровня (многие исследовательские компиляторы будут C в качестве целевого языка), то для получения окончательной целевой программы требуется дополнительная компиляция целевого языка программирования высокого уровня, которую можно назвать преобразователем исходного кода в исходный.
Как видно из рисунка выше, процесс компиляции Babel можно разделить на три этапа:
- Разбор (Parse): включая лексический анализ и синтаксический анализ. Лексический анализ в основном преобразует исходный код потока символов (Char Stream) в поток токенов (Token Stream), а синтаксический анализ в основном преобразует поток токенов в абстрактное синтаксическое дерево (AST).
- Преобразование (преобразование): с помощью подключаемых модулей Babel преобразуйте AST синтаксиса более высокой версии в AST, поддерживающий синтаксис более низкой версии. Конечно, во время этого процесса на узлах Node AST также могут выполняться операции оптимизации, такие как добавление, обновление и удаление узлов.
- Генерировать (Generate): конвертировать AST в низкоуровневый код в строковой форме, а также создавать сопоставление Source Map.
Конкретный процесс выглядит следующим образом:
Например, если вы хотите преобразовать синтаксис TypeScript в синтаксис ES5:
// 源代码
let a: string = 1;
// 目标代码
var a = 1;
6.1 Парсер
Можно использовать процесс синтаксического анализа Babel (преобразование исходного кода в AST).@babel/parser, его основные особенности заключаются в следующем:
- Поддержка разбора последней версии ES2020
- Поддержка парсинга JSX, Flow и TypeScript
- Поддержка синтаксического анализа экспериментальных грамматических предложений (поддерживает любыеStage 0ПРС)
@babel/parser в основном основан на потоке входных строк (исходный код) для синтаксического анализа и, наконец, преобразован вТехнические характеристики(на основеESTree корректируется) следующим образом:
import { parse } from '@babel/parser';
const source = `let a: string = 1;`;
enum ParseSourceTypeEnum {
Module = 'module',
Script = 'script',
Unambiguous = 'unambiguous',
}
enum ParsePluginEnum {
Flow = 'flow',
FlowComments = 'flowComments',
TypeScript = 'typescript',
Jsx = 'jsx',
V8intrinsic = 'v8intrinsic',
}
// 解析(Parser)阶段
const ast = parse(source, {
// 严格模式下解析并且允许模块定义
sourceType: ParseSourceTypeEnum.Module,
// 支持解析 TypeScript 语法(注意,这里只是支持解析,并不是转换 TypeScript)
plugins: [ParsePluginEnum.TypeScript],
});
Следует отметить, что на этапе Parser в основном выполняется лексический и грамматический анализ, если лексический или грамматический анализ ошибочен, то это будет обнаружено на этом этапе. Если определение правильное, можно перейти к этапу преобразования грамматики.
6.2 Преобразование
Процесс преобразования Babel (преобразование AST в AST) в основном использует@babel/traverse, к пакету библиотеки можно получить доступ черезшаблон посетителяАвтоматически просматривать и получать доступ к информации о каждом узле узла дерева AST, чтобы реализовать замену, удаление и добавление узлов следующим образом:
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
enum ParseSourceTypeEnum {
Module = 'module',
Script = 'script',
Unambiguous = 'unambiguous',
}
enum ParsePluginEnum {
Flow = 'flow',
FlowComments = 'flowComments',
TypeScript = 'typescript',
Jsx = 'jsx',
V8intrinsic = 'v8intrinsic',
}
const source = `let a: string = 1;`;
// 解析(Parser)阶段
const ast = parse(source, {
// 严格模式下解析并且允许模块定义
sourceType: ParseSourceTypeEnum.Module,
// 支持解析 TypeScript 语法(注意,这里只是可以解析,并不是转换 TypeScript)
plugins: [ParsePluginEnum.TypeScript],
});
// 转换(Transform) 阶段
traverse(ast, {
// 访问变量声明标识符
VariableDeclaration(path) {
// 将 const 和 let 转换为 var
path.node.kind = 'var';
},
// 访问 TypeScript 类型声明标识符
TSTypeAnnotation(path) {
// 移除 TypeScript 的声明类型
path.remove();
},
});
Что касается API доступа в Babel, я не буду здесь слишком много объяснять, если вы хотите узнать больше, вы можете проверитьРуководство по плагину Babel.除此之外,你可能已经注意到这里的转换逻辑其实可以理解为实现一个简单的 Babel 插件,只是没有封装成 Npm 包。当然,在真正的插件开发开发中,还可以配合@babel/typesИнструментарий выполняет оценку и обработку информации об узле.
Напоминание: Это всего лишь простой демонстрационный пример. В процессе фактического преобразования объявлений переменных, таких как let и const, также будут ситуации, в которых обрабатывается временная мертвая зона (TDZ). Для получения более подробной информации обратитесь к официальному веб-сайту. .плагинbabel-plugin-transform-block-scoping.
6.3 Сгенерировать (Сгенерировать)
Процесс генерации кода Babel (преобразование AST в объектный код) в основном использует@babel/generator,Следующим образом:
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
enum ParseSourceTypeEnum {
Module = 'module',
Script = 'script',
Unambiguous = 'unambiguous',
}
enum ParsePluginEnum {
Flow = 'flow',
FlowComments = 'flowComments',
TypeScript = 'typescript',
Jsx = 'jsx',
V8intrinsic = 'v8intrinsic',
}
const source = `let a: string = 1;`;
// 解析(Parser)阶段
const ast = parse(source, {
// 严格模式下解析并且允许模块定义
sourceType: ParseSourceTypeEnum.Module,
// 支持解析 TypeScript 语法(注意,这里只是可以解析,并不是转换 TypeScript)
plugins: [ParsePluginEnum.TypeScript],
});
// 转换(Transform) 阶段
traverse(ast, {
// 访问词法规则
VariableDeclaration(path) {
path.node.kind = 'var';
},
// 访问词法规则
TSTypeAnnotation(path) {
// 移除 TypeScript 的声明类型
path.remove();
},
});
// 生成(Generate)阶段
const { code } = generate(ast);
// code: var a = 1;
console.log('code: ', code);
Если вы хотите узнать данные AST, соответствующие указанным выше источникам ввода, или попытаться составить их самостоятельно, вы можете использовать инструментAST Explorer(Вы также можете использовать тот, который поставляется с официального сайта Babel.Try It Out), следующим образом:
Напоминание: третье поле выше вызывается в форме API плагина.Если вы хотите узнать о разработке плагина Babel, вы можете проверитьСправочник по плагинам Babel / Написание вашего первого плагина Babel.
Если вы считаете, что процесс компиляции Babel слишком прост, вы можете попробовать геймплей более высокого уровня, например разработать собственные лексические и грамматические правила для реализации простого компилятора (эти правила встроены в Babel). компилятор преобразования в исходный код, но реализовать настоящий полный компилятор из JavaScript (TypeScript) в машинный код, включая реализацию промежуточного кода IR и предоставление рабочей среды машины и т. д. Вот пример, который может попробовать это высокоуровневая библиотека геймплеяantlr4ts(можно использовать с набором инструментов для кросс-компиляцииriscv-gnu-toolchain, создание инструментов компиляции gcc по-прежнему требует очень много времени).
Прочтите ссылку:Руководство пользователя Бабель,Руководство по плагину Babel
9. Каковы преимущества модуля ES6 над CommonJS?
Напоминание: Если вы просто хотите узнать ответ на этот вопрос, то переходите сразу на портал16.8.2 Static module structure. Кроме того, следующий код модуля ES был протестирован только в среде Node.js, и заинтересованные учащиеся могут использовать браузер для повторного тестирования. Webpack выбран для компиляции кода различных стандартных модулей, и заинтересованные студенты также могут использовать Rollup для компиляции и тестирования.
Спецификация и синтаксис ES Module и CommonJS здесь подробно описываться не будут, если вы не знаете их синтаксический сахар, то можете проверитьНачало работы с ECMAScript 6 / Синтаксис модуля,Стандарт модуля ESтак же какМодули CommonJS для Node.js, основные различия между ними заключаются в следующем:
Типы | ES Module | CommonJS |
---|---|---|
Способ загрузки | время компиляции | Время выполнения |
Знакомство с природой | ссылка / только чтение | Поверхностное копирование/чтение-запись |
объем модуля | this | this / __filename / __dirname... |
9.1 Способ загрузки
Метод загрузки является основным отличием между ES Module и CommonJS, что делает эти двавремя компиляцииа такжеВремя выполненияУ каждого есть преимущества и недостатки. Во-первых, давайте посмотрим на характеристики модуля ES с точки зрения методов загрузки, а именно:
// 编译时:VS Code 鼠标 hover 到 b 时可以显示出 b 的类型信息
import { b } from './b';
const a = 1;
// WARNING: 具有逻辑
if(a === 1) {
// 编译时:ESLint: Parsing error: 'import' and 'export' may only appear at the top level
// 运行时:SyntaxError: Unexpected token '{'
// TIPS: 这里可以使用 import() 进行动态导入
import { b } from './b';
}
const c = 'b';
// WARNING: 含有变量
// 编译时:ESLint:Parsing error: Unexpected token `
// 运行时:SyntaxError: Unexpected template string
import { d } from `./${c}`;
Метод загрузки CommonJS относительно модуля ES выглядит следующим образом:
const a = 1;
if(a === 1) {
// VS Code 鼠标 hover 到 b 时,无法显示出 b 的类型信息
const b = require('./b');
}
const c = 'b';
const d = require(`./${c}`);
Возможно, вы знаете различия между приведенными выше грамматиками.Далее мы сосредоточимся на объяснении основных причин различий между ними посредством теоретических знаний. существуетНачальная грамотность (1) / принцип компиляцииОсновное внимание уделяется объяснению фазы выполнения всего компилятора, как показано на следующем рисунке:Модуль ES принятстатическийСпособ загрузки, то есть импортируемые и экспортируемые в модуль зависимости, можно определить при компиляции кода. Как показано на рисунке выше, в процессе компиляции код может выполнять лексический и синтаксический анализ, проверку типов и оптимизацию кода. Таким образом, при использовании модуля ES для разработки кода вы можете быстро найти лексические синтаксические ошибки и ввести информацию о модуле через ESLint во время компиляции. В модуле ES будут некоторые неправильные методы загрузки, потому что эти методы загрузки содержат оценку логики и переменных во время выполнения.Только на этапе выполнения кода могут быть определены зависимости импорта и экспорта, что явно не соответствует механизму загрузки модуля ES. . . .
По сравнению с ES Module, CommonJS имеет очевидные отличия в способе загрузки модулей, потому что CommonJS выполняет динамический анализ метода загрузки во время выполнения, а взаимосвязь импорта и экспорта может быть определена только на этапе выполнения, поэтому статическая оптимизация компиляции и проверка типов не может быть выполнено.
Напоминание: обратите внимание на разницу между синтаксисом import и import(), import()Предложение в tc39, это предложение позволяет вам использовать операторы импорта, аналогичные import(`${path}/foo.js`) (предположительно, заимствуя способность CommonJS динамически загружать модули), поэтому оно также позволяет вам выполнять условную загрузку во время выполнения, поэтому- называетсяленивая загрузка. Кроме того, между import и import() есть еще несколько важных отличий, вы должны сами найти их в Google.
9.2 Оптимизация компиляции
Поскольку модуль ES может определять зависимости между модулями во время компиляции, оптимизация кода может выполняться в процессе компиляции. Например:
// hello.js
export function a() {
console.log('a');
}
export function b() {
console.log('b');
}
// index.js
// TIPS: Webpack 编译入口文件
// 这里不引入 function b
import { a } from './hello';
console.log(a);
Использование Webpack 5.47.1 (Webpack Cli 4.7.2) для компиляции кода приводит к следующим продуктам компиляции:
(()=>{"use strict";console.log((function(){console.log("a")}))})();
Можно обнаружить, что скомпилированный продукт не имеет кода функции б. Это необходимо для оптимизации кода на этапе компиляции и удаления неиспользуемого кода (мертвый код). Термин для такой оптимизации называетсяTree Shaking.
Дополнительный совет: вы можете думать о приложении как о дереве. Зеленым цветом обозначены фактически используемые Исходный код и Библиотека, которые являются живыми листьями на дереве. Серый представляет код без ссылок, увядшие листья на деревьях осенью. Чтобы убрать опавшие листья, нужно встряхнуть дерево, чтобы они упали.
Напоминание: в модуле ES оптимизация может завершиться ошибкой из-за побочных эффектов кода (таких как работа с методами прототипа и добавление свойств глобальных объектов и т. д.) Если вы хотите узнать больше об оптимизации Tree Shaking, вы можете прочитать ее подробно.Ваш Tree-Shaking бесполезен.
Чтобы сравнить возможности компиляции и оптимизации модуля ES, для импорта модуля также используется спецификация CommonJS:
// hello.js
exports.a = function () {
console.log('a');
};
exports.b = function () {
console.log('b');
};
// index.js
// TIPS: Webpack 编译入口文件
const { a } = require('./hello');
console.log(a);
При использовании Webpack для компиляции кода создаются следующие продукты компиляции:
(() => {
var o = {
418: (o, n) => {
(n.a = function () {
console.log('a');
}),
// function b 的代码并没有被去除
(n.b = function () {
console.log('b');
});
},
},
n = {};
function r(t) {
var e = n[t];
if (void 0 !== e) return e.exports;
var s = (n[t] = { exports: {} });
return o[t](s, s.exports, r), s.exports;
}
(() => {
const { a: o } = r(418);
console.log(o);
})();
})();
Может быть обнаружен, что в модуле Commonjs, хотя функция B не используется, код все еще будет упакован и скомпилирован, точно потому, что модуль Commonjs может быть импортирован только синхронно во время выполнения, поэтому его нельзя определить при компиляции, будь то функция b это мертвый код.
Советы. В среде Node.js обычно нет необходимости компилировать код модуля CommonJS, если только вы не используете некоторые новые грамматические характеристики, с которыми текущая версия Node несовместима.
Вы можете заметить новую проблему.Когда мы создаем библиотеку инструментов или библиотеку компонентов, мы обычно компилируем пакет библиотеки в синтаксисе ES5, поэтому, хотя Babel и Webpack по умолчанию игнорируют модули в node_modules, наш проект компилирует эти модули, представленные в время по-прежнему совместимы. В этом процессе, если пакет библиотеки, который вы создаете, очень большой и вы не предоставляете очень подробный метод загрузки по требованию, вы можете скомпилировать свой исходный код, чтобы скомпилированный продукт мог поддерживать режим импорта и экспорта ES. Модуль (обратите внимание, что поддерживается только синтаксис модулей в ES6, другие синтаксисы все еще необходимо скомпилировать в ES5).Когда проект фактически вводит эти библиотечные пакеты, непредставленный код (мертвый код) может быть удален во время компиляции с помощью функции встряхивания дерева.
Напоминание: если вы хотите узнать, как сделать так, чтобы выпущенный пакет библиотеки Npm поддерживал функцию Tree Shaking, вы можете проверитьdefense-of-dot-js / Typical Usage,Webpack / Final Steps,pgk.moduleтак же какrollup.js / Дерево Шаки….
Описание Подсказка для поддержки WebPack для поля модуля: свойство модуля должно указывать на скрипт, который использует синтаксис модуля ES2015, но никаких других функций синтаксиса, которые еще не поддерживаются браузерами или узлом. Это позволяет WebPack разбирать сам модуль, что позволяет Для легких пучков через дерево встряхивание, если пользователи потребляют только определенные части библиотеки.
9.3 Принцип загрузки и введение свойств
Напоминание: следующая теоретическая часть и содержание изображений взяты из статьи 2018 года. ES modules: A cartoon deep-dive, если вы хотите узнать больше принципиальной информации, вы можете проверить TC3916.2 Modules.
Использование модулей для разработки в ES Module фактически строит граф зависимостей между модулями во время компиляции. При запуске кода ES6 в файловой системе браузера или службы необходимо проанализировать все файлы модулей, а затем модули преобразовать в структуру данных Module Record, как показано на следующем рисунке:
事实上, ES Module 的加载过程主要分为如下三个阶段:
- Конструкция: в основном делится на поиск, загрузку (загрузка файлов в браузере, загрузка файлов в локальную файловую систему) и последующий разбор файлов в записи модулей.
- Создание экземпляра: Выделите пространство памяти для всех записей модуля (в данный момент значения не заполнены) и определите ссылочное отношение между ними в соответствии с отношением импорта и экспорта.Процесс определения ссылочного отношения называется связыванием.
- Оценка: выполняет код, заполняя адрес памяти данными модуля времени выполнения.
Напоминание: вышеупомянутые три этапа импорта на самом деле более интуитивно понятны в import() (хотя импорт поддерживается большинством браузеров, мы по-прежнему будем использовать скомпилированный код для запуска в фактическом процессе разработки и выполнения, а не метод динамической асинхронной загрузки из удаленный адрес тега скрипта браузера) и import() на самом деле, если вы хотите добиться оптимизации ленивой загрузки (например, маршрутизация ленивой загрузки в Vue, это больше в среде хостинга браузера, чем в среде Node. js , мы не будем подробно останавливаться на реализации после компиляции), велика вероятность, что вам придется полностью пройти вышеперечисленные три этапа.асинхронныйПроцесс загрузки, проверьте еще раз подробностиtc39 динамическое предложение: это предложение добавляет синтаксическую форму импорта (спецификатора), которая во многом действует как функция (но см. ниже). Она возвращает обещание для объекта пространства имен модуля запрашиваемого модуля, который создается после выборки, создания экземпляра и оценка всех зависимостей модуля, а также самого модуля.
Три этапа загрузки модуля модуля ES должны выполняться во время компиляции и во время выполнения соответственно (некоторым студентам может быть так же любопытно, как и мне, выполняется ли фаза создания экземпляра во время компиляции или во время выполнения, в соответствии сtc39 динамически загружать предложенияОписание здесь дает ответ, который вы хотите: существующие синтаксические формы для импорта модулей являются статическими объявлениями.Они принимают строковый литерал в качестве спецификатора модуля и вводят привязки в локальную область через процесс «связывания» перед выполнением. ), и модули в спецификации CommonJS выполняются синхронно и последовательно во время выполнения, и модули не будут прерываться в процессе загрузки, как показано на следующем рисунке:На приведенном выше рисунке, когда main.js запускается и загружает counter.js, он будет ждать завершения работы counter.js, прежде чем продолжить выполнение кода, поэтому загрузка модулей в CommonJS блокируется. CommonJS использует модули синхронной блокировки загрузки, потому что ему нужно загружать файлы только из локальной файловой системы, что потребляет очень мало производительности и времени, в то время как модуль ES необходимо загружать при работе в браузере (обратите внимание, что браузер упоминается здесь). Затем файл может быть создан и запущен, что, если это делается синхронно, может повлиять на производительность загрузки страницы.
Из процесса связывания модулей ES можно обнаружить, что эталонное отношение между модулями является ссылкой на адрес памяти следующим образом:
// hello.js
export let a = 1;
setTimeout(() => {
a++;
}, 1000);
// index.js
import { a } from './hello.js';
setTimeout(() => {
console.log(a); // 2
}, 2000);
Результат выполнения приведенного выше кода в среде Node (v14.15.4) равен 2. Сравните выполнение спецификации CommonJS:
// hello.js
exports.a = 1;
setTimeout(() => {
exports.a++;
}, 1000);
// index.js
let { a } = require('./hello');
setTimeout(() => {
console.log(a); // 1
}, 2000);
Можно обнаружить, что напечатанная информация о результате отличается от результата модуля ES, а результат выполнения здесь равен 1. Основной причиной вышеуказанной разницы является другой способ создания экземпляра, как показано на следующем рисунке:
При экспорте модуля ES Module Record будет отслеживать в реальном времени (под проводом здесь понимается ссылка или ссылка) и привязывать адрес памяти, соответствующий каждой экспортируемой переменной (из приведенного выше рисунка видно, что значение не был заполнен, в то время как функция может быть инициализирована на этапе связывания), а импорт также соответствует тому же адресу памяти, что и экспорт, поэтому обработка импортируемой переменной фактически обрабатывает данные того же адреса ссылки , как показано на следующем рисунке:
Спецификация CommonJS фактически экспортирует копию значения при экспорте, как показано на следующем рисунке:
Во время выполнения приведенного выше кода сначала копируется значение переменной а, поэтому, хотя таймер установлен, информация, напечатанная после введения переменной а, по-прежнему равна 1. Следует отметить, что эта копия является поверхностной копией, а именно:
// hello.js
exports.a = {
value: 1,
};
setTimeout(() => {
exports.a.value++;
}, 1000);
// index.js
let { a } = require('./hello');
setTimeout(() => {
console.log(a.value); // 2
}, 2000);
Затем сравните различия после компиляции и скомпилируйте исходный код модуля ES (по-прежнему используя Webpack).Скомпилированный код выглядит следующим образом:
(() => {
'use strict';
let e = 1;
setTimeout(() => {
e++;
}, 1e3),
setTimeout(() => {
console.log(e);
}, 2e3);
})();
Видно, что после компиляции кода модуля ES используется одно и то же значение переменной, в это время компилируется код CommonJS:
(() => {
var e = {
418: (e, t) => {
// hello.js 中的模块代码
(t.a = 1),
setTimeout(() => {
t.a++;
}, 1e3);
},
},
t = {};
function o(r) {
// 开辟模块的缓存空间
var s = t[r];
// 获取缓存信息,每次返回相同的模块对象信息
if (void 0 !== s) return s.exports;
// 开辟模块对象的内存空间
var a = (t[r] = { exports: {} });
// 逗号运算符,先运行模块代码,赋值模块对象的值,然后返回模块信息
// 由于缓存,模块代码只会被执行一次
return e[r](a, a.exports, o), a.exports;
}
(() => {
// 浅拷贝
let { a: e } = o(418);
setTimeout(() => {
// 尽管 t.a ++,这里输出的仍然是 1
console.log(e);
}, 2e3);
})();
})();
Можно обнаружить, что спецификация CommonJS кэширует информацию о модуле после компиляции, так что в следующий раз данные модуля будут напрямую получены из кэша. Кроме того, кэширование приводит к тому, что код модуля выполняется только один раз. Ознакомьтесь с официальной документацией Node.js дляОписание кеша для спецификации CommonJS, и обнаружил, что компиляция Webpack полностью соответствует механизму кэширования спецификации CommonJS. Поняв этот механизм, вы обнаружите, что многократное использование require для загрузки модуля не приведет к многократному выполнению кода, что является решением проблемы бесконечности.круговая зависимостьважная особенность ж.
В дополнение к возможным различиям в способе введения, могут быть некоторые различия в импортированном коде, например, в модуле ES:
// hello.js
export function a() {
console.log('a this: ', this);
}
// index.js
import { a } from './hello.js';
// a = 1;
^
// TypeError: Assignment to constant variable.
// ...
// at ModuleJob.run (internal/modules/esm/module_job.js:152:23)
// at async Loader.import (internal/modules/esm/loader.js:166:24)
// at async Object.loadESM (internal/process/esm_loader.js:68:5)
a = 1;
Использование Node.js для прямого запуска вышеуказанного кода модуля ES приведет к ошибке, поскольку импортированная переменная может рассматриваться как переменная только для чтения в соответствии с подсказкой, и если вы используете Webpack для компиляции и запуска, выше не будет Кроме того, импортированные в CommonJS переменные могут быть доступны для чтения и записи. Конечно, в дополнение к этому вы также можете попробовать другие аспекты, такие как:
// hello.js
// 非严格模式
b = 1;
export function a() {
console.log('a this: ', this);
}
// index.js
import { a } from './hello.js';
console.log('a: ', a);
Вы обнаружите, что выполнение приведенного выше кода модуля ES с использованием среды Node.js приведет к прямому появлению следующего сообщения об ошибке:
ReferenceError: b is not defined
at file:///Users/ziyi/Desktop/Gitlab/Explore/module-example/esmodule/hello.js:1:3
at ModuleJob.run (internal/modules/esm/module_job.js:152:23)
at async Loader.import (internal/modules/esm/loader.js:166:24)
at async Object.loadESM (internal/process/esm_loader.js:68:5)
Поскольку модули модулей ES должны работать в строгом режиме, но спецификация Commonjs не является такого требования, если вы более тщательно наблюдаете, вы найдете использование времени компиляции Webpack, Compabled Compabled Compabled будет добавлен перед «использованием Строгий «и Commonjs скомпилирован код №.
9.4 Объем модуля
Вы обнаружите, что такие переменные, как __dirname и __filename, можно использовать при разработке кода в модулях Node.js (следует отметить, что во внешнем продукте CommonJS, скомпилированном Webpack, нет информации о переменных, такой как __filename и __dirname. Эти информация о переменных файловой системы не требуется), потому что Node.js при загрузке оборачивает модуль следующим образом:
// https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L206
const wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});',
];
Просто просмотрев код этой области модуля, давайте продолжим смотреть на исходный код require:
// https://github.com/nodejs/node/blob/3914354cd7ddc65774f13bbe435978217149793c/lib/internal/modules/cjs/loader.js#L997
Module.prototype.require = function(id) {
validateString(id, 'id');
if (id === '') {
throw new ERR_INVALID_ARG_VALUE('id', id,
'must be a non-empty string');
}
requireDepth++;
try {
return Module._load(id, this, /* isMain */ false);
} finally {
requireDepth--;
}
};
// https://github.com/nodejs/node/blob/3914354cd7ddc65774f13bbe435978217149793c/lib/internal/modules/cjs/loader.js#L757
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call
// `NativeModule.prototype.compileForPublicLoader()` and return the exports.
// 3. Otherwise, create a new module for the file and save it to the cache.
// Then have it load the file contents before returning its exports
// object.
Module._load = function(request, parent, isMain) {
let relResolveCacheIdentifier;
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
// Fast path for (lazy loaded) modules in the same directory. The indirect
// caching is required to allow cache invalidation without changing the old
// cache key names.
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
const filename = relativeResolveCache[relResolveCacheIdentifier];
// 有缓存,则走缓存
if (filename !== undefined) {
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded)
return getExportsForCircularRequire(cachedModule);
return cachedModule.exports;
}
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
// `node:` 用于检测核心模块,例如 fs、path 等
// Node.js 文档:http://nodejs.cn/api/modules.html#modules_core_modules
// 这里主要用于绕过 require 缓存
const filename = Module._resolveFilename(request, parent, isMain);
if (StringPrototypeStartsWith(filename, 'node:')) {
// Slice 'node:' prefix
const id = StringPrototypeSlice(filename, 5);
const module = loadNativeModule(id, request);
if (!module?.canBeRequiredByUsers) {
throw new ERR_UNKNOWN_BUILTIN_MODULE(filename);
}
return module.exports;
}
// 缓存处理
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded) {
const parseCachedModule = cjsParseCache.get(cachedModule);
if (!parseCachedModule || parseCachedModule.loaded)
return getExportsForCircularRequire(cachedModule);
parseCachedModule.loaded = true;
} else {
return cachedModule.exports;
}
}
const mod = loadNativeModule(filename, request);
if (mod?.canBeRequiredByUsers) return mod.exports;
// Don't call updateChildren(), Module constructor already does.
const module = cachedModule || new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
Module._cache[filename] = module;
if (parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
}
let threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
if (parent !== undefined) {
delete relativeResolveCache[relResolveCacheIdentifier];
const children = parent?.children;
if (ArrayIsArray(children)) {
const index = ArrayPrototypeIndexOf(children, module);
if (index !== -1) {
ArrayPrototypeSplice(children, index, 1);
}
}
}
} else if (module.exports &&
!isProxy(module.exports) &&
ObjectGetPrototypeOf(module.exports) ===
CircularRequirePrototypeWarningProxy) {
ObjectSetPrototypeOf(module.exports, ObjectPrototype);
}
}
return module.exports;
};
Напоминание: связь между оберткой и _load здесь не проясняется (как в конце концов выполнить обертку в _load), вы можете проследить в исходном коде Node.js, чтобы увидеть, как выполняется приведенный выше код, является ли он eval? Не говори, у меня голова болит, если хочешь узнать больше информации, можешь проверитьNode.js / vm. Кроме того, заинтересованные студенты должны также понимать базовую реализацию синтаксиса импорта в Node.js, Здесь мозг болит, поэтому нет глубокого изучения.
Теплое напоминание: например, если вы не можете найти ссылку на выполнение приведенного выше кода в исходном коде, самый простой способ — ввести модуль ошибки и позволить сообщению об ошибке создать стек ошибок, например, как показано ниже: Вы найдете Внизу выполняется wrapSafe, и вы можете снова начать изучение, потому что вам должно быть интересно узнать слово «безопасный».Использовали ли вы изоляцию песочницы при его выполнении?
SyntaxError: Cannot use import statement outside a module
at wrapSafe (internal/modules/cjs/loader.js:979:16)
at Module._compile (internal/modules/cjs/loader.js:1027:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
at Module.load (internal/modules/cjs/loader.js:928:32)
at Function.Module._load (internal/modules/cjs/loader.js:769:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
at internal/main/run_main_module.js:17:47
Напоминание: часто ли интервьюеры спрашивали о связи между exports и module.exports в прошлом? На самом деле, нет необходимости беспокоиться об этом вопросе вообще, потому что они указывают на один и тот же ссылочный адрес. Если вы переназначите exports, то ссылка произошла.Если вы измените его, конечно, новая часть, на которую ссылаются, не будет экспортирована, потому что из исходного кода видно, что мы экспортируем здесь module.exports.
Далее мы в основном сосредоточимся на различии этого контекста выполнения (обратите внимание, что здесь тестируется только среда Node.js, а скомпилированный код может быть другим).Сначала выполняем код модуля ES Module:
// hello.js
export function a() {
console.log('this: ', this); // undefined
}
// index.js
import { a } from './hello.js';
a();
Затем мы выполняем код CommonJS:
// hello.js
exports.a = function () {
console.log('this: ', this);
};
// index.js
let { a } = require('./hello');
a();
Вы обнаружите, что контекст этого является информативным, который может быть информацией о текущем модуле.Подробности не детализированы:
Напоминание: можно ли выполнять отладку Node.js в браузере? вы можете проверить этоОтладка Node.js, конечно, вы также можете использовать VS Code для отладки, вам нужно сделать некоторую дополнительную настройку запуска, конечно, если вы чувствуете, что метод отладки браузера, который идет с Node.js, слишком неудобен, вы также можете придумать способ , как использовать IP-порт в браузере Отладка в , и может отслеживать и отлаживать изменения кода.
Не нужно особо заморачиваться над детальной реализацией кода, нужно лишь иметь общее представление о процессе импорта модулей в CommonJS.По сути, результат компиляции Webpack можно примерно понимать как упрощенный браузер версия кода. Затем я до сих пор помню тему, о которой я говорил в интервью ранее:Два года опыта работы успешно прошли собеседование с Ali P6. Резюме / Как настроить псевдонимы пути на стороне узла (аналогично настройке псевдонимов в Webpack)Если вы прочитаете вышеуказанный источник, в основном идея состоит в том, чтобы потребовать метода цепочки прототипов взлома:
const Module = require('module');
const originalRequire = Module.prototype.require;
Module.prototype.require = function(id){
// 这里加入 path 的逻辑
return originalRequire.apply(this, id);
};
резюме
Текущая серия ответов на вопросы для интервью немного сбивает с толку.В будущем вопросы для интервью могут быть просто классифицированы по категориям, чтобы сортировать более систематические ответы. Цель этой статьи — надеяться, что каждый сможет сделать выводы о вопросах интервью, чтобы углубить свое понимание (когда мы задаем вопрос, можно получить N вопросов).