webpack 编译过程

  • 配置初始化
  • 内容编译
  • 输出编译后内容

这三个过程的整体执行过程可以看作是一种事件驱动型的事件工作流机制,这个机制可以将不同的插件串联起来,最后完成所有的工作。
其中最核心的两个部分就是负责编译的 compiler 和负责创建 bundles 的 compilation。

tapable

tapable 本身是一个独立的库。

工作流程

  • 实例化 hook 注册事件监听
  • 通过 hook 触发事件监听
  • 执行懒编译生成的可执行代码

hook 本质是 tapable 实例对象,也成为钩子。

hook 执行机制可以分为同步和异步,异步的钩子也可以分为并行和串行两种模式。

hook 执行特点

  • Hook:普通钩子,监听器之间互相独立互不干扰
  • BailHook:熔断钩子,某个钩子监听返回非 undefiend 时,后续监听的钩子不执行
  • WaterfallHook:瀑布钩子,上一个监听的返回值可传递至下一个
  • LoopHook:循环钩子,如果当前未返回 false 则一直执行

tapable 库同步钩子

  • SynckHook
  • SyncBailHook
  • SyncWaterfallHook
  • SyncLoopHook

tapable 库异步串行钩子

  • AsyncSeriesHook
  • AsyncSeriesBailHook
  • AsyncSeriesWaterfallHook

tapable 库异步钩子

  • AsyncParalleHook
  • AsyncParalleBailhook

同步钩子使用

SyncHook

  1. const { SyncHook } = require('tapable');
  2. const hook = new SyncHook(['name', 'age']);
  3. hook.tap('fn1', function (name, age) {
  4. console.log('fn1--> ', name, age);
  5. });
  6. hook.tap('fn2', function (name, age) {
  7. console.log('fn2--> ', name, age);
  8. });
  9. hook.tap('fn3', function (name, age) {
  10. console.log('fn3--> ', name, age);
  11. });
  12. hook.call('yueluo', 18);
  13. // yueluo 18
  14. // yueluo 18
  15. // yueluo 18

SyncBailHook

  1. const { SyncBailHook } = require('tapable');
  2. const hook = new SyncBailHook(['name', 'age']);
  3. hook.tap('fn1', function (name, age) {
  4. console.log('fn1--> ', name, age);
  5. });
  6. hook.tap('fn2', function (name, age) {
  7. console.log('fn2--> ', name, age);
  8. return 'result 2';
  9. });
  10. hook.tap('fn3', function (name, age) {
  11. console.log('fn3--> ', name, age);
  12. });
  13. hook.call('heora', 100);
  14. // heora 100
  15. // hrora 100

熔断钩子,如果某一个事件监听是非 undefined,后续逻辑不会使用

SyncWaterfallHook

  1. const { SyncWaterfallHook } = require('tapable');
  2. const hook = new SyncWaterfallHook(['name', 'age']);
  3. hook.tap('fn1', function (name, age) {
  4. console.log('fn1--> ', name, age);
  5. return 'ret1';
  6. });
  7. hook.tap('fn2', function (name, age) {
  8. console.log('fn2--> ', name, age);
  9. return 'ret2';
  10. });
  11. hook.tap('fn3', function (name, age) {
  12. console.log('fn3--> ', name, age);
  13. return 'ret3';
  14. });
  15. hook.call('heora', 33);
  16. // fn1--> heora 33
  17. // fn2--> ret1 33
  18. // fn3--> ret2 33

可以在上一个钩子返回某个值,交给下一个钩子使用。

SyncLoopHook

  1. const { SyncLoopHook } = require('tapable');
  2. const hook = new SyncLoopHook(['name', 'age']);
  3. let count1 = 0;
  4. let count2 = 0;
  5. let count3 = 0;
  6. hook.tap('fn1', function (name, age) {
  7. console.log('fn1--> ', name, age);
  8. if (++count1 === 1) {
  9. count1 = 0;
  10. return undefined;
  11. } else {
  12. return true;
  13. }
  14. });
  15. hook.tap('fn2', function (name, age) {
  16. console.log('fn2--> ', name, age);
  17. if (++count2 === 2) {
  18. count2 = 0;
  19. return undefined;
  20. } else {
  21. return true;
  22. }
  23. });
  24. hook.tap('fn3', function (name, age) {
  25. console.log('fn3--> ', name, age);
  26. });
  27. hook.call('heora', 33);

如果钩子内部返回非 undefined 值,就会从新开始循环执行钩子。

根据其实就是 do while 循环,如果返回非 undefined,会循环执行钩子函数。

异步钩子使用

AsyncParallelHook

  1. const { AsyncParallelHook } = require('tapable');
  2. const hook = new AsyncParallelHook(['name']);
  3. // 对于异步钩子使用,添加事件时存在三种方式:tap/tapAsync/tapPromise
  4. // hook.tap('fn1', function (name) {
  5. // console.log('fn1--> ', name);
  6. // });
  7. // hook.tap('fn2', function (name) {
  8. // console.log('fn2--> ', name);
  9. // });
  10. // hook.callAsync('yueluo', function () {
  11. // console.log('callback exec')
  12. // });
  13. // console.time('time');
  14. // hook.tapAsync('fn1', function (name, callback) {
  15. // setTimeout(() => {
  16. // console.log('fn1--> ', name);
  17. // callback();
  18. // }, 1000)
  19. // });
  20. // hook.tapAsync('fn2', function (name, callback) {
  21. // setTimeout(() => {
  22. // console.log('fn2--> ', name);
  23. // callback();
  24. // }, 2000)
  25. // });
  26. // hook.callAsync('yueluo', function () {
  27. // console.log('callback exec')
  28. // console.timeEnd('time');
  29. // });
  30. console.time('time');
  31. hook.tapPromise('fn1', function (name) {
  32. return new Promise((resolve, reject) => {
  33. setTimeout(() => {
  34. console.log('fn1--> ', name);
  35. resolve();
  36. }, 1000)
  37. })
  38. });
  39. hook.tapPromise('fn2', function (name) {
  40. return new Promise((resolve, reject) => {
  41. setTimeout(() => {
  42. console.log('fn2--> ', name);
  43. resolve();
  44. }, 2000)
  45. })
  46. });
  47. hook.promise('yueluo').then(() => {
  48. console.log('callback exec')
  49. console.timeEnd('time');
  50. });
  51. // fn1--> yueluo
  52. // fn2--> yueluo
  53. // callback exec
  54. // time: 2017.780ms

AsyncParallelBailHook

  1. const { AsyncParallelBailHook } = require('tapable');
  2. const hook = new AsyncParallelBailHook(['name']);
  3. console.time('time');
  4. hook.tapPromise('fn1', function (name) {
  5. return new Promise((resolve, reject) => {
  6. setTimeout(() => {
  7. console.log('fn1--> ', name);
  8. resolve();
  9. }, 1000)
  10. })
  11. });
  12. hook.tapPromise('fn2', function (name) {
  13. return new Promise((resolve, reject) => {
  14. setTimeout(() => {
  15. console.log('fn2--> ', name);
  16. resolve(false);
  17. }, 2000)
  18. })
  19. });
  20. hook.tapPromise('fn3', function (name) {
  21. return new Promise((resolve, reject) => {
  22. setTimeout(() => {
  23. console.log('fn3--> ', name);
  24. resolve();
  25. }, 3000)
  26. })
  27. });
  28. hook.promise('yueluo').then(() => {
  29. console.log('callback exec')
  30. console.timeEnd('time');
  31. });
  32. // fn1--> yueluo
  33. // fn2--> yueluo
  34. // callback exec
  35. // time: 2016.021ms
  36. // fn3--> yueluo

AsyncSeriesHook

const { AsyncSeriesHook } = require('tapable');

const hook = new AsyncSeriesHook(['name']);

console.time('time');
hook.tapPromise('fn1', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('fn1--> ', name);
      resolve();
    }, 1000)
  })
});

hook.tapPromise('fn2', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('fn2--> ', name);
      resolve(false);
    }, 2000)
  })
});

hook.tapPromise('fn3', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('fn3--> ', name);
      resolve();
    }, 3000)
  })
});

hook.promise('yueluo').then(() => {
  console.log('~~~~~~')
  console.timeEnd('time');
});

// fn1-->  yueluo
// fn2-->  yueluo
// fn3-->  yueluo
// ~~~~~~
// time: 6050.305ms

SyncHook 源码

测试代码

const { SyncHook } = require('tapable');

const hook = new SyncHook(['name', 'age']);

hook.tap('fn1', function (name, age) {
  console.log('fn1--> ', name, age);
});

hook.tap('fn2', function (name, age) {
  console.log('fn2--> ', name, age);
});

hook.tap('fn3', function (name, age) {
  console.log('fn3--> ', name, age);
});

hook.call('yueluo', 18);

源码分析

SyncHook.js

"use strict";

// Hook 是所有钩子类的父类
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;

Hook.js

"use strict";

const util = require("util");

const deprecateContext = util.deprecate(() => {},
"Hook.context is deprecated and will be removed");

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 (typeof options !== "object" || options === null) {
            throw new Error("Invalid tap options");
        }
        if (typeof options.name !== "string" || options.name === "") {
            throw new Error("Missing name for tap");
        }
        if (typeof options.context !== "undefined") {
            deprecateContext();
        }
    // 合并对象
        options = Object.assign({ type, fn }, options);
    // 注册拦截器
        options = this._runRegisterInterceptors(options);
    // 添加 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; // 提前占位,数组长度加 1
            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;
        }
    // 向 taps 里增加 item,即传入的 options
        this.taps[i] = item;
    }
}

Object.setPrototypeOf(Hook.prototype, null);

module.exports = Hook;

HookCodeFactory.js

class HookCodeFactory {
    constructor(config) {
        this.config = config;
        this.options = undefined;
        this._args = undefined;
    }

    create(options) {
        this.init(options);
        let fn;

    // 根据 type 生成不同的代码
        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;
    }

    setup(instance, options) {
    // 向 hook 身上挂载 _x 属性
        instance._x = options.taps.map(t => t.fn);
    }

    /**
     * @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options
     */
    init(options) {
        this.options = options;
        this._args = options.args.slice();
    }

    deinit() {
        this.options = undefined;
        this._args = undefined;
    }

    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 += `${this.getInterceptor(i)}.call(${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 += `${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 += `${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 += `${this.getInterceptor(i)}.done();\n`;
                                }
                            }
                            code += onDone();
                            return code;
                        })
                })
            );
            return code;
        } else {
            return this.content(options);
        }
    }

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

    needContext() {
        for (const tap of this.options.taps) if (tap.context) return true;
        return false;
    }

    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} = ${this.getTap(tapIndex)};\n`;
                    hasTapCached = true;
                }
                code += `${this.getInterceptor(i)}.tap(${
                    interceptor.context ? "_context, " : ""
                }_tap${tapIndex});\n`;
            }
        }
        code += `var _fn${tapIndex} = ${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}(${this.args({
                        before: tap.context ? "_context" : undefined
                    })});\n`;
                } else {
                    code += `_fn${tapIndex}(${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}(${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}(${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;
    }

    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;
            const unroll =
                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}();\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;
    }

    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 = (function() {\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;
            },
            onDone:
                onDone &&
                (() => {
                    let code = "";
                    code += "if(!_loop) {\n";
                    code += onDone();
                    code += "}\n";
                    return code;
                }),
            rethrowIfPossible: rethrowIfPossible && syncOnly
        });
        code += "} while(_loop);\n";
        if (!syncOnly) {
            code += "_loopAsync = true;\n";
            code += "});\n";
            code += "_looper();\n";
        }
        return code;
    }

    callTapsParallel({
        onError,
        onResult,
        onDone,
        rethrowIfPossible,
        onTap = (i, run) => run()
    }) {
        if (this.options.taps.length <= 1) {
            return this.callTapsSeries({
                onError,
                onResult,
                onDone,
                rethrowIfPossible
            });
        }
        let code = "";
        code += "do {\n";
        code += `var _counter = ${this.options.taps.length};\n`;
        if (onDone) {
            code += "var _done = (function() {\n";
            code += onDone();
            code += "});\n";
        }
        for (let i = 0; i < this.options.taps.length; i++) {
            const done = () => {
                if (onDone) return "if(--_counter === 0) _done();\n";
                else return "--_counter;";
            };
            const doneBreak = skipDone => {
                if (skipDone || !onDone) return "_counter = 0;\n";
                else return "_counter = 0;\n_done();\n";
            };
            code += "if(_counter <= 0) break;\n";
            code += onTap(
                i,
                () =>
                    this.callTap(i, {
                        onError: error => {
                            let code = "";
                            code += "if(_counter > 0) {\n";
                            code += onError(i, error, done, doneBreak);
                            code += "}\n";
                            return code;
                        },
                        onResult:
                            onResult &&
                            (result => {
                                let code = "";
                                code += "if(_counter > 0) {\n";
                                code += onResult(i, result, done, doneBreak);
                                code += "}\n";
                                return code;
                            }),
                        onDone:
                            !onResult &&
                            (() => {
                                return done();
                            }),
                        rethrowIfPossible
                    }),
                done,
                doneBreak
            );
        }
        code += "} while(false);\n";
        return code;
    }

    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(", ");
        }
    }

    getTapFn(idx) {
        return `_x[${idx}]`;
    }

    getTap(idx) {
        return `_taps[${idx}]`;
    }

    getInterceptor(idx) {
        return `_interceptors[${idx}]`;
    }
}

module.exports = HookCodeFactory;

Hook 提供所有内容,HookCodeFactory 进行代码拼接的工厂,SyncHook 是一个 Hook 之上的特定普通钩子。

源码实现

Hook、SyncHook、HookCodeFactory

测试代码

const SyncHook = require('./shared/SyncHook');

const hook = new SyncHook(['name', 'age']);

hook.tap('fn1', function (name, age) {
  console.log('fn1--> ', name, age);
});

hook.tap('fn2', function (name, age) {
  console.log('fn2--> ', name, age);
});

hook.tap('fn3', function (name, age) {
  console.log('fn3--> ', name, age);
});

hook.call('yueluo', 18);

shared/SyncHook

const Hook = require('./Hook.js');

class HookCodeFactory {
  // 准备后续需要使用的数据
  setup (instance, options) {
    this.options = options; // 源码中是通过 init 方法实现
    instance._x = options.taps.map(o => o.fn);
  }

  args () {
    return this.options.args.join(',');
  }

  head () {
    return `var _x = this._x;`;
  }

  content () {
    let code = '';

    for (var i = 0; i < this.options.taps.length; i++) {
      code += `var _fn${i} = _x[${i}]; _fn${i}(${this.args()});`;
    }

    return code;
  }

  // 创建一段可执行的代码体并返回
  create (options) {
    let fn = undefined;

    fn = new Function(
      this.args(),
      this.head() + this.content()
    )

    return fn;
  }
}

const factory = new HookCodeFactory();

class SyncHook extends Hook {
  constructor (args) {
    super(args);
  }

  compile (options) { // { taps: [], args: [name, age] }
    factory.setup(this, options);
    return factory.create(options);
  }
}

module.exports = SyncHook;

shared/Hook.js

class Hook {
  constructor (args = []) {
    this.args = args;
    this.taps = []; // 用于存放组装好的对象信息
    this._x = undefined; // 用于在代码工厂函数中使用
  }

  tap (options, fn) {
    if (typeof options === 'string') {
      options = { name: options }
    }
    options = Object.assign({ fn }, options); // { fn, name: fn1 }

    // 将组装好的 options 添加至数组中
    this._insert(options);
  }

  _insert (options) {
    this.taps[this.taps.length] = options;
  }

  call (...args) {
    // 创建具体要执行的函数代码结构
    let callFn = this._createCall();

    // 调用上述函数,传参
    return callFn.apply(this, args);    
  }

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

module.exports = Hook;

AsyncParallelHook 源码

测试代码

const AsyncParallelHook = require('./lib/AsyncParallelHook.js')

const hook = new AsyncParallelHook(['name', 'age']);

hook.tapAsync('fn1', function (name, age, callback) {
  console.log('fn1--> ', name, age);
  callback();
});

hook.tapAsync('fn2', function (name, age, callback) {
  console.log('fn2--> ', name, age);
  callback();
});

hook.tapAsync('fn3', function (name, age, callback) {
  console.log('fn3--> ', name, age);
  callback();
});

hook.callAsync('yueluo', 18, function () {
  console.log('end');
});

源码实现

lib/AsyncParallelHook.js

const Hook = require('./Hook.js');

class HookCodeFactory {
  setup (instance, options) {
    this.options = options;
    instance._x = options.taps.map(o => o.fn);
  }

  args ({ after, before } = {}) {
    let allArgs = this.options.args;

    if (before) allArgs = [before].concat(allArgs);
    if (after) allArgs = allArgs.concat(after);

    return allArgs.join(',');
  }

  head () {
    return `"use strict"; var _context; var _x = this._x;`;
  }

  content () {
    let code = '';

    code += `
      var _counter = ${ this.options.taps.length };
      var _done = (function () {
        _callback();
      });
    `

    for (var i = 0; i < this.options.taps.length; i++) {
      code += `
        var _fn${i} = _x[${i}];

        _fn${i}(name, age, (function () {
          if (--_counter === 0) _done();
        }))
      `;
    }

    return code;
  }

  // 创建一段可执行的代码体并返回
  create (options) {
    let fn = undefined;

    fn = new Function(
      this.args({
        after: '_callback'
      }),
      this.head() + this.content()
    )

    return fn;
  }
}

const factory = new HookCodeFactory();

class AsyncParallelHook extends Hook {
  constructor (args) {
    super(args);
  }

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

module.exports = AsyncParallelHook;

lib/Hook.js

class Hook {
  constructor (args = []) {
    this.args = args;
    this.taps = []; // 用于存放组装好的对象信息
    this._x = undefined; // 用于在代码工厂函数中使用
  }

  tap (options, fn) {
    if (typeof options === 'string') {
      options = { name: options }
    }
    options = Object.assign({ fn }, options); // { fn, name: fn1 }

    this._insert(options);
  }

  tapAsync (options, fn) {
    if (typeof options === 'string') {
      options = { name: options }
    }
    options = Object.assign({ fn }, options);

    this._insert(options);
  }

  _insert (options) {
    this.taps[this.taps.length] = options;
  }

  call (...args) {
    // 创建具体要执行的函数代码结构
    let callFn = this._createCall();

    // 调用上述函数,传参
    return callFn.apply(this, args);    
  }

  callAsync (...args) {
    let callFn = this._createCall();

    return callFn.apply(this, args);    
  }

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

module.exports = Hook;