preface

As I mentioned in my last post, professional interviewers may have to soul test Webpack’s core dependency on Tapable. I, as an interviewer, naturally have to master this soul test technique. After all, I am a professional interviewer 😀, but after reading tapable’s source code, my mood is like this. What the hell is this?

The body of the

Shiny Webpack has a homely and even a little ugly heart, Tapable

Tapable is a small library, but it is at the heart of the Webpack plugin mechanism. Tapable means that, like a water pipe, Webpack wraps all the build process into plugins that are plugged into a slot called a Hook. Because of the asynchronous nature of JavaScript, hooks are divided into SyncHook and AsyncHook. As the name implies, it depends on whether your plug-in is synchronous or asynchronous. In addition, Tapable also supports promise-style plug-ins.

README. Md is fine. First of all, Tapable is not a traditional publish/subscribe model, which might not be what we expect when we hear about event mechanics, but it is. If you want to define a pattern, I think it’s more like a pipeline pattern.

Taps → taps[] → taps[] → Taps [] → Taps [] → Taps [] → Taps [] → Taps [] → Taps [] → Taps [] → Taps [] → Taps [] → Taps [] there is no event name in the Event model, there is no API like ON, or listening, you can think of it as a plug-in queue, with various life cycles in the Webpack. These life cycles are essentially hook pipes, look at some source code to feel

			/ * *@type {SyncBailHook<[Compilation], boolean>} * /
			shouldEmit: new SyncBailHook(["compilation"]),
			/ * *@type {AsyncSeriesHook<[Stats]>} * /
			done: new AsyncSeriesHook(["stats"]),
			/ * *@type {SyncHook<[Stats]>} * /
			afterDone: new SyncHook(["stats"]),
			/ * *@type {AsyncSeriesHook<[]>} * /
			additionalPass: new AsyncSeriesHook([]),
			/ * *@type {AsyncSeriesHook<[Compiler]>} * /
			beforeRun: new AsyncSeriesHook(["compiler"]),
			/ * *@type {AsyncSeriesHook<[Compiler]>} * /
			run: new AsyncSeriesHook(["compiler"]),
			/ * *@type {AsyncSeriesHook<[Compilation]>} * /
			emit: new AsyncSeriesHook(["compilation"]),
			/ * *@type {AsyncSeriesHook<[string, AssetEmittedInfo]>} * /
			assetEmitted: new AsyncSeriesHook(["file"."info"]),
			/ * *@type {AsyncSeriesHook<[Compilation]>} * /
			afterEmit: new AsyncSeriesHook(["compilation"]),
Copy the code

First of all, these hooks do not share state. For the beforeRun Hook, multiple plugins can be executed on the Hook, and these plugins are essentially tap by tap, just like water in a pipe, flowing in drop by drop. Plug-ins run in several ways

  • Synchronous execution, normal queue
  • Parallel execution, Parallel
  • Series, the Series

There are several models on top of this

  • Bail
  • WaterFall
  • Loop (this is not actually implemented)

If it is synchronous, it is a normal function. If it is asynchronous, it is a callback. You pass in your own callback when you hook. It’s just a chain call

Of course this section I introduce tapable detailed design is secondary…. I’m mainly here to tease, to look at a piece of code

_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

What does this code do? Insert tap…… into taps array If this method seems just a little wacky, let me show you how it works when you hook. Call tapable and you know I’m not saying ugly for nothing

Every call of the app is symptomatic of tapable’s effort to do something about it

For Webpack, any plugin has an apply method, and you can use various hooks in the Compiler to build the logic inside the plugin, but do you know how the logic on the hooks you write is executed by tapable?

Before reading the Tapable source code, I was naive to guess, intuitive should be

Hook. Call (x=>{some plugin logic}), and then Webpack calls my logic at the stage of the hook, probably using something like function.call(… Args), this link is basically

hook.call(function) → Proxy function (function.call(args))
Copy the code

Then when I got dizzy reading tapable’s logic, his link looked like this

hook.call(function) -CALL_DELEGATE → this.call = this._createCall →
this.compile() -factory.setup → factory.create() -codeGenerate logic 1,2,3, →new Function → this.call(args)
Copy the code

There’s a lot of detail in this, and if you’re not busy, you’re talking to me about the boiling fish players, you can read this logic, and it will give you a sense of what factory mode is, what archetypal inheritance is, how call is used, and how to understand this, And have a higher level of understanding of JavaScript object-oriented programming than you want to give up.

I was thinking that I could take the tapable source code and let them read it when interviewing students for college recruitment. I just wanted to know whether the logical thinking and patience of young people can withstand the torture of the soul

I don’t know if Webpack compilation is slow because tapable is a package code that dynamically generates hooks. After all, there are so many hooks, a bunch of plugins, a bunch of logic in plugins, how can we compile hundreds of new functions at a time? Just to give you a taste of tapable’s code.

				if (errorHelperUsed) {
					code += "var _sync = true; \n";
					code += "function _error(_err) {\n";
					code += "if(_sync)\n";
					code += "_resolve(Promise.resolve().then(() => { throw _err; })); \n";
					code += "else\n";
					code += "_reject(_err); \n";
					code += "}; \n";
				}
Copy the code

I don’t know how the author fixes the bug, but if the author asks me to fix the bug, I’ll swear and fix it at the same time.

Tapable’s overall design is commendable

Having teased the implementation, aside from the endless details of the design, I’d like to say that tapable’s overall design is worth applauds. Tapable’s pipe mode is obviously much more controllable than the publish/subscribe mode, and its completely independent slots provide greater stability. And the magic strings that don’t rely on event names also avoid spelling surprises. If you need an alternative pipeline to publish/subscribe, consider tapable’s overall design, which I’ve summarized

  • The Hook class manages taps and Interceptors
  • The HookCodeFactory class is responsible for the code that wraps the logic into pipe executable
  • Subclasses inherit from the two parent classes and write some custom logic

But I think we should really refactor the code… Tapable, as the core library of Webpack, is still a facade in terms of performance and readability, and the impact of dynamic wrapping of code on performance is also debatable. The pipeline mode may have elegant and higher performance detailed design and implementation, and there is room for refactoring both in terms of data structure and algorithm.

The latter

If you’re an energetic layman, I really suggest you try to refactor Tapable so that you can sign off on it once the Webpack passes with a PR

I’m the man who rewrote the heart of Webpack