资讯专栏INFORMATION COLUMN

Webpack源码阅读之Tapable

yanwei / 2283人阅读

摘要:源码分析安装好包,根据上述方法,我们运行如下命令初始化在构造函数处打上断点,可以看到继承自,上面定义了一个函数。因为函数定义在原型上,并通过在构造函数中赋值。

Webpack源码阅读之Tapable

webpack采用Tapable来进行流程控制,在这套体系上,内部近百个插件有条不紊,还能支持外部开发自定义插件来扩展功能,所以在阅读webpack源码前先了解Tapable的机制是很有必要的。

Tapable的基本使用方法就不介绍了,可以参考官方文档

https://github.com/webpack/ta...

1. 例子

从网上拷贝了一个简单的使用例子:

//main.js
const { SyncHook } = require("tapable")

//创建一个简单的同步串行钩子
let h1 = new SyncHook(["arg1,arg2"]);

//在钩子上添加订阅者,钩子被call时会触发订阅的回调函数
h1.tap("A",function(arg){
  console.log("A",arg);
  return "b"
})
h1.tap("B",function(){
  console.log("b")
})
h1.tap("C",function(){
  console.log("c")
})

//在钩子上添加拦截器
h1.intercept({
  //钩子被call的时候触发
  call: (...args)=>{
     console.log(...args, "-------------intercept call");
  },
  //定义拦截器的时候注册taps
  register:(tap)=>{
     console.log(tap, "------------------intercept register");
  },
  //循环方法
  loop:(...args)=>{
     console.log(...args, "---------------intercept loop")
  },
  //tap调用前触发
  tap:(tap)=>{
     console.log(tap, "---------------intercept tap")
  }
})

//触发钩子
h1.call(6)
2. 调试方法

最直接的方式是在 chrome 中通过断点在关键代码上进行调试,在如何使用 Chrome 调试webpack源码中学到了调试的技巧:

我们可以用 node-inspector 在chrome中调试nodejs代码,这比命令行中调试方便太多了。nodejs 从 v6.x 开始已经内置了一个 inspector,当我们启动的时候可以加上 --inspect 参数即可:

node --inspect app.js

然后打开chrome,打开一个新页面,地址是: chrome://inspect,就可以在 chrome 中调试你的代码了。

如果你的JS代码是执行一遍就结束了,可能没时间加断点,那么你可能希望在启动的时候自动在第一行自动加上断点,可以使用这个参数 --inspect-brk,这样会自动断点在你的第一行代码上。

3. 源码分析

安装好Tapable包,根据上述方法,我们运行如下命令:

node --inspect-brk main.js 

3.1 初始化

在构造函数处打上断点,step into可以看到SyncHook继承自Hook,上面定义了一个compile函数。

class SyncHook extends Hook {
    tapAsync() {
        throw new Error("tapAsync is not supported on a SyncHook");
    }

    tapPromise() {
        throw new Error("tapPromise is not supported on a SyncHook");
    }

    compile(options) {
        factory.setup(this, options);
        return factory.create(options);
    }
}

再step into来到Hook.js

class Hook {
    //初始化
    constructor(args) {
      if (!Array.isArray(args)) args = [];
      this._args = args;
      //订阅者数组
      this.taps = [];
      //拦截器数组
      this.interceptors = [];
      //原型上触发钩子的方法,为什么复制到构造函数上?
      this.call = this._call;
      this.promise = this._promise;
      this.callAsync = this._callAsync;
      //用于保存订阅者回调函数数组
      this._x = undefined;
    }
    ...
    }

h1初始化完成:

h1:{
  call: ƒ lazyCompileHook(...args)
  callAsync: ƒ lazyCompileHook(...args)
  interceptors: []
  promise: ƒ lazyCompileHook(...args)
  taps: []
  _args: ["options"]
  _x: undefined
}
3.2 注册观察者

Tapable采用观察者模式来进行流程管理,在钩子上使用tap方法注册观察者,钩子被call时,观察者对象上定义的回调函数按照不同规则触发(钩子类型不同,触发顺序不同)。

Step into tap方法:

//options="A", fn=f(arg)
tap(options, fn) {
        //类型检测
        if (typeof options === "string") options = { name: options };
        if (typeof options !== "object" || options === null)
            throw new Error(
                "Invalid arguments to tap(options: Object, fn: function)"
            );
        //options ==>{type: "sync", fn: fn,name:options}
        options = Object.assign({ type: "sync", fn: fn }, options);
        if (typeof options.name !== "string" || options.name === "")
            throw new Error("Missing name for tap");
      //这里调用拦截器上的register方法,当intercept定义在tap前时,会在这里调用intercept.register(options), 当intercept定义在tap后时,会在intercept方法中调用intercept.register(this.taps)
        options = this._runRegisterInterceptors(options);
        //根据before, stage 的值来排序this.taps = [{type: "sync", fn: fn,name:options}]
        this._insert(options);
    }

当三个观察者注册完成后,h1变为:

{
  call: ƒ lazyCompileHook(...args)
  callAsync: ƒ lazyCompileHook(...args)
  interceptors: []
  promise: ƒ lazyCompileHook(...args)
  taps:[
       0: {type: "sync", fn: ƒ, name: "A"}
    1: {type: "sync", fn: ƒ, name: "B"}
    2: {type: "sync", fn: ƒ, name: "C"}
  ]
  length: 3
  __proto__: Array(0)
  _args: ["options"]
_x: undefined
}
3.3 注册拦截器

在调用h1.intercept() 处step into,可以看到定义的拦截回调被推入this.interceptors中。

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]);
        }
    }

此时h1变为:

{
  call: ƒ lazyCompileHook(...args)
  callAsync: ƒ lazyCompileHook(...args)
  interceptors: Array(1)
    0:
    call: (...args) => {…}
    loop: (...args) => {…}
    register: (tap) => {…}
    tap: (tap) => {…}
    __proto__: Object
    length: 1
    __proto__: Array(0)
  promise: ƒ lazyCompileHook(...args)
  taps: Array(3)
    0: {type: "sync", fn: ƒ, name: "A"}
    1: {type: "sync", fn: ƒ, name: "B"}
    2: {type: "sync", fn: ƒ, name: "C"}
    length: 3
    __proto__: Array(0)
  _args: ["options"]
  _x: undefined
}
3.4 钩子调用

在观察者和拦截器都注册后,会保存在this.interceptorsthis.taps中;当我们调用h1.call()函数后,会按照一定的顺序调用它们,现在我们来看看具体的流程,在call方法调用时step into, 会来到Hook.js中的createCompileDelegate函数。

function createCompileDelegate(name, type) {
    return function lazyCompileHook(...args) {
        this[name] = this._createCall(type);
        return this[name](...args);
    };
}

因为_call函数定义在Hook原型上,并通过在构造函数中this.call=this.__call赋值。

Object.defineProperties(Hook.prototype, {
    _call: {
        value: createCompileDelegate("call", "sync"),
        configurable: true,
        writable: true
    },
    _promise: {
        value: createCompileDelegate("promise", "promise"),
        configurable: true,
        writable: true
    },
    _callAsync: {
        value: createCompileDelegate("callAsync", "async"),
        configurable: true,
        writable: true
    }
});

按照执行顺序转到 this._createCall

_createCall(type) {
        return this.compile({
            taps: this.taps,
            interceptors: this.interceptors,
            args: this._args,
            type: type
        });
    }

this.compile()处step into 跳转到SyncHook.js上的compile方法上,其实我们在Hook.js上就可以看到,compile是需要在子类上重写的方法, 在SyncHook上其实现如下:

compile(options) {
        factory.setup(this, options);
        return factory.create(options);
    }

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

const factory = new SyncHookCodeFactory();

factory.setup处step into,可以看到factory.setup(this, options)其实只是把taps上注册的回调推入this._x:

    setup(instance, options) {
        instance._x = options.taps.map(t => t.fn);
    }

factory.create中定义了this.interceptorsthis.taps的具体执行顺序,在这里step into:

//HookFactory.js
create(options) {
        this.init(options);
        let fn;
        switch (this.options.type) {
            case "sync":
                fn = new Function(
                    this.args(),
                    ""use strict";
" +
                        this.header() +
                        this.content({
                            onError: err => `throw ${err};
`,
                            onResult: result => `return ${result};
`,
                            resultReturns: true,
                            onDone: () => "",
                            rethrowIfPossible: true
                        })
                );
                break;
            case "async":
                ....
            case "promise":
                ....
        }
        this.deinit();
        return fn;
    }

可以看到这里是通过new Function构造函数传入this.interceptorsthis.taps动态进行字符串拼接生成函数体执行的。

this.header()中打断点:

header() {
        let code = "";
        if (this.needContext()) {
            code += "var _context = {};
";
        } else {
            code += "var _context;
";
        }
        code += "var _x = this._x;
";
        if (this.options.interceptors.length > 0) {
            code += "var _taps = this.taps;
";
            code += "var _interceptors = this.interceptors;
";
        }
        for (let i = 0; i < this.options.interceptors.length; i++) {
            const interceptor = this.options.interceptors[i];
            if (interceptor.call) {
                code += `${this.getInterceptor(i)}.call(${this.args({
                    before: interceptor.context ? "_context" : undefined
                })});
`;
            }
        }
        return code;
    }

生成的code如下,其执行了拦截器中定义的call回调:

"var _context;
var _x = this._x;
var _taps = this.taps;
var _interceptors = this.interceptors;
_interceptors[0].call(options);

this.content()打断点,可以看到this.content定义在HookCodeFactory中:

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

其返回了定义在子类中的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 || false;
        let code = "";
        let current = onDone;
        for (let j = this.options.taps.length - 1; j >= 0; j--) {
            const i = j;
            const unroll = current !== onDone && this.options.taps[i].type !== "sync";
            if (unroll) {
                code += `function _next${i}() {
`;
                code += current();
                code += `}
`;
                current = () => `${somethingReturns ? "return " : ""}_next${i}();
`;
            }
            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;
    }

具体的拼接步骤这里就不详述了,感兴趣可以自己debugger,嘿嘿。最后返回的code为:

var _tap0 = _taps[0];
_interceptors[0].tap(_tap0);
var _fn0 = _x[0];
_fn0(options);
var _tap1 = _taps[1];
_interceptors[0].tap(_tap1);
var _fn1 = _x[1];
_fn1(options);
var _tap2 = _taps[2];
_interceptors[0].tap(_tap2);
var _fn2 = _x[2];
_fn2(options);
var _tap3 = _taps[3];
_interceptors[0].tap(_tap3);
var _fn3 = _x[3];
_fn3(options);

这里定义了taps和其相应的拦截器的执行顺序。

4. webpack调试技巧

当我们调试webpack源码是,经常需要在钩子被call的代码处调试到具体插件的执行过程,可以参考上述过程进行调试,具体步骤为:

在call处step into

在return处step into

得到生成的动态函数

(function anonymous(options
) {
"use strict";
  var _context;
  var _x = this._x;
  var _taps = this.taps;
  var _interceptors = this.interceptors;
  _interceptors[0].call(options);
  var _tap0 = _taps[0];
  _interceptors[0].tap(_tap0);
  var _fn0 = _x[0];
  _fn0(options);
  var _tap1 = _taps[1];
  _interceptors[0].tap(_tap1);
  var _fn1 = _x[1];
  _fn1(options);
  var _tap2 = _taps[2];
  _interceptors[0].tap(_tap2);
  var _fn2 = _x[2];
  _fn2(options);
  var _tap3 = _taps[3];
  _interceptors[0].tap(_tap3);
  var _fn3 = _x[3];
  _fn3(options);
})

在fn(options)处打step into

回到tap注册的函数

h1.tap("A", function (arg) {
    console.log("A",arg);
    return "b"; 
})

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106805.html

相关文章

  • webpack源码tapable

    摘要:它的行为和的方法相似,用来注册一个处理函数监听器,来在信号事件发生时做一些事情他最终还是调用进行存储。而就全部取出来执行。总结上面这些知识是理解插件和运行原理的前置条件更多内容待下次分解参考源码版本说明参考链接 引言 去年3月的时候当时写了一篇webpack2-update之路,到今天webpack已经到了4.2,更新挺快的,功能也在不断的完善,webpack4特性之一就是零配置, w...

    Keagan 评论0 收藏0
  • 浅析webpack源码Tapable粗解(五)

    摘要:打开是个构造函数,定义了一些静态属性和方法我们先看在插件下地址上面写的解释就跟没写一样在文件下我们看到输出的一些对象方法每一个对应一个模块而在下引入的下面,我们先研究引入的对象的英文单词解释,除了最常用的点击手势之外,还有一个意思是水龙头进 打开compile class Compiler extends Tapable { constructor(context) { ...

    Arno 评论0 收藏0
  • Webpack 源码(一)—— Tapable 和 事件流

    摘要:开始对进行遍历,当遇到等一些调用表达式时,触发事件的执行,收集依赖,并。 1、Tapable Tap 的英文单词解释,除了最常用的 点击 手势之外,还有一个意思是 水龙头 —— 在 webpack 中指的是后一种; Webpack 可以认为是一种基于事件流的编程范例,内部的工作流程都是基于 插件 机制串接起来; 而将这些插件粘合起来的就是webpack自己写的基础类 Tapable 是...

    supernavy 评论0 收藏0
  • webpack源码分析四:plugin

    摘要:流程划分纵观整个打包过程,可以流程划分为四块。核心类关系图功能实现模块通过将源码解析为树并拆分,以及直至基础模块。通过的依赖和切割文件构建出含有和包含关系的对象。通过模版完成主入口文件的写入,模版完成切割文件的写入。 前言 插件plugin,webpack重要的组成部分。它以事件流的方式让用户可以直接接触到webpack的整个编译过程。plugin在编译的关键地方触发对应的事件,极大的...

    yhaolpz 评论0 收藏0
  • webpack源码plugin机制

    摘要:调用的目的是为了注册你的逻辑指定一个绑定到自身的事件钩子。这个对象在启动时被一次性建立,并配置好所有可操作的设置,包括,和。对象代表了一次资源版本构建。一个对象表现了当前的模块资源编译生成资源变化的文件以及被跟踪依赖的状态信息。 引言 在上一篇文章Tapable中介绍了其概念和一些原理用法,和这次讨论分析webpack plugin的关联很大。下面从实现一个插件入手。 demo插件 f...

    glumes 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<