陌小路的个人博客 陌小路的个人博客
首页
  • 技术专区

    • 面试
    • Vue
    • Electron
    • TypeScript
    • Serverless
    • GraphQL
  • 我的秋招之旅
  • 2019年终总结
Todo
收藏夹
关于作者
GitHub

陌小路

前端切图仔
首页
  • 技术专区

    • 面试
    • Vue
    • Electron
    • TypeScript
    • Serverless
    • GraphQL
  • 我的秋招之旅
  • 2019年终总结
Todo
收藏夹
关于作者
GitHub
  • Vue

  • React

  • 面试

  • Electron

  • Serverless

  • GraphQL

  • TypeScript

  • RxJS

  • 工程化

  • Webpack

    • Module Federation
    • 深入理解 Tapable
      • 概览
      • tapable hooks
        • 返回值处理方式分类
        • 执行任务机制分类
      • 上手实践
        • 如何使用 tapable
        • 实现一个简易 tapable
      • Tapable 原理分析
        • 入口函数
        • Hook 类
    • 深入探索 Webpack 插件机制
  • Nestjs

  • WebRTC & P2P

  • Docker

  • Linux

  • Git

  • Svelte

  • 踩坑日记 & 小Tips

  • 其他

  • technology
  • Webpack
陌小路
2022-07-28

深入理解 Tapable

# 深入理解 Webpack 中的 Tapable

# 概览

其实稍微有了解过一些 Webpack 插件机制的同学应该会知道,Webpack 整个构建流程中,内部都是基于 tapable 做整体的事件流管控,通过 tapable 实现 Webpack 各个阶段之间的串联,同时也为开发者插件编写提供了十分丝滑的接入方案。那首先我们可以先简单了解下 tapable 包含了哪些 hooks。

# tapable hooks

# 返回值处理方式分类

从主要执行任务之后对待任务返回值的态度来看来看,可以这么区分:

# 普通

  • SyncHook:同步串行
  • AsyncSeriesHook:异步串行
  • AsyncParallelHook:异步并行

按顺序执行一个个事件函数,不处理也不关心函数返回值。

# Bail - 熔断机制

  • SyncBailHook:同步串行
  • AsyncSeriesBailHook:异步串行
  • AsyncParallelBailHook:异步并行

按顺序执行一个个事件函数,一旦有一个函数返回值不为 undefined 则直接结束。

# Water - 瀑布流机制

  • SyncWaterfallHook:同步串行
  • AsyncSeriesWaterHook:异步串行

瀑布流形式,将上一个函数的返回值作为当前函数的入参。

# Loop - 循环机制

  • SyncLoopHook:同步循环执行

执行任务队列时候,只要其中一个返回非 undefined 就会重新开始从一个开始执行,直到所有任务都返回 undefined 。

# 执行任务机制分类

函数用途介绍如上

# 同步 hooks

  • SyncHook
  • SyncBailHook
  • SyncWaterfallHook
  • SyncLoopHook

# 异步串行 hooks

  • AsyncSeriesHook
  • AsyncSeriesBailHook
  • AsyncSeriesWaterHook

# 异步并行 hooks

  • AsyncParallelHook
  • AsyncParallelBailHook

# 上手实践

# 如何使用 tapable

import { SyncHook } from 'tapable';

const instance = new SyncHook(['mxl']);

instance.tap('func1', (...args) => {
  console.log('func1', ...args);
})

instance.tap('func2', (...args) => {
  console.log('func2', ...args);
})

instance.call('first call');

// output:
// func1 first call
// func2 first call
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

使用方式上非常简单,选择你想要使用的 hook,然后实例化一下,就可以开始使用它的相关机制了。

其实上述用法来看,它很像我们常说的发布订阅模式,实例化出来的这个对象就类似于一个事件管理中心,你可以使用 tap 函数来订阅一个事件,在合适的时候,调用 call 就能按照你选的 hook 运行机制进行依次执行了。

# 实现一个简易 tapable

type ITask = (...args: unknown[]) => void;

export class SyncHook{
  public taps: ITask[] = []

  constructor(public args: unknown[]) {}

  // 订阅
  tap(name: string, fn: ITask) {
    this.taps.push(fn);
  }

  // 执行
  call(...args: unknown[]){
    this.taps.forEach(tap => tap(...args));
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

一个简单的发布订阅模式其实就只需要一个函数用来收集,并提供另一个函数来进行批量执行就好了。

# Tapable 原理分析

这里就以 SyncHook 来进行解析。

# 入口函数

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;
}
1
2
3
4
5
6
7
8

第一步从入口看,这里实例化了一个 Hook 类,然后把我们刚刚传的参数透传进去,同时挂载了几个其他方法,如 tapAsync、tapPromise 等等。

这个时候机智的读者可能会问了,这不是同步 hook 吗,怎么还能调用 tapAsync?且看下面他们的实现你就知道了:

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");
};
1
2
3
4
5
6
7

这里是因为 new 出来的 hook 实例上有这些方法,但是 SyncHook 是不能调的,所以要覆盖成抛错的函数,避免被错误调用。

# Hook 类

这里简化一下,选取一部分核心逻辑。

const CALL_DELEGATE = function(...args) {
	this.call = this._createCall("sync");
	return this.call(...args);
};

class Hook {
	constructor(args = [], name = undefined) {
		this._args = args;
		this.name = name;
		this.taps = [];
		this.call = CALL_DELEGATE;
		this.tap = this.tap;
	}

	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) {
    // ...省略一些判断逻辑
		options = Object.assign({ type, fn }, options);
		this._insert(options);
	}

  _insert(item) {
    // ...以上省略一大段代码
    this.taps[i] = item;
  }

	tap(options, fn) {
		this._tap("sync", options, fn);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

这里梳理一下其实也很简单,调用 tap 方法会把你注册的方法放到 taps 队列中,等待后面被 call 调用。逻辑复杂一点的还是这个 call 之后执行的逻辑,他首先调用 _createCall,_createCall 函数里面又调了这个 compile 方法,而这个方法是要求在实例化的时候被重写的,我们来看看这个 compile 函数调用之后做了什么事。

class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}

const factory = new SyncHookCodeFactory();

const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这里其实也不是核心的部分,我们可以直接跳过,来关注下核心的部分,这里的 create 方法会最终调到上面的 content 方法,也就是进到 callTapsSeries 方法里,这里其实 tapable 实现了一套拦截器机制,这里就不过多阐述了,感兴趣可以直接看这块的源码,内容不多。我们来继续看 callTapsSeries 方法做了什么。

callTapsSeries({
		onError,
		onResult,
		resultReturns,
		onDone,
		doneReturns,
		rethrowIfPossible
}) {
	let code = "";
	let current = onDone;
  for (let j = this.options.taps.length - 1; j >= 0; j--) {
		const done = current;
  	const content = this.callTap(i, {......});
  	current = () => content;
  }
  code += current();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这里也是简化了一大段逻辑,主要是在遍历整个 taps 队列,然后进行字符串拼接,我们可以大致看下这个 callTap 函数做了什么

callTap() {
	// .....
	let code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
	code += `_fn${tapIndex}(${params});\n`;
	if (onDone) {
  	code += onDone();
  }
	// ......
}

1
2
3
4
5
6
7
8
9
10

这里也简化了下代码,我们可以差不多看到它就是组装了每一项 tap 的执行逻辑。同时我们可以看上面 callTapsSeries 函数遍历 taps 队列是从后往前遍历的,为啥这么做其实我们从它代码的组装逻辑可以看出来为啥,每次执行 callTap 都会把前面处理好的 code 拼到当前处理的 code 字符串的后面,也就是执行的时候越晚被遍历的函数执行的越早,和上面倒的遍历就对上了,保证了最终的执行顺序。

最后再看一眼它最终调用这段组装好的代码的地方,其实又来回归到前面调用的 create 函数里:

function create(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;
  }
	return fn;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

刚刚我们看的那几个函数就是从 contentWithInterceptors 这个函数一路调用过去的,最终拿到的结果会放到这个 new Function 中去执行。

至此大致的流程就差不多介绍到这了,如果有发现笔者表述不正确的地方,请帮忙指出,感激不尽。

编辑
上次更新: 2023/11/25, 4:11:00
Module Federation
深入探索 Webpack 插件机制

← Module Federation 深入探索 Webpack 插件机制→

最近更新
01
github加速
01-01
02
在线小工具
01-01
03
Lora-Embeddings
11-27
更多文章>
Theme by Vdoing | Copyright © 2020-2024 STDSuperman | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式