Автор: Цуй Цзин
Предыдущий обзорМы представили общий процесс компиляции веб-пакета, и на этот раз мы проанализируем базовый Tapable.
представлять
Во время всего процесса компиляции webpack выставляется большое количество хуков для внутренних/внешних плагинов, и различные плагины могут быть расширены.Код внутренней обработки также зависит от хуков и плагинов, и эта часть функции зависит от Tapable. Общий процесс выполнения webpack в целом управляется событиями. От одного события к другому.TapableИспользуется для предоставления различных типов крючков. Давайте предварительно познакомимся с Tapable на следующем интуитивно понятном примере использования:
const {
SyncHook
} = require('tapable')
// 创建一个同步 Hook,指定参数
const hook = new SyncHook(['arg1', 'arg2'])
// 注册
hook.tap('a', function (arg1, arg2) {
console.log('a')
})
hook.tap('b', function (arg1, arg2) {
console.log('b')
})
hook.call(1, 2)
Похоже, что эта функция похожа на EventEmit, сначала регистрируя событие, а затем запуская его. Но Tapable более мощный, чем EventEmit. Из официального введения вы можете видеть, что Tapable предоставляет множество типов хуков, которые делятся на две категории: синхронные и асинхронные (в асинхронном различают асинхронный параллельный и асинхронный последовательный), и в соответствии с различными условиями завершения выполнения события, происходит от типа Out Bail/Waterfall/Loop.
На диаграмме ниже показано, что делает каждый тип:
-
BasicHook: выполнить каждый, не заботясь о возвращаемом значении функции, есть SyncHook, AsyncParallelHook, AsyncSeriesHook.
В типе eventEmit, который мы обычно используем, этот тип хука очень распространен.
-
BailHook: последовательно выполнять хук и возвращаться, когда первый результат приводит к !== undefined , и не продолжать выполнение. Есть: SyncBailHook, AsyncSeriseBailHook, AsyncParallelBailHook.
В каких сценариях будет использоваться BailHook? Рассмотрим следующий пример: предположим, что у нас есть модуль M, и если он удовлетворяет любому из трех условий A, B или C, упаковать его в один. Здесь нет порядка A, B и C, поэтому вы можете использовать AsyncParallelBailHook для решения этой проблемы:
x.hooks.拆分模块的Hook.tap('A', () => { if (A 判断条件满足) { return true } }) x.hooks.拆分模块的Hook.tap('B', () => { if (B 判断条件满足) { return true } }) x.hooks.拆分模块的Hook.tap('C', () => { if (C 判断条件满足) { return true } })
Если A возвращает true, то нет необходимости оценивать B и C. Однако, когда проверка A, B и C должна строго следовать последовательности, необходимо использовать последовательный SyncBailHook (используется, когда A, B и C являются синхронными функциями) или AsyncSeriseBailHook (используется, когда A, B и C — асинхронные функции) ).
-
WaterfallHook: аналогично уменьшению, если результат предыдущей функции Hook !== undefined, результат будет использоваться в качестве первого параметра последней функции Hook. Поскольку он выполняется последовательно, этот хук предоставляется только в классах Sync и AsyncSeries: SyncWaterfallHook, AsyncSeriesWaterfallHook.
Когда данные необходимо обработать в три этапа A, B, C для получения конечного результата, и если в A выполняется условие a, то они будут обработаны, иначе не будут обработаны, B и C одинаковы, то вы можете использовать следующее
x.hooks.tap('A', (data) => { if (满足 A 需要处理的条件) { // 处理数据 data return data } else { return } }) x.hooks.tap('B', (data) => { if (满足B需要处理的条件) { // 处理数据 data return data } else { return } }) x.hooks.tap('C', (data) => { if (满足 C 需要处理的条件) { // 处理数据 data return data } else { return } })
-
LoopHook: выполнять хук в цикле до тех пор, пока все результаты функции не приведут к результату === undefined. Точно так же из-за зависимости от серийности есть только SyncLoopHook и AsyncSeriseLoopHook (PS: я пока не видел конкретного варианта использования)
принцип
Сначала мы даем основной контекст кода Tapable:
Регистрация события хука -> триггер хука -> генерировать код выполнения хука -> выполнить
Схема классов хуков очень проста: все хуки наследуются от базового абстрактного класса Hook, но в то же время он содержит класс xxxCodeFactory, который будет использоваться для генерации кода выполнения хуков.
регистрация на мероприятие
Основная логика Tapable - это сначала зарегистрировать функцию обработчика, соответствующую крюку через метод крана экземпляра класса:
Tapable предоставляет три метода для регистрации событий: tap/tapAsync/tapPromise (логика реализации находится в базовом классе Hook), которые предназначены для синхронного (tap)/асинхронного (tapAsync/tapPromise) события соответственно и не присваивают значения содержимому, которое должно быть push to taps.То же значение типа, как показано выше.
Для перехватчиков SyncHook, SyncBailHook, SyncLoopHook и SyncWaterfallHook методы tapAsync и tapPromise в базовом классе будут переопределены, чтобы пользователи не могли неправильно использовать асинхронные методы в синхронных перехватчиках.
tapAsync() {
throw new Error("tapAsync is not supported on a SyncHook");
}
tapPromise() {
throw new Error("tapPromise is not supported on a SyncHook");
}
триггер события
В соответствии с tap/tapAsync/tapPromise, Tapable предоставляет три метода запуска событий call/callAsync/promise. Эти три метода также находятся в базовом классе Hook, конкретная логика выглядит следующим образом.
this.call = this._call = this._createCompileDelegate("call", "sync");
this.promise = this._promise = this._createCompileDelegate("promise", "promise");
this.callAsync = this._callAsync = this._createCompileDelegate("callAsync", "async");
// ...
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
_createCompileDelegate(name, type) {
const lazyCompileHook = (...args) => {
this[name] = this._createCall(type);
return this[name](...args);
};
return lazyCompileHook;
}
Будь то вызов или callAsync и обещание, в конечном итоге он будет вызван дляcompile
метод, а до этого разницаcompile
прошел вtype
значение другое. а такжеcompile
по разнымtype
Тип генерирует исполняемую функцию, которая затем выполняется.
Обратите внимание, что в приведенном выше коде есть имя переменнойlazyCompileHook, ленивая компиляция. Когда мы создадим новый хук, сначала будет сгенерирован код CompileDelegate, соответствующий promise, call и callAsync.
this.call = (...args) => {
this[name] = this._createCall('sync');
return this['call'](...args);
}
this.promise = (...args) => {
this[name] = this._createCall('promise');
return this['promise'](...args);
}
this.callAsync = (...args) => {
this[name] = this._createCall('async');
return this['callAsync'](...args);
}
Когда хук срабатывает, например, при выполненииxxhook.call()
Когда соответствующая исполнительная функция скомпилирована. Этот процесс является так называемой «ленивой компиляцией», то есть он компилируется только тогда, когда он используется, и достигнута оптимальная эффективность выполнения.
Далее мы в основном рассматриваемcompile
Логика этого заключается в том, где находится большая часть логики Tapable.
Выполнить генерацию кода
Прежде чем смотреть исходный код, мы можем написать несколько простых демонстраций, чтобы увидеть, какой код выполнения Tapable в конечном итоге генерирует, чтобы получить интуитивное представление:
Изображение выше — это код, сгенерированный SyncHook.call, AsyncSeriesHook.callAsync и AsyncSeriesHook.promise соответственно._x
Зарегистрированные функции событий сохраняются в_fn${index}
Это выполнение каждой функции, а сгенерированный код основан на разных хуках и разных методах вызова._fn${index}
Будут разные реализации. Как эти различия генерируются с помощью кода? Давайте посмотрим поближеcompile
метод.
compile
Этот метод не реализован в базовом классе, его реализация находится в каждом производном классе. Возьмите SyncHook в качестве примера, посмотрите
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
const factory = new SyncHookCodeFactory();
class SyncHook extends Hook {
// ... 省略其他代码
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}
Фабричный шаблон используется для генерации исполняемого кода здесь:HookCodeFactory
Это базовый класс фабрики, используемый для генерации кода, и каждый хук является производным от подкласса. Метод компиляции во всех хуках вызывает метод create. Давайте посмотрим, что делает этот метод create.
create(options) {
this.init(options);
switch(this.options.type) {
case "sync":
return new Function(this.args(), "\"use strict\";\n" + this.header() + this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
onDone: () => "",
rethrowIfPossible: true
}));
case "async":
return new Function(this.args({
after: "_callback"
}), "\"use strict\";\n" + this.header() + this.content({
onError: err => `_callback(${err});\n`,
onResult: result => `_callback(null, ${result});\n`,
onDone: () => "_callback();\n"
}));
case "promise":
let code = "";
code += "\"use strict\";\n";
code += "return new Promise((_resolve, _reject) => {\n";
code += "var _sync = true;\n";
code += this.header();
code += this.content({
onError: err => {
let code = "";
code += "if(_sync)\n";
code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`;
code += "else\n";
code += `_reject(${err});\n`;
return code;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
code += "_sync = false;\n";
code += "});\n";
return new Function(this.args(), code);
}
}
На первый взгляд кода многовато, для упрощения нарисуем картинку, которая представляет собой следующий процесс:
Из этого видно, что в create реализуется только основной шаблон кода, а общие части (параметры функции и публичные параметры в начале функции) реализуются, а затем откладываются разностные части.content
, для каждого подкласса для реализации. Затем сравните sub-CodeFactory, унаследованный от HookCodeFactory в каждом хуке по горизонтали, и взгляните на различия реализации контента:
//syncHook
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
//syncBailHook
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onResult: (i, result, next) => `if(${result} !== undefined) {\n${onResult(result)};\n} else {\n${next()}}\n`,
onDone,
rethrowIfPossible
});
}
//AsyncSeriesLoopHook
class AsyncSeriesLoopHookCodeFactory extends HookCodeFactory {
content({ onError, onDone }) {
return this.callTapsLooping({
onError: (i, err, next, doneBreak) => onError(err) + doneBreak(true),
onDone
});
}
}
// 其他的结构都类似,便不在这里贴代码了
Как видите, во всех подклассах реализованоcontent
метод, который вызывается в соответствии с различными процессами выполнения ловушек.callTapsSeries/callTapsParallel/callTapsLooping
и будетonError, onResult, onDone, rethrowIfPossible
Фрагменты кода для этих четырех случаев.
callTapsSeries/callTapsParallel/callTapsLooping
Все они находятся в методе базового класса, и каждый из этих трех методов перейдет в метод callTap. Сначала взгляните на метод callTap. Код относительно длинный, если вы не хотите читать код, вы можете прямо посмотреть на следующую картинку.
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
let code = "";
let hasTapCached = false;
// 这里的 interceptors 先忽略
for(let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if(interceptor.tap) {
if(!hasTapCached) {
code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
hasTapCached = true;
}
code += `${this.getInterceptor(i)}.tap(${interceptor.context ? "_context, " : ""}_tap${tapIndex});\n`;
}
}
code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
const tap = this.options.taps[tapIndex];
switch(tap.type) {
case "sync":
if(!rethrowIfPossible) {
code += `var _hasError${tapIndex} = false;\n`;
code += "try {\n";
}
if(onResult) {
code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined
})});\n`;
} else {
code += `_fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined
})});\n`;
}
if(!rethrowIfPossible) {
code += "} catch(_err) {\n";
code += `_hasError${tapIndex} = true;\n`;
code += onError("_err");
code += "}\n";
code += `if(!_hasError${tapIndex}) {\n`;
}
if(onResult) {
code += onResult(`_result${tapIndex}`);
}
if(onDone) {
code += onDone();
}
if(!rethrowIfPossible) {
code += "}\n";
}
break;
case "async":
let cbCode = "";
if(onResult)
cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
else
cbCode += `_err${tapIndex} => {\n`;
cbCode += `if(_err${tapIndex}) {\n`;
cbCode += onError(`_err${tapIndex}`);
cbCode += "} else {\n";
if(onResult) {
cbCode += onResult(`_result${tapIndex}`);
}
if(onDone) {
cbCode += onDone();
}
cbCode += "}\n";
cbCode += "}";
code += `_fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined,
after: cbCode
})});\n`;
break;
case "promise":
code += `var _hasResult${tapIndex} = false;\n`;
code += `_fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined
})}).then(_result${tapIndex} => {\n`;
code += `_hasResult${tapIndex} = true;\n`;
if(onResult) {
code += onResult(`_result${tapIndex}`);
}
if(onDone) {
code += onDone();
}
code += `}, _err${tapIndex} => {\n`;
code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;
code += onError(`_err${tapIndex}`);
code += "});\n";
break;
}
return code;
}
Он также делится на sync/async/promise соответственно.Приведенный выше код переводится в диаграмму следующим образом
- тип синхронизации:
- асинхронный тип:
- тип обещания
В общем, callTap — это шаблон для одноразового выполнения функции, и он тоже делится на три типа: sync/async/promise по методу вызова.
Затем посмотрите на метод callTapsSeries,
callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {
if(this.options.taps.length === 0)
return onDone();
const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
const next = i => {
if(i >= this.options.taps.length) {
return onDone();
}
const done = () => next(i + 1);
const doneBreak = (skipDone) => {
if(skipDone) return "";
return onDone();
}
return this.callTap(i, {
onError: error => onError(i, error, done, doneBreak),
// onResult 和 onDone 的判断条件,就是说有 onResult 或者 onDone
onResult: onResult && ((result) => {
return onResult(i, result, done, doneBreak);
}),
onDone: !onResult && (() => {
return done();
}),
rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
});
};
return next(0);
}
Обратите внимание на условия onResult и onDone в this.callTap, то есть выполняется либо onResult, либо onDone. Давайте рассмотрим простую логику прямого перехода к onDone. Затем в сочетании с описанным выше процессом callTap, взяв в качестве примера синхронизацию, вы можете получить следующую картину:
В этом случае результатом callTapsSeries является рекурсивная генерация каждого кода вызова до последнего, прямой вызов переданного извне метода onDone для получения конечного кода, и рекурсия завершается. А для процесса выполнения onResult взгляните на код onResult:return onResult(i, result, done, doneBreak)
. Простое понимание, тот же процесс, что и на рисунке выше, за исключением того, что onResult обернут слоем логики о onResult снаружи.
Затем смотрим на код callTapsLooping:
callTapsLooping({ onError, onDone, rethrowIfPossible }) {
if(this.options.taps.length === 0)
return onDone();
const syncOnly = this.options.taps.every(t => t.type === "sync");
let code = "";
if(!syncOnly) {
code += "var _looper = () => {\n";
code += "var _loopAsync = false;\n";
}
// 在代码开始前加入 do 的逻辑
code += "var _loop;\n";
code += "do {\n";
code += "_loop = false;\n";
// interceptors 先忽略,只看主要部分
for(let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if(interceptor.loop) {
code += `${this.getInterceptor(i)}.loop(${this.args({
before: interceptor.context ? "_context" : undefined
})});\n`;
}
}
code += this.callTapsSeries({
onError,
onResult: (i, result, next, doneBreak) => {
let code = "";
code += `if(${result} !== undefined) {\n`;
code += "_loop = true;\n";
if(!syncOnly)
code += "if(_loopAsync) _looper();\n";
code += doneBreak(true);
code += `} else {\n`;
code += next();
code += `}\n`;
return code;
},
onDone: onDone && (() => {
let code = "";
code += "if(!_loop) {\n";
code += onDone();
code += "}\n";
return code;
}),
rethrowIfPossible: rethrowIfPossible && syncOnly
})
code += "} while(_loop);\n";
if(!syncOnly) {
code += "_loopAsync = true;\n";
code += "};\n";
code += "_looper();\n";
}
return code;
}
Простейшая логика, которую нужно упростить в первую очередь, — это следующий абзац, очень простая логика do/while.
var _loop
do {
_loop = false
// callTapsSeries 生成中间部分代码
} while(_loop)
CallTapsSeries изучил свой код раньше.При вызове callTapsSeries здесь есть логика onResult, что означает, что средняя часть будет генерировать код, подобный следующему (в качестве примера все еще берется синхронизация)
var _fn${tapIndex} = _x[${tapIndex}];
var _hasError${tapIndex} = false;
try {
fn1(${this.args({
before: tap.context ? "_context" : undefined
})});
} catch(_err) {
_hasError${tapIndex} = true;
onError("_err");
}
if(!_hasError${tapIndex}) {
// onResult 中生成的代码
if(${result} !== undefined) {
_loop = true;
// doneBreak 位于 callTapsSeries 代码中
//(skipDone) => {
// if(skipDone) return "";
// return onDone();
// }
doneBreak(true); // 实际为空语句
} else {
next()
}
}
Управляя функцией onResult между следующей функцией, которая будет выполняться после завершения выполнения, сгенерируйте другой код, полученный из логики callTapsSeries LoopHook.
Затем мы смотрим на Calltapsparallel
callTapsParallel({ onError, onResult, onDone, rethrowIfPossible, onTap = (i, run) => run() }) {
if(this.options.taps.length <= 1) {
return this.callTapsSeries({ onError, onResult, onDone, rethrowIfPossible })
}
let code = "";
code += "do {\n";
code += `var _counter = ${this.options.taps.length};\n`;
if(onDone) {
code += "var _done = () => {\n";
code += onDone();
code += "};\n";
}
for(let i = 0; i < this.options.taps.length; i++) {
const done = () => {
if(onDone)
return "if(--_counter === 0) _done();\n";
else
return "--_counter;";
};
const doneBreak = (skipDone) => {
if(skipDone || !onDone)
return "_counter = 0;\n";
else
return "_counter = 0;\n_done();\n";
}
code += "if(_counter <= 0) break;\n";
code += onTap(i, () => this.callTap(i, {
onError: error => {
let code = "";
code += "if(_counter > 0) {\n";
code += onError(i, error, done, doneBreak);
code += "}\n";
return code;
},
onResult: onResult && ((result) => {
let code = "";
code += "if(_counter > 0) {\n";
code += onResult(i, result, done, doneBreak);
code += "}\n";
return code;
}),
onDone: !onResult && (() => {
return done();
}),
rethrowIfPossible
}), done, doneBreak);
}
code += "} while(false);\n";
return code;
}
Поскольку код, окончательно сгенерированный callTapsParallel, выполняется одновременно, поток кода сильно отличается от двух. Вышеприведенный код выглядит много, но если вы посмотрите на основную структуру, то на самом деле это следующая картинка (в качестве примера все еще используется синхронизация)
Подводя итог, callTap реализует три основных шаблона одноразового выполнения функции sync/promise/async, и в то же время часть кода onError/onDone/onResult, задействованная в процессе выполнения функции, не учитывается. В callTapsSeries/callTapsLooping/callTapsParallel разные шаблоны процессов реализуются путем передачи разных onError/onDone/onResult. Однако из-за большой разницы в callTapsParallel сгенерированные результаты снова обрабатываются путем переноса слоя функции onTap вне callTap.
На данный момент у нас есть три основных шаблона: серия/петля/параллельность. Мы заметили, что callTapsSeries/callTapsLooping/callTapsParallel также предоставляет свои собственные onError, onResult, onDone, rethrowIfPossible, onTap, так что каждый под-хук может настраивать базовый шаблон в соответствии с различными ситуациями. Если взять в качестве примера SyncBailHook, основное отличие его от базового шаблона, полученного с помощью callTapsSeries, заключается в другом времени окончания выполнения функции. Таким образом, для SyncBailHook изменение onResult может достичь цели:
class SyncBailHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
// 修改一下 onResult,如果 函数执行得到的 result 不为 undefined 则直接返回结果,否则继续执行下一个函数
onResult: (i, result, next) => `if(${result} !== undefined) {\n${onResult(result)};\n} else {\n${next()}}\n`,
onDone,
rethrowIfPossible
});
}
}
Наконец, давайте используем картинку, чтобы обобщить идею генерации окончательного кода выполнения в части компиляции в целом: обобщить общий шаблон кода, разделить дифференцированные части на функции и выставить их наружу для реализации.
Суммировать
По сравнению с простым EventEmit, Tapable, как основная библиотека потока событий веб-пакета, предоставляет богатые события. Выполнение после запуска конечного события заключается в динамической генерации и выполнении кода, а затем его выполнении через новую функцию. По сравнению с нашим обычным прямым обходом или рекурсивным вызовом каждого события, этот метод выполнения относительно более эффективен. Хотя при написании кода в обычное время, для цикла, будь то отделять и писать каждый или напрямую для цикла, нет ничего с точки зрения эффективности, но для вебпака, потому что он управляется механизмом событий в целом, есть большое количество таких петель.логика. Тогда этот способ дизассемблирования и непосредственного выполнения каждой функции можно увидеть в своих преимуществах.