手写webpack4-Tapable

71 阅读6分钟

懒编译(延迟编译):每个Hook在创建的时候有一个call方法,但一直等到调用它的时候才将它动态改写成触发各个回调的方法,之后的执行依然使用改写后的方法,这个过程通过代理类函数,如CALL_DELEGATE(call的代理)来实现

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

class Hook {
    constructor(args = []) {
        this.args = args;//['name','age']
        this.taps = [];//就是一个存放我们事件函数的数组,订阅把函数存起来,存到这个数组里去了 {name,fn}
        this.call = CALL_DELEGATE;//this.taps.map(tap=>tap.fn)=this._x
        this.callAsync = CALL_ASYNC_DELEGATE;
        this.promise = PROMISE_DELEGATE;//promise方法
        this._x = undefined;//其实才是真正存放我们事件函数的数组 [fn]
        this.interceptors = [];//这就是拦截器
    }
}

如果在hook实例调用过call之后,又重新添加了回调,则需要重新编译:

下面的代码中再第一次syncHook.call之后,又通过tap订阅了一个名为'4'的方法,再次调用了syncHook.call

const { SyncHook } = require('./tapable');
//参数是一个数组,参数长度有用,代表取真实的call的参数的个数,数组里字符串的名字没用
const syncHook = new SyncHook(['name','age']);
//注册事件函数
syncHook.tap({name:'1'}, (name, age) => { //  events on
    console.log(1, name, age);
});
syncHook.tap('2', (name, age) => {
    console.log(2, name, age);
});
syncHook.tap('3', (name, age) => {
    console.log(3, name, age);
});
//触发事件函数
syncHook.call('zhufeng', 12); // emit
syncHook.tap('4', (name, age) => {
    console.log(4, name, age);
});
syncHook.call('zhufeng', 12);

代码中对应的处理是_resetCompilation方法:

class Hook {
    constructor(args = []) {
        this.args = args;//['name','age']
        this.taps = [];//就是一个存放我们事件函数的数组,订阅把函数存起来,存到这个数组里去了 {name,fn}
        this.call = CALL_DELEGATE;//this.taps.map(tap=>tap.fn)=this._x
        this.callAsync = CALL_ASYNC_DELEGATE;
        this.promise = PROMISE_DELEGATE;//promise方法
        this._x = undefined;//其实才是真正存放我们事件函数的数组 [fn]
        this.interceptors = [];//这就是拦截器
    }
    tap(options, fn) {
        this._tap('sync', options, fn);
    }
    _tap(type, options, fn) {
        if (typeof options === "string") {
            options = { name: options };
        }
        //创建tapInfo并且插入到数组中去
        let tapInfo = { ...options, type, fn };
        tapInfo = this._runRegisterInterceptors(tapInfo);
        this._insert(tapInfo);
    }
    _insert(tapInfo) {
        this._resetCompilation();//每次插入新的函数,需要重新编译call方法
	      this.taps.push(tapInfo);
    }
}

不同类型的Hook会有各自的compile方法,会调用HookCodeFactory生成模板代码,HookCodeFactory会接收Hook对象的taps数组(订阅数组)、Hook类型等作为参数(代码中体现为options参数):

let Hook = require('./Hook');
const HookCodeFactory = require('./HookCodeFactory');
class SyncHookCodeFactory extends HookCodeFactory{
    //内容不一样
    content({onDone}){
        //串行调用taps函数  fn0() fn1() fn2()
        return this.callTapsSeries({onDone});
    }
}
let factory = new SyncHookCodeFactory();
class SyncHook extends Hook{
    compile(options){
        //把钩子的实例和选项的值用来初始化代码工厂
        factory.setup(this,options);//options={type:'sync',taps,args}
        //根据选项创建call方法 new Function(args,函数体taps);
        return factory.create(options);
    }
}

module.exports = SyncHook;

HookCodeFactory完整代码:

class HookCodeFactory {
    setup(hookInstance, options) {
        //把tapInfo中的fn取出变成数组赋值给hookInstance._x
        hookInstance._x = options.taps.map(tapInfo => tapInfo.fn);
    }
    args(options = {}) {//{taps,args,type}
        let { before, after } = options;
        let allArgs = this.options.args || [];//args = ['name','age'];
        if (before) allArgs = [before, ...allArgs];
        if (after) allArgs = [...allArgs, after];
        return allArgs.join(',');//name,age
    }
    header() {
        let code = '';
        code += `var _x = this._x;\n`;
        return code;
    }
    callTapsSeries({ onDone } = {}) {
        let taps = this.options.taps;
        if (taps.length === 0) {
            return '';
        }
        let code = '';
        for (let i = 0; i < taps.length; i++) {
            const content = this.callTap(j);
		        code += content;
        }
        return code;
    }
    //onDone是每一个事件函数执行后的回调
    callTap(tapIndex, { onDone }) {
        //var _fn0 = _x[0];_fn0(name, age);
        let code = '';
        code += `var _fn${tapIndex} = _x[${tapIndex}];\n`;
        let tapInfo = this.options.taps[tapIndex];//{name,fn,type}
        switch (tapInfo.type) {
            case 'sync':
                code += `_fn${tapIndex}(${this.args()})\n`;
                  code += onDone();
                break;
            default:
                break;
        }
        return code;
    }
    init(options) {
        this.options = options;
    }
    deinit() {
        this.options = null;
    }
    create(options) {
        this.init(options);
        let fn;
        switch (options.type) {//sync
            case 'sync'://同步
                fn = new Function(
                    this.args(),//name,age
                    this.header() + this.content({onDone:()=>""})
                );
                break;
            default:
                break;
        }
        this.deinit();
        return fn;
    }

}
module.exports = HookCodeFactory;

Hook完整代码:

const CALL_DELEGATE = function (...args) {
    //先动态创建一个sync同步的类型的call方法,然后赋值给this.call
    //关于this的问题,只有一句话记住就行了.谁调用它就指向谁 钩子的实例syncHook.call('zhufeng', 12);
    this.call = this._createCall('sync');
    return this.call(...args);
}
class Hook {
    constructor(args = []) {
        this.args = args;//['name','age']
        this.taps = [];//就是一个存放我们事件函数的数组,订阅把函数存起来,存到这个数组里去了 {name,fn}
        this.call = CALL_DELEGATE;//this.taps.map(tap=>tap.fn)=this._x
        this.callAsync = CALL_ASYNC_DELEGATE;
        this.promise = PROMISE_DELEGATE;//promise方法
        this._x = undefined;//其实才是真正存放我们事件函数的数组 [fn]
        this.interceptors = [];//这就是拦截器
    }
    tap(options, fn) {
        this._tap('sync', options, fn);
    }
    _tap(type, options, fn) {
        if (typeof options === "string") {
            options = { name: options };
        }
        //创建tapInfo并且插入到数组中去
        let tapInfo = { ...options, type, fn };
        this._insert(tapInfo);
    }
    _insert(tapInfo) {
      	this.taps.push(tapInfo);
    }
    compile(options) {
        throw new Error('Abstract:此方法应该被子类重写');
    }
    _createCall(type) {
        //动态创建一个函数
        return this.compile({
            taps: this.taps,//执行函数的事件函数
            args: this.args,//事件函数接收的参数
            type,//执行的类型 sync  async
        });
    }
}

module.exports = Hook;

综上,钩子在触发时的调用顺序依次为:

sysHook.call('zhufeng', 12)
// 由于Hook对象在初始化时做了this.call = CALL_DELEGATE这一操作
// 所以实际执行了CALL_DELEGATE

// 然后又将this._createCall('sync')赋值给this.call:
this.call = this._createCall('sync')

// this._createCall又调用了compile方法:
this.compile({
    taps: this.taps,//执行函数的事件函数
    args: this.args,//事件函数接收的参数
    type,//执行的类型 sync  async
})

// 接下来调用各自Hook的子类对应的compile方法
// compile内部会先调用factory的setup方法,factory对象是先前实例化好的

// setup主要是给hookInstance添加_x属性,存储所有订阅的函数:
hookInstance._x = options.taps.map(tapInfo => tapInfo.fn)

// 接下来,compile里面会继续调用factory.create(options)
// 并将此调用的返回值作为compile的返回值
factory.create(options)

// factory.create内部会拼接组成函数的各个片段
// 这个过程会调用this.args、this.header、this.content这些方法分别去拼接各个部分
new Function(
    this.args(),//name,age
    this.header() + this.content({onDone:()=>""})
)

// 需要注意this.content是在HookCodeFactory父类中调用了子类(例如SyncHookCodeFactory)的方法

对于同步的钩子,想要拼接出类似这样的结构:

(function anonymous(name, age) {
    //是一个数组,里面存放着我们的所有的事件函数
    var _x = this._x;

    var _fn0 = _x[0];
    _fn0(name, age);

    var _fn1 = _x[1];
    _fn1(name, age);

    var _fn2 = _x[2];
    _fn2(name, age);
})

对于异步并行的钩子,想要拼接出这样的结构:

(function anonymous(name, age, _callback) {
    var _x = this._x;
    var _counter = 3;
    var _done = (function () {//所有的任务都完成了调用_done方法,从而调用最终的回调
        _callback();
    });

    var _fn0 = _x[0];
    _fn0(name, age, (function () {
        if (--_counter === 0) _done();
    }));

    var _fn1 = _x[1];
    _fn1(name, age, (function () {
        if (--_counter === 0) _done();
    }));

    var _fn2 = _x[2];
    _fn2(name, age, (function () {
        if (--_counter === 0) _done();
    }));
})

异步并行的钩子类似于Promise.all

异步并行的调用方式可以是callAsync和promise

hook.tapAsync('1', (name, age, callback) => {
    setTimeout(() => {
        console.log(1, name, age);
        callback();
    }, 1000);
});
hook.tapAsync('2', (name, age, callback) => {
    setTimeout(() => {
        console.log(1, name, age);
        callback();
    }, 2000);
});
hook.tapAsync('3', (name, age, callback) => {
    setTimeout(() => {
        console.log(1, name, age);
        callback();
    }, 3000);
});

hook.callAsync('zhufeng', 12, (err) => {
    console.log('err', err);
    console.timeEnd('cost');
});
hook.tapPromise('1', (name, age) => {
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            console.log(1, name, age);
            resolve();
        }, 1000);
    });
});
hook.tapPromise('2', (name, age,) => {
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            console.log(2, name, age);
            resolve();
        }, 2000);
    });
});
hook.tapPromise('3', (name, age,) => {
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            console.log(3, name, age);
            resolve();
        }, 3000);
    });
});
hook.promise('zhufeng', 12).then(result=>{
    console.log(result);
    console.timeEnd('cost');
});

需要添加对异步并行的处理:

    callTapsParallel() {
        let code = '';
        code += `var _counter = ${this.options.taps.length};\n`;
        code += `var _done = (function () {
            _callback();
        });\n`;
        for (let i = 0; i < this.options.taps.length; i++) {
            let content = this.callTap(i, {});
            code += content;
        }
        return code;
    }
    callTap(tapIndex) {
        //var _fn0 = _x[0];_fn0(name, age);
        let code = '';
        code += `var _fn${tapIndex} = _x[${tapIndex}];\n`;
        let tapInfo = this.options.taps[tapIndex];//{name,fn,type}
        switch (tapInfo.type) {
            case 'sync':
                code += `_fn${tapIndex}(${this.args()})\n`;
                  code += onDone();
                break;
            case 'async':
                code += `_fn${tapIndex}(${this.args({
                    after: ` function () {
                        if (counter === 0) _done();
                    }`
                })});\n`;
                break;

异步串行,调用方式和异步并行是一样的

需要拼接的代码:

(function anonymous(name, age, _callback) {
    var _x = this._x;
    
    function _next1() {
        var _fn2 = _x[2];
        _fn2(name, age, (function () {
            _callback();
        }));
    }
    function _next0() {
        var _fn1 = _x[1];
        _fn1(name, age, (function () {
            _next1();
        }));
    }
    var _fn0 = _x[0];
    _fn0(name, age, (function () {
        _next0();
    }));
})

异步串行用的和同步一样的callTapsSeries方法,需要对其进行改造:

改造的原则:

从上面的模板代码中可以发现,_next1、_next0的定义顺序是倒序的,所以会有一个倒序的循环

_next1、_next0的函数体里面,分别调用_fn2、_fn1的回调中,执行的方法分别是_callback _next1,是各不相同的,所以,需要将callTap中拼接回调的部分做成动态的

    callTapsSeries({ onDone } = {}) {
        let taps = this.options.taps;
        if (taps.length === 0) {
            return onDone(); //_callback();
        }
        let code = '';
        let current = onDone;
        for (let j = taps.length - 1; j >= 0; j--) {
            const unroll = current !== onDone;//如果不是最终的执行函数
            if (unroll) {//0=>1=>2
                //1.在外层 包裹 next函数
                //2.决定 下一个事件函数的onDone是自己这个next
                code += `function _next${j}(){\n`;
                code += current();
                code += '}\n';
                current = () => `_next${j}();\n`;
            }
            const done = current;
            const content = this.callTap(j, { onDone: done });
            current = () => content;
        }
        code += current();
        return code;
    }

callTap中的参数onDone是每执行一个回调,要执行的回调

所有回调执行完时,也有一个onDone回调,上面的callTapSeries中的参数onDone就是所有回调执行完时调用的回调,上面的callTapSeries中for循环上面的current变量,实际上是用来跟踪执行当前callTap的onDone回调,这两个回调在最开始是一样的

    callTap(tapIndex, {onDone}) {
        //var _fn0 = _x[0];_fn0(name, age);
        let code = '';
        code += `var _fn${tapIndex} = _x[${tapIndex}];\n`;
        let tapInfo = this.options.taps[tapIndex];//{name,fn,type}
        switch (tapInfo.type) {
            case 'sync':
                code += `_fn${tapIndex}(${this.args()})\n`;
                  code += onDone();
                break;
            case 'async':
                code += `_fn${tapIndex}(${this.args({
                    after: ` function () {
                        ${onDone()}
                    }`
                })});\n`;
                break;

接下来我们分析一下,callTapsSeries中for循环每次执行,current函数是什么,code变成了什么:

调用了this.callTap(j, { onDone: done }),j === 2 的这轮for循环结束时

current === ()=>`
var _fn2 = _x[2];
_fn2(name, age, (function () {
    _callback();
}));
`

code === ''

接下来进入j === 1的这轮循环,if中的代码执行完后

current === ()=>`_next1();\n`;

code === `
function _next1(){
    var _fn2 = _x[2];
    _fn2(name, age, (function () {
        _callback();
    }));
}
`

j === 1这轮循环结束时

current === ()=>`
var _fn1 = _x[1];
_fn1(name, age, (function () {
    _next1();
}));`

接下来进入j === 0的这轮循环,if中的代码执行完后

current === ()=>`_next0();\n`;

code === `
function _next1(){
    var _fn2 = _x[2];
    _fn2(name, age, (function () {
        _callback();
    }));
}

function _next0() {
    var _fn1 = _x[1];
    _fn1(name, age, (function () {
        _next1();
    }));
}
`

j === 0这轮循环结束时

current === () => `
var _fn0 = _x[0];
_fn0(name, age, (function () {
    _next0();
}));
`

此时整个for循环也就结束了,for循环后面我们可以看到执行了code += current(),因此code会拼上最后这部分代码,最终变成:

code === `
function _next1(){
    var _fn2 = _x[2];
    _fn2(name, age, (function () {
        _callback();
    }));
}

function _next0() {
    var _fn1 = _x[1];
    _fn1(name, age, (function () {
        _next1();
    }));
}

var _fn0 = _x[0];
_fn0(name, age, (function () {
    _next0();
}));
`