The webpack tapable

Recently, I studied webpack Loader and plugin compilation, and found that tapable was involved, so I had nothing to do and looked through the source code combined with the use method to record it

The original github.com/webpack/tap…

Hook type

Tapable provides Hook classes

Const {SyncHook, // Synchronizes hook execution from top to bottom, // synchronizes early exit hook execution from top to bottom, Stop SyncWaterfallHook when it encounters a registration function whose return value is not undefined. // Synchronize the waterfall hook from top to bottom, passing the return value to the next function SyncLoopHook, // Synchronize the loop hook from top to bottom, A function might be executed several times, AsyncParallelBailHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncParallelBailHook, AsyncParallelBailHook // async sequential hook AsyncSeriesBailHook, // AsyncSeriesWaterfallHook AsyncSeriesWaterfallHook = require("tapable");Copy the code

Ii. Construction project

Github.com/17139313271…

Three, hook use method

3.1 Synchronization hook -SyncHook

import { SyncHook } from '.. /table/lib'; const hook = new SyncHook(); / object/create hook hook. Tap (' logPlugin '() = > console. The log (' registered')); // the tap method registers the hook callback hook.call(); // The call method calls the hook and prints' checked 'Copy the code

3.2 Synchronizing early exit hooks -SyncBailHook

SyncBailHook is used to decide whether or not to go down based on the value returned by each step. If you return a non-undefined value, you will not go down. Note that if you return nothing, you will return undefined.

import { SyncBailHook } from '.. /table/lib'; const hook = new SyncBailHook(); Hook. Tap (' SyncBailHook1 '() = > console. The log (1 ` ` hook)); Hook. Tap ('SyncBailHook2', () => {console.log(' hook 2'); return 1}); Hook. Tap (' SyncBailHook3 '() = > console. The log (3 ` ` hook)); hook.call(); // Print 'hook 1', 'hook 2', 'hook 3'Copy the code

3.3 Synchronous waterfall hook -SyncWaterfallHook

Each step depends on the result of the previous step, that is, the value of the previous step’s return is the parameter of the next step.

import { SyncWaterfallHook } from '.. /table/lib'; const hook = new SyncWaterfallHook(["newSpeed"]); Hook. Tap ('SyncWaterfallHook1', (speed) => {console.log(' increment to ${speed} '); return speed + 100; }); Hook. Tap ('SyncWaterfallHook2', (speed) => {console.log(' increment to ${speed} '); return speed + 50; }); Hook. Tap ('SyncWaterfallHook3', (speed) => {console.log(' increment to ${speed} '); }); hook.call(50); // Print 'increase to 50' 'increase to 150' 'increase to 200'Copy the code

3.4 Synchronization Loop hook -SyncLoopHook

SyncLoopHook is a synchronous loop hook whose plugin returns a non-undefined hook. The plugin’s callback is executed until it returns undefined.

import { SyncLoopHook } from '.. /table/lib'; let index = 0; const hook = new SyncLoopHook(); Hook. Tap (' startPlugin1 '() = > {the console. The log (` execution `); if (index < 5) { index++; return 1; }}); Hook. Tap (' startPlugin2 '() = > {the console. The log (` ` 2); }); hook.call(); // Print 'execute' 6 times and 'execute 2' once.Copy the code

3.5 Asynchronous concurrent Hooks – AsyncParallelhooks

When all asynchronous tasks are completed, the following code is executed in the final callback

import { AsyncParallelHook } from '.. /table/lib'; const hook = new AsyncParallelHook(); Hook. TapAsync ('calculateRoutesPlugin1', (callback) => {setTimeout(() => {console.log(' async event 1'); callback(); }, 1000); }); Hook. TapAsync ('calculateRoutesPlugin2', (callback) => {setTimeout(() => {console.log(' async event 2'); callback(); }, 2000); }); Hook. CallAsync (() => {console.log(' final callback '); }); // Prints' asynchronous event 1 'at 1s. 'Asynchronous event 2' is printed at 2s. Then print 'Final callback'Copy the code

3.6 AsyncParallelBailHook -AsyncParallelBailHook

import { AsyncParallelBailHook } from '.. /table/lib'; const hook = new AsyncParallelBailHook(); Hook. TapAsync ('calculateRoutesPlugin1', (callback) => {setTimeout(() => {console.log(' async event 1'); callback(1); }, 1000); }); Hook. TapAsync ('calculateRoutesPlugin2', (callback) => {setTimeout(() => {console.log(' async event 2'); callback(); }, 2000); }); Hook. CallAsync ((result) => {console.log(' final callback ',result); }); // 'asynchronous event 1' is printed at 1s, followed by 'final callback', and 'asynchronous event 2' at 2s.Copy the code

3.7 Asynchronous Sequential Hook – AsyncSeriesHook

import { AsyncSeriesHook } from '.. /table/lib'; const hook = new AsyncSeriesHook(); hook.tapPromise('calculateRoutesPlugin1', () => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' async event 1'); resolve(); }, 1000); }); }); hook.tapPromise('calculateRoutesPlugin2', () => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' async event 2'); resolve(); }, 2000); }); }); Hook. Then (() => {console.log(' final callback '); }); After 1s, print asynchronous event 1, after 2s (not 2s, but 3s) print asynchronous event 2, and then immediately print the final callback.Copy the code

3.8 Asynchronous Sequential Early exit hook -AsyncSeriesBailHook

import { AsyncSeriesBailHook } from '.. /table/lib'; const hook = new AsyncSeriesBailHook(); hook.tapPromise('calculateRoutesPlugin1', () => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' async event 1'); resolve(1); }, 1000); }); }); hook.tapPromise('calculateRoutesPlugin2', () => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' async event 2'); resolve(); }, 2000); }); }); Hook. Then (() => {console.log(' final callback '); }); After 1s, print asynchronous event 1, print the final callback immediately, no longer execute asynchronous event 2.Copy the code

3.9 Asynchronous Sequential Waterfall hook -AsyncSeriesWaterfallHook

import { AsyncSeriesWaterfallHook } from '.. /table/lib'; const hook = new AsyncSeriesWaterfallHook(['args']); hook.tapPromise('calculateRoutesPlugin1', (result) => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' async event 1', result); resolve(result+1); }, 1000); }); }); hook.tapPromise('calculateRoutesPlugin2', (result) => { return new Promise((resolve, Reject) => {setTimeout(() => {console.log(' async event 2', result); resolve(result+2); }, 2000); }); }); Hook. Promise (12). Then ((result) => {console.log(' final callback '+ result); }); // // after 1s, print asynchronous event 1 12, after 2s print asynchronous event 2 13, and then print the final callback 15 immediately.Copy the code

Four, from the source code analysis hook implementation principle

4.1 Process Analysis

  1. Tapable source code uses factory and template modes. Hookcodefactory.js uses template mode to generate different execution functions according to different Hook functions and return _call().

  2. The registration process

    There are three tapable registrations: TAP, tapAsync, and tapPromise, but the process is basically the same, all calling _INSERT ()

    HOOK.js _insert(item) { this._resetCompilation(); let before; if (typeof item.before === "string") before = new Set([item.before]); else if (Array.isArray(item.before)) { before = new Set(item.before); } let stage = 0; if (typeof item.stage === "number") stage = item.stage; let i = this.taps.length; while (i > 0) { i--; const x = this.taps[i]; this.taps[i + 1] = x; const xStage = x.stage || 0; if (before) { if (before.has(x.name)) { before.delete(x.name); continue; } if (before.size > 0) { continue; } } if (xStage > stage) { continue; } i++; break; } this.taps[i] = item; Tap: {type: "sync", fn: fn,name: name} tapAsync: {type: "async", fn: fn ,name: name} tapPromise: { type: "promise", fn: fn ,name: The function execution after name} is executed according to the type of the registration. 2._insert () functions: different objects are generated according to the registration type and stored in TAPS []Copy the code
  3. Call the process

    3.1 Tapable can be called in three ways: Call, Promise and callAsync. CreateCompileDelegate () is called and the corresponding type is passed in

    Hook.js function createCompileDelegate(name, type) { return function lazyCompileHook(... args) { this[name] = this._createCall(type); console.log(this[name]) return this[name](... args); }; }... _createCall(type) { return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type }); }... The _createCall() function passes arguments to the compile() function of each hook, which is used to generate the callback functionCopy the code

    3.2 Each hook function inherits two functions:

A. conent() generates different execution functions according to different hook functions, namely _call()

B. compile(), which calls hookcodeFactory.js’s two functions setup() and create()

setup(instance, options) { instance._x = options.taps.map(t => t.fn); } // Filter out the registered functions stored in the optiuon array and assign values to the current hook function._X create(options) {this.init(options); switch (this.options.type) { case "sync": fn = new Function( this.args(), '"use strict"; \n' + this.header() + this.content({ onError: err => `throw ${err}; \n`, onResult: result => `return ${result}; \n`, resultReturns: true, onDone: () => "", rethrowIfPossible: true }) ); break; . } this.deinit(); return fn; } init(options) { this.options = options; this._args = options.args.slice(); } 1.init () this is very simple, but notice where _args comes from. This is the argument to lazyCompileHook (). 2. Create calls the content() of each hook function based on the type of the registered hook and generates the execution function fn.  anonymous() { "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; _fn0(); var _fn1 = _x[1]; // Multiple registration functions generate multiple _fn1(); }Copy the code

4.2 Implementation of SyncHook

// Content Function content({onError, onDone, rethrowIfPossible}) {return this.callTapsSeries({onError: (i, err) => onError(err), onDone, rethrowIfPossible }); } // The generated execution function anonymous() {"use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; _fn0(); var _fn1 = _x[1]; _fn1(); }Copy the code

SyncHook hooks generate multiple FN () functions that can be executed one by one when a call() is called

4.3 SyncBailHook

This hook determines whether to continue execution based on the return value. Its internal structure is slightly different from that of SyncHook:

// content function content({onError, onResult, resultReturns, 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`, resultReturns, onDone, rethrowIfPossible }); Function anonymous() {"use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; var _result0 = _fn0(); if(_result0 ! == undefined) {return _result0; } else { var _fn1 = _x[1]; var _result1 = _fn1(); if(_result1 ! == undefined) { return _result1; } else { var _fn2 = _x[2]; var _result2 = _fn2(); if(_result2 ! == undefined) { return _result2; } else {} } } })Copy the code

The onResult of SyncBailHook’s Content function has an additional criterion that determines whether to continue with the result of the previous hook function

4.4 SyncWaterfallHook

// Content Function content({onError, onResult, resultReturns, rethrowIfPossible}) {return this.callTapsSeries({onError: (i, err) => onError(err), onResult: (i, result, next) => { let code = ""; code += `if(${result} ! == undefined) {\n`; code += `${this._args[0]} = ${result}; \n`; // More assignment than SyncBailHook code += '}\n '; code += next(); return code; }, onDone: () => onResult(this._args[0]), doneReturns: resultReturns, rethrowIfPossible }); Function anonymous(newSpeed) {"use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; var _result0 = _fn0(newSpeed); if(_result0 ! == undefined) { newSpeed = _result0; } var _fn1 = _x[1]; var _result1 = _fn1(newSpeed); if(_result1 ! == undefined) { newSpeed = _result1; } var _fn2 = _x[2]; var _result2 = _fn2(newSpeed); if(_result2 ! == undefined) { newSpeed = _result2; } return newSpeed; })Copy the code

The onResult of SyncWaterfallHook’s content assigns more than SyncBailHook does to determine if there is a return value, and if there is a return value is passed to the next executing function

4.5 SyncLoopHook

// content content({ onError, onDone, rethrowIfPossible }) { return this.callTapsLooping({ onError: (i, err) => onError(err), onDone, rethrowIfPossible }); } // The generated function (function anonymous() {"use strict"; var _context; var _x = this._x; var _loop; do { _loop = false; var _fn0 = _x[0]; var _result0 = _fn0(); if(_result0 ! == undefined) {// Proceed with _loop = true; } else { var _fn1 = _x[1]; var _result1 = _fn1(); if(_result1 ! == undefined) { _loop = true; } else { if(! _loop) {} } } } while(_loop); }) 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"; } code += "var _loop; \n"; code += "do {\n"; code += "_loop = false; \n"; 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; },... }Copy the code

Note that SyncLoopHook’s content calls callTapsLooping(), whereas the other SyncLoopHook hooks call callTapsSeries().

4.6 AsyncParallelHook

// content function Content ({onError, onDone}) {return this.callTapsParallel - gradient ({onError: gradient gradient) (i, err, done, doneBreak) => onError(err) + doneBreak(true), onDone }); } function anonymous(_callback) {"use strict"; var _context; var _x = this._x; do { var _counter = 2; var _done = () => { _callback(); }; if(_counter <= 0) break; var _fn0 = _x[0]; _fn0(_err0 => {if(_err0) {// If (_counter > 0) {_callback(_err0); _counter = 0; } } else { if(--_counter === 0) _done(); }}); if(_counter <= 0) break; var _fn1 = _x[1]; _fn1(_err1 => { if(_err1) { if(_counter > 0) { _callback(_err1); _counter = 0; } } else { if(--_counter === 0) _done(); }}); } while(false); })Copy the code

The AsyncParallelHook generates an onError for err and executes a callback(). If there are no arguments, the AsyncParallelHook performs a callback() when _counter is set to 0.

4.7 AsyncParallelBailHook

  • // content content({ onError, onResult, onDone }) { let code = ""; code += `var _results = new Array(${this.options.taps.length}); \n`; code += "var _checkDone = () => {\n"; code += "for(var i = 0; i < _results.length; i++) {\n"; code += "var item = _results[i]; \n"; code += "if(item === undefined) return false; \n"; code += "if(item.result ! == undefined) {\n"; code += onResult("item.result"); code += "return true; \n"; code += "}\n"; code += "if(item.error) {\n"; code += onError("item.error"); code += "return true; \n"; code += "}\n"; code += "}\n"; code += "return false; \n"; code += "}\n"; code += this.callTapsParallel({ onError: (i, err, done, doneBreak) => { let code = ""; code += `if(${i} < _results.length && ((_results.length = ${i + 1}), (_results[${i}] = { error: ${err} }), _checkDone())) {\n`; code += doneBreak(true); code += "} else {\n"; code += done(); code += "}\n"; return code; }, onResult: (i, result, done, doneBreak) => { let code = ""; code += `if(${i} < _results.length && (${result} ! == undefined && (_results.length = ${i + 1}), (_results[${i}] = { result: ${result} }), _checkDone())) {\n`; code += doneBreak(true); code += "} else {\n"; code += done(); code += "}\n"; return code; }, onTap: (i, run, done, doneBreak) => { let code = ""; if (i > 0) { code += `if(${i} >= _results.length) {\n`; code += done(); code += "} else {\n"; } code += run(); if (i > 0) code += "}\n"; return code; }, onDone }); return code; } function anonymous(_callback) {"use strict"; var _context; var _x = this._x; var _results = new Array(2); var _checkDone = () => { for(var i = 0; i < _results.length; i++) { var item = _results[i]; if(item === undefined) return false; if(item.result ! == undefined) { _callback(null, item.result); return true; } if(item.error) { _callback(item.error); return true; } } return false; } do { var _counter = 2; var _done = () => { _callback(); }; if(_counter <= 0) break; var _fn0 = _x[0]; _fn0((_err0, _result0) => { if(_err0) { if(_counter > 0) { if(0 < _results.length && ((_results.length = 1), (_results[0] = { error: _err0 }), _checkDone())) { _counter = 0; } else { if(--_counter === 0) _done(); } } } else { if(_counter > 0) { if(0 < _results.length && (_result0 ! == undefined && (_results.length = 1), (_results[0] = { result: _result0 }), _checkDone())) { _counter = 0; } else { if(--_counter === 0) _done(); }}}}); if(_counter <= 0) break; if(1 >= _results.length) { if(--_counter === 0) _done(); } else { var _fn1 = _x[1]; _fn1((_err1, _result1) => { console.log('_err',_err1) if(_err1) { if(_counter > 0) { if(1 < _results.length && ((_results.length = 2), (_results[1] = { error: _err1 }), _checkDone())) { _counter = 0; } else { if(--_counter === 0) _done(); } } } else { if(_counter > 0) { if(1 < _results.length && (_result1 ! == undefined && (_results.length = 2), (_results[1] = { result: _result1 }), _checkDone())) { _counter = 0; } else { if(--_counter === 0) _done(); }}}}); } } while(false); })Copy the code

This hook is fused when the first plugin registered is executed, but it is not. This function is interesting because if err has an argument, it goes onError, and if there is no argument it goes onResult

callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) { .... switch (tap.type) { case "sync": case "async": let cbCode = ""; if (onResult) cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`; else cbCode += `_err${tapIndex} => {\n`; cbCode += `console.log('_err',_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": ..... } return code; } Callback () if(_counter > 0) {if(0 < 0) {if(0 < 0); _results.length && ((_results.length = 1), (_results[0] = { error: _err0 }), _checkDone())) { _counter = 0; } else { if(--_counter === 0) _done(); }}Copy the code

4.8 AsyncSeriesHook

//content content({ onError, onDone }) { return this.callTapsSeries({ onError: (i, err, next, doneBreak) => onError(err) + doneBreak(true), onDone }); } // The generated function (function anonymous() {"use strict"; return new Promise((_resolve, _reject) => { var _sync = true; function _error(_err) { if(_sync) _resolve(Promise.resolve().then(() => { throw _err; })); else _reject(_err); }; var _context; var _x = this._x; function _next0() { var _fn1 = _x[1]; var _hasResult1 = false; var _promise1 = _fn1(); if (! _promise1 || ! _promise1.then) throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise1 + ')'); _promise1.then(_result1 => { _hasResult1 = true; _resolve(); }, _err1 => { if(_hasResult1) throw _err1; _error(_err1); }); } var _fn0 = _x[0]; var _hasResult0 = false; var _promise0 = _fn0(); if (! _promise0 || ! _promise0.then) throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise0 + ')'); _promise0.then(_result0 => { _hasResult0 = true; _next0(); }, _err0 => { if(_hasResult0) throw _err0; _error(_err0); }); _sync = false; }); })Copy the code

AsyncSeriesHook calls the execution function generated by callTapsSeries. The principle is relatively simple, that is, when one promise is finished, another promise will be executed

4.9 AsyncSeriesBailHook

// content content({ onError, onResult, resultReturns, onDone }) { return this.callTapsSeries({ onError: (i, err, next, doneBreak) => onError(err) + doneBreak(true), onResult: (i, result, next) => `if(${result} ! == undefined) {\n${onResult( result )}; \n} else {\n${next()}}\n`, resultReturns, onDone }); } // The generated function (function anonymous() {"use strict"; return new Promise((_resolve, _reject) => { var _sync = true; function _error(_err) { if(_sync) _resolve(Promise.resolve().then(() => { throw _err; })); else _reject(_err); }; var _context; var _x = this._x; function _next0() { var _fn1 = _x[1]; var _hasResult1 = false; var _promise1 = _fn1(); if (! _promise1 || ! _promise1.then) throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise1 + ')'); _promise1.then(_result1 => { _hasResult1 = true; if(_result1 ! == undefined) { _resolve(_result1); ; } else { _resolve(); } }, _err1 => { if(_hasResult1) throw _err1; _error(_err1); }); } var _fn0 = _x[0]; var _hasResult0 = false; var _promise0 = _fn0(); if (! _promise0 || ! _promise0.then) throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise0 + ')'); _promise0.then(_result0 => { _hasResult0 = true; if(_result0 ! == undefined) { _resolve(_result0); ; } else { _next0(); } }, _err0 => { if(_hasResult0) throw _err0; _error(_err0); }); _sync = false; }); })Copy the code

This hook is the same as the SyncBailHook implementation, with one more step to determine the return value, and if there is a return value, it will be thrown

4.10 AsyncSeriesWaterfallHook

content({ onError, onResult, onDone }) { return this.callTapsSeries({ onError: (i, err, next, doneBreak) => onError(err) + doneBreak(true), onResult: (i, result, next) => { let code = ""; code += `if(${result} ! == undefined) {\n`; code += `${this._args[0]} = ${result}; \n`; code += `}\n`; code += next(); return code; }, onDone: () => onResult(this._args[0]) }); } // Generate function (function anonymous(home) {"use strict"; return new Promise((_resolve, _reject) => { var _sync = true; function _error(_err) { if(_sync) _resolve(Promise.resolve().then(() => { throw _err; })); else _reject(_err); }; var _context; var _x = this._x; function _next0() { var _fn1 = _x[1]; var _hasResult1 = false; var _promise1 = _fn1(home); if (! _promise1 || ! _promise1.then) throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise1 + ')'); _promise1.then(_result1 => { _hasResult1 = true; if(_result1 ! == undefined) { home = _result1; } _resolve(home); }, _err1 => { if(_hasResult1) throw _err1; _error(_err1); }); } var _fn0 = _x[0]; var _hasResult0 = false; var _promise0 = _fn0(home); if (! _promise0 || ! _promise0.then) throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise0 + ')'); _promise0.then(_result0 => { _hasResult0 = true; if(_result0 ! == undefined) { home = _result0; } _next0(); }, _err0 => { if(_hasResult0) throw _err0; _error(_err0); }); _sync = false; }); })Copy the code

As with SyncWaterfallHook, the multi-step assignment process