preface
This article officially entered the source code interpretation.
Overall, Tapable source code is not complex, the core of the two classes Hook and HookCodeFactory.
Taking SyncHook as an example, let’s take a look at its internal implementation, gradually introducing the internal implementation logic of Hook and HookCodeFactory.
Finally, two helper classes, HookMap and MultiHook.
The source code
Take SyncHook as an example and look at its implementation.
SyncHook
The implementation of this class is in the tapable /lib/syncook.js file.
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) = >onError(err), onDone, rethrowIfPossible }); }}const factory = new SyncHookCodeFactory();
const TAP_ASYNC = () = > {
throw new Error("tapAsync is not supported on a SyncHook");
};
const TAP_PROMISE = () = > {
throw new Error("tapPromise is not supported on a SyncHook");
};
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = SyncHook;
hook.tapAsync = TAP_ASYNC;
hook.tapPromise = TAP_PROMISE;
hook.compile = COMPILE;
return hook;
}
SyncHook.prototype = null;
module.exports = SyncHook;
Copy the code
The overall logical
- Define the SyncHookCodeFactory class and inherit from HookCodeFactory, which is a class that generates executable code
- Next, instantiate the SyncHookCodeFactory
- Then define the TAP_ASYNC, TAP_PROMISE, and COMPILE methods. Because SyncHook is synchronous and does not have asynchronous subscriptions, an error is thrown when using tapAsync and tapPromise subscriptions
- Define a synchronous SyncHook function and export it
SyncHookMethod internal logic
- Instantiate a Hook with a new Hook
- Then modify the constructor object of the instantiated object
- Override the tapAsync and tapPromise methods for instantiating objects
- Override the compile method of instantiated objects
- Returns the instantiated object
Let’s ignore the Hook and HookCodeFactory classes, the logic of the entire file is a complete routine.
The other hooks are implemented in the same way, the only difference being the implementation of the classes that inherit the HookCodeFactory and their respective Content methods.
Such as AsyncSeriesLoopHook implementation is as follows: the code in a file tapable/lib/AsyncSeriesLoopHook js
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");
class AsyncSeriesLoopHookCodeFactory extends HookCodeFactory {
content({ onError, onDone }) {
return this.callTapsLooping({
onError: (i, err, next, doneBreak) = > onError(err) + doneBreak(true), onDone }); }}const factory = new AsyncSeriesLoopHookCodeFactory();
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
function AsyncSeriesLoopHook(args = [], name = undefined) {
const hook = new Hook(args, name);
hook.constructor = AsyncSeriesLoopHook;
hook.compile = COMPILE;
hook._call = undefined;
hook.call = undefined;
return hook;
}
AsyncSeriesLoopHook.prototype = null;
module.exports = AsyncSeriesLoopHook;
Copy the code
AsyncSeriesLoopHook is an asynchronous method, so resetting the call and _call methods on its instantiated object means it cannot be called using the call method.
Identify two points from the source code
Looking at the source code can determine two points:
- Synchronous hooks cannot be subscribed with tapAsync and tapPromise because these two methods are reset to:
const TAP_ASYNC = () = > {
throw new Error("tapAsync is not supported on a SyncHook");
};
const TAP_PROMISE = () = > {
throw new Error("tapPromise is not supported on a SyncHook");
};
Copy the code
- An asynchronous Hook cannot be called through the call method because it is reset to:
hook._call = undefined;
hook.call = undefined;
Copy the code
Hook type
This class is defined in the tapable /lib/hook.js file.
const CALL_DELEGATE = function(. args) {
this.call = this._createCall("sync");
return this.call(... args); };const CALL_ASYNC_DELEGATE = function(. args) {
this.callAsync = this._createCall("async");
return this.callAsync(... args); };const PROMISE_DELEGATE = function(. args) {
this.promise = this._createCall("promise");
return this.promise(... args); };class Hook {
constructor(args = [], name = undefined) {
this._args = args;
this.name = name;
this.taps = [];
this.interceptors = [];
this._call = CALL_DELEGATE;
this.call = CALL_DELEGATE;
this._callAsync = CALL_ASYNC_DELEGATE;
this.callAsync = CALL_ASYNC_DELEGATE;
this._promise = PROMISE_DELEGATE;
this.promise = PROMISE_DELEGATE;
this._x = undefined;
this.compile = this.compile;
this.tap = this.tap;
this.tapAsync = this.tapAsync;
this.tapPromise = this.tapPromise;
}
compile(options) {
throw new Error("Abstract: should be overridden");
}
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
_tap(type, options, fn) {
if (typeof options === "string") {
options = {
name: options.trim()
};
} else if (typeofoptions ! = ="object" || options === null) {
throw new Error("Invalid tap options");
}
if (typeofoptions.name ! = ="string" || options.name === "") {
throw new Error("Missing name for tap");
}
if (typeofoptions.context ! = ="undefined") {
deprecateContext();
}
options = Object.assign({ type, fn }, options);
options = this._runRegisterInterceptors(options);
this._insert(options);
}
tap(options, fn) {
this._tap("sync", options, fn);
}
tapAsync(options, fn) {
this._tap("async", options, fn);
}
tapPromise(options, fn) {
this._tap("promise", options, fn);
}
_runRegisterInterceptors(options) {
for (const interceptor of this.interceptors) {
if (interceptor.register) {
const newOptions = interceptor.register(options);
if(newOptions ! = =undefined) { options = newOptions; }}}return options;
}
withOptions(options) {
const mergeOptions = opt= >
Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);
return {
name: this.name,
tap: (opt, fn) = > this.tap(mergeOptions(opt), fn),
tapAsync: (opt, fn) = > this.tapAsync(mergeOptions(opt), fn),
tapPromise: (opt, fn) = > this.tapPromise(mergeOptions(opt), fn),
intercept: interceptor= > this.intercept(interceptor),
isUsed: () = > this.isUsed(),
withOptions: opt= > this.withOptions(mergeOptions(opt))
};
}
isUsed() {
return this.taps.length > 0 || this.interceptors.length > 0;
}
intercept(interceptor) {
this._resetCompilation();
this.interceptors.push(Object.assign({}, interceptor));
if (interceptor.register) {
for (let i = 0; i < this.taps.length; i++) {
this.taps[i] = interceptor.register(this.taps[i]); }}}_resetCompilation() {
this.call = this._call;
this.callAsync = this._callAsync;
this.promise = this._promise;
}
_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; }}Copy the code
In the constructor, we define the tap, tapAsync, and tapPromise methods, which are the methods we use when we subscribe to the function.
All three methods call the this._tap private method on the prototype chain, the only difference being that the first arguments sync, Async, and PROMISE represent different subscription function types.
_tap method
_tap(type, options, fn) {
if (typeof options === "string") {
options = {
name: options.trim()
};
} else if (typeofoptions ! = ="object" || options === null) {
throw new Error("Invalid tap options");
}
if (typeofoptions.name ! = ="string" || options.name === "") {
throw new Error("Missing name for tap");
}
if (typeofoptions.context ! = ="undefined") {
deprecateContext();
}
options = Object.assign({ type, fn }, options);
options = this._runRegisterInterceptors(options);
this._insert(options);
}
Copy the code
The main function of the private _TAP method is to serialize options and add them to the TAPS array.
Implementation logic:
- Check the type of the options parameter. Options is ultimately an object and must have the name attribute
- If options has a context property, the deprecateContext method is called, indicating that the context property is about to be discarded
- Call the assign method to merge options so that it has the name, type, and FN attributes
- Call the private _runRegisterInterceptors method to modify options
- The private method _insert is called to adjust the order of the subscription functions stored in the TAPS array
_runRegisterInterceptors method
_runRegisterInterceptors(options) {
for (const interceptor of this.interceptors) {
if (interceptor.register) {
const newOptions = interceptor.register(options);
if(newOptions ! = =undefined) { options = newOptions; }}}return options;
}
Copy the code
The main function of the private _runRegisterInterceptors method is to modify the Options object.
Implementation logic
- Iterates through all interceptors, calling the register property method if it has one, passing the original options as an argument, and modifying options if it does not return undefined. The options modified by the last interceptor register is the final options
The register property methods of all interceptors have the ability to modify the Tap object of the subscription function.
_insert method
_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;
}
Copy the code
The main function of the private method _run is to adjust the order of subscription functions based on the value of before or stage, which is ultimately stored in the TAPS array.
Implementation logic
- Let’s convert before to Set
- Check that stage is assigned to the variable stage if it is of a numeric type
- fromtapsStart traversal at the end
- Let I –
- Get the value x of position I
- I’m assigning x to I plus 1, so I’m moving x back one position
- Get the stages of x
- If you havebefore
- Determine if x’s name exists before, i.e. the current item to be inserted before X, continue, and then execute while
- If there is no before, determine whether the stage of x is greater than the stage of item, and continue if it is
- I++ to close the loop, and that’s where the index should be inserted
- Put item in place of I
Here’s the logic:
Call, callAsync, and Promise methods
All three methods call the _createCall private method and then reassign the return value to the corresponding method.
Call method for example
this.call = CALL_DELEGATE;
Copy the code
const CALL_DELEGATE = function(. args) {
this.call = this._createCall("sync");
return this.call(... args); };Copy the code
_createCall method
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
Copy the code
This method calls the compile method.
Back at the beginning of the article, each Hook class overwrites the compile method after instantiation, which goes into HookCodeFactory logic.
HookCodeFactory class
The sample code
Let’s take a look at how the HookCodeFactory logic is entered internally using the following example.
const {
SyncHook
} = require('tapable');
const hook = new SyncHook(['name'.'age']);
hook.tap({
name: 'js'
}, (name, age) = > {
console.log('js', name, age);
});
hook.tap({
name: 'css'
}, (name, age) = > {
console.log('css', name, age);
});
hook.tap({
name: 'node'
}, (name, age) = > {
console.log('node', name, age);
});
hook.call('naonao'.2);
Copy the code
In the previous article has been the implementation of hook. Tap, source code analysis.
Let’s look at hook.call execution.
In the class Hook constructor there are:
This call is defined
this.call = CALL_DELEGATE;
Copy the code
CALL_DELEGATE definition
const CALL_DELEGATE = function(. args) {
this.call = this._createCall("sync");
return this.call(... args); };Copy the code
_createCall definition:
_createCall(type) {
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}
Copy the code
That what time is it
This place makes a few points:
- Taps stores something like this:
[{type: 'sync'.fn: f, name: 'js'},
{type: 'sync'.fn: f, name: 'css'},
{type: 'sync'.fn: f, name: 'node'}]Copy the code
- This. Interceptors stores an array of interceptor related objects
- This._args is the args passed in when the class is instantiated, like this:
['name'.'age']
Copy the code
- Type is one of sync, Async, or Promise
This.com running the definition
The this.pile method is overridden after each Hook is instantiated.
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
function SyncHook(args = [], name = undefined) {
const hook = new Hook(args, name);
// omit some code...
hook.compile = COMPILE;
return hook;
}
Copy the code
COMPILEInternal logic
- Call the setup method
- Call the create method and return
The result returned by the create method is eventually assigned to this.call of the CALL_DELEGATE method, which is then executed.
This completes the execution logic of hook. Call.
Let’s look at the setup and create methods executed inside the COMPILE method.
setup
The setup method is defined in HookCodeFactory as follows:
setup(instance, options) {
instance._x = options.taps.map(t= > t.fn);
}
Copy the code
It maps taps, pulls out fn, and assigns a value to the _x property of the current Hook instance.
create
The create method is defined in HookCodeFactory as follows:
create(options) {
this.init(options);
let fn;
switch (this.options.type) {
case "sync":
fn = new Function(
this.args(),
'"use strict"; \n' +
this.header() +
this.contentWithInterceptors({
onError: err= > `throw ${err}; \n`.onResult: result= > `return ${result}; \n`.resultReturns: true.onDone: () = > "".rethrowIfPossible: true}));break;
case "async":
fn = new Function(
this.args({
after: "_callback"
}),
'"use strict"; \n' +
this.header() +
this.contentWithInterceptors({
onError: err= > `_callback(${err}); \n`.onResult: result= > `_callback(null, ${result}); \n`.onDone: () = > "_callback(); \n"}));break;
case "promise":
let errorHelperUsed = false;
const content = this.contentWithInterceptors({
onError: err= > {
errorHelperUsed = true;
return `_error(${err}); \n`;
},
onResult: result= > `_resolve(${result}); \n`.onDone: () = > "_resolve(); \n"
});
let code = "";
code += '"use strict"; \n';
code += this.header();
code += "return new Promise((function(_resolve, _reject) {\n";
if (errorHelperUsed) {
code += "var _sync = true; \n";
code += "function _error(_err) {\n";
code += "if(_sync)\n";
code +=
"_resolve(Promise.resolve().then((function() { throw _err; }))); \n";
code += "else\n";
code += "_reject(_err); \n";
code += "}; \n";
}
code += content;
if (errorHelperUsed) {
code += "_sync = false; \n";
}
code += "})); \n";
fn = new Function(this.args(), code);
break;
}
this.deinit();
return fn;
}
Copy the code
It concatenates different code strings based on type and creates a Function via new Function, assigns a value to fn, and returns fn.
So, previously, printing out the hook. Call method was completely transparent about the order and flow of subscription functions.
sync
Type is the case branch of sync:
case "sync":
fn = new Function(
this.args(),
'"use strict"; \n' +
this.header() +
this.contentWithInterceptors({
onError: err= > `throw ${err}; \n`.onResult: result= > `return ${result}; \n`.resultReturns: true.onDone: () = > "".rethrowIfPossible: true}));break;
Copy the code
The main logic is as follows:
- Call the this.args method to get the function parameters
- The string that starts with “use strict” is the body of the concatenation function
Enclosing the args
args({ before, after } = {}) {
let allArgs = this._args;
if (before) allArgs = [before].concat(allArgs);
if (after) allArgs = allArgs.concat(after);
if (allArgs.length === 0) {
return "";
} else {
return allArgs.join(","); }}Copy the code
Concatenate the this._args array into a string according to **, **
This header method
header() {
let code = "";
if (this.needContext()) {
code += "var _context = {}; \n";
} else {
code += "var _context; \n";
}
code += "var _x = this._x; \n";
if (this.options.interceptors.length > 0) {
code += "var _taps = this.taps; \n";
code += "var _interceptors = this.interceptors; \n";
}
return code;
}
Copy the code
The main logic
- Call the this.needContext method to determine whether a context object is needed. Define context as an empty object if necessary. The taps function determines whether any member in this. Taps has context: true configured, i.e. {type: ‘sync’, fn: f, name: ‘js’, context: true}.
- Define cache _x is this._x, which is an array of the subscription function fn
- Check if there are interceptors, if there are _TAPS and _interceptors that define cached responses.
- Returns the concatenated string code
Enclosing contentWithInterceptors method
contentWithInterceptors(options) {
if (this.options.interceptors.length > 0) {
const onError = options.onError;
const onResult = options.onResult;
const onDone = options.onDone;
let code = "";
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.call) {
code += `The ${this.getInterceptor(i)}.call(The ${this.args({
before: interceptor.context ? "_context" : undefined
})}); \n`;
}
}
code += this.content(
Object.assign(options, {
onError:
onError &&
(err= > {
let code = "";
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.error) {
code += `The ${this.getInterceptor(i)}.error(${err}); \n`;
}
}
code += onError(err);
return code;
}),
onResult:
onResult &&
(result= > {
let code = "";
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.result) {
code += `The ${this.getInterceptor(i)}.result(${result}); \n`;
}
}
code += onResult(result);
return code;
}),
onDone:
onDone &&
(() = > {
let code = "";
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.done) {
code += `The ${this.getInterceptor(i)}.done(); \n`;
}
}
code += onDone();
returncode; })}));return code;
} else {
return this.content(options); }}Copy the code
- First of all, there is no logic without an interceptor, else logic, which returns this.content directly.
This. content is defined in
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) = >onError(err), onDone, rethrowIfPossible }); }}Copy the code
You can see that it calls this.callTapsSeries and returns the result.
- Take a look at the situation with interceptors:
- For loops through all interceptors and concatenates a string for each interceptor to execute the Call method if the call property method is configured
- The this.content method is still executed, except that the arguments are the result of the original options and the concatenation of each error, result, and done executable code string on the interceptor
callTapsSeries
callTapsSeries({ onError, onResult, resultReturns, onDone, doneReturns, rethrowIfPossible }) {
if (this.options.taps.length === 0) return onDone();
const firstAsync = this.options.taps.findIndex(t= >t.type ! = ="sync");
const somethingReturns = resultReturns || doneReturns;
let code = "";
let current = onDone;
let unrollCounter = 0;
for (let j = this.options.taps.length - 1; j >= 0; j--) {
const i = j;
constunroll = current ! == onDone && (this.options.taps[i].type ! = ="sync" || unrollCounter++ > 20);
if (unroll) {
unrollCounter = 0;
code += `function _next${i}() {\n`;
code += current();
code += `}\n`;
current = () = > `${somethingReturns ? "return " : ""}_next${i}(a); \n`;
}
const done = current;
const doneBreak = skipDone= > {
if (skipDone) return "";
return onDone();
};
const content = this.callTap(i, {
onError: error= > onError(i, error, done, doneBreak),
onResult:
onResult &&
(result= > {
return onResult(i, result, done, doneBreak);
}),
onDone: !onResult && done,
rethrowIfPossible:
rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
});
current = () = > content;
}
code += current();
return code;
}
Copy the code
The main function is to traverse TAPS in reverse order, ignoring asynchronous cases first.
Focus on the this.callTap section, which assigns the return result to the content and then resets current to () => content; This takes advantage of the closure’s properties to cache the string that was last concatenated.
Each time the loop assigns current to done, the done method is executed inside the this.callTap call, concatenating its results in place.
Finally return the concatenated string code.
callTap
callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
let code = "";
let hasTapCached = false;
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} = The ${this.getTap(tapIndex)}; \n`;
hasTapCached = true;
}
code += `The ${this.getInterceptor(i)}.tap(${
interceptor.context ? "_context, " : ""
}_tap${tapIndex}); \n`;
}
}
code += `var _fn${tapIndex} = The ${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}(The ${this.args({
before: tap.context ? "_context" : undefined
})}); \n`;
} else {
code += `_fn${tapIndex}(The ${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 += `(function(_err${tapIndex}, _result${tapIndex}) {\n`;
else cbCode += `(function(_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}(The ${this.args({
before: tap.context ? "_context" : undefined,
after: cbCode
})}); \n`;
break;
case "promise":
code += `var _hasResult${tapIndex}= false; \n`;
code += `var _promise${tapIndex} = _fn${tapIndex}(The ${this.args({
before: tap.context ? "_context" : undefined
})}); \n`;
code += `if (! _promise${tapIndex}| |! _promise${tapIndex}.then)\n`;
code += ` throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex}+ ') '); \n`;
code += `_promise${tapIndex}.then((function(_result${tapIndex}) {\n`;
code += `_hasResult${tapIndex}= true; \n`;
if (onResult) {
code += onResult(`_result${tapIndex}`);
}
if (onDone) {
code += onDone();
}
code += `}), function(_err${tapIndex}) {\n`;
code += `if(_hasResult${tapIndex}) throw _err${tapIndex}; \n`;
code += onError(`_err${tapIndex}`);
code += "}); \n";
break;
}
return code;
}
Copy the code
It begins by traversing the interceptor and concatenating its execution string code if it has a TAP property method.
So each subscription function is executed by executing the interceptor’s TAP method.
Var _fn${tapIndex} var _fn0 = _x[0]
Then determine the current tap type and enter the different branches.
Take a look at the Sync branch here.
- If rethrowIfPossible is false, concatenate ** _hasError** and try
- In the case of onResult, the result of the concatenation of whether the subscription function needs to be cached is __result, because some hooks need to proceed based on the result
- Again, in the case of rethrowIfPossible, concatenate the catch part, so try… Catch is complete, and step 2 is just try
- Then, according to the case of onResult, pass the cached execution result as a parameter, concatenate if… The else. For example, SyncBailHook’s onResult is:
class SyncBailHookCodeFactory extends HookCodeFactory {
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 }); }}Copy the code
From this onResult we can concatenate the string code that determines that if the result is not undefined, next will not be executed, that is, it will not be executed down. 5. Execute onDone as current in callTapsSeries, which caches the string code from the last subscription function to perform concatenation 6. Finally, close the code block according to rethrowIfPossible
So that’s the whole splicing process.
Concatenated string code that eventually creates a new Function from new Function and assigns it to Call.
TapAsync and Promise are also concatenated according to this process, but the specific concatenated code is different. Refer to async and Promise in the branch for details.
HookMap
The HookMap class maps to a Hook based on a string. Internal is stored in a Map object according to the key, when used according to the specific key value can obtain the corresponding Hook.
class HookMap {
constructor(factory, name = undefined) {
this._map = new Map(a);this.name = name;
this._factory = factory;
this._interceptors = [];
}
get(key) {
return this._map.get(key);
}
for(key) {
const hook = this.get(key);
if(hook ! = =undefined) {
return hook;
}
let newHook = this._factory(key);
const interceptors = this._interceptors;
for (let i = 0; i < interceptors.length; i++) {
newHook = interceptors[i].factory(key, newHook);
}
this._map.set(key, newHook);
return newHook;
}
intercept(interceptor) {
this._interceptors.push(
Object.assign(
{
factory: defaultFactory }, interceptor ) ); }}Copy the code
These are the set and get operations on the Map.
MultiHook
A MultiHook is one that defines many hooks to be stored in an array. When using TAP or tapAsync or tapPromise, the group of numbers is traversed and the corresponding TAP or tapAsync or tapPromise methods are called for each member.
This is to define a subscription function that can be distributed to many hooks.
class MultiHook {
constructor(hooks, name = undefined) {
this.hooks = hooks;
this.name = name;
}
tap(options, fn) {
for (const hook of this.hooks) { hook.tap(options, fn); }}tapAsync(options, fn) {
for (const hook of this.hooks) { hook.tapAsync(options, fn); }}tapPromise(options, fn) {
for (const hook of this.hooks) { hook.tapPromise(options, fn); }}isUsed() {
for (const hook of this.hooks) {
if (hook.isUsed()) return true;
}
return false;
}
intercept(interceptor) {
for (const hook of this.hooks) { hook.intercept(interceptor); }}withOptions(options) {
return new MultiHook(
this.hooks.map(h= > h.withOptions(options)),
this.name ); }}Copy the code
conclusion
Tapable source code analysis is finished, for callAsync and promise method concatenation process, interested in you can take a look.
Overall, Tapable’s source code is not complex, but it is the core of the Webpack plug-in, and the ideas are worth learning.
More exciting, please pay attention to the wechat public number: make a noise front end