【webpack源码3】- tapable使用

webpack | 2020-06-13 16:01:20 507次 0次

wewpack 可以认为是一种基于事件流的编程范例,内部的工作流程都是基于 插件 机制串接起来,而将这些插件粘合起来的就是 webpack 自己写的基础类 Tapableplugin 方法就是该类暴露出来的。tapable 是一个类似于 nodejs 的 EventEmitter 的库,主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系。webpack 的本质就是一系列的插件运行。

本篇先介绍这个东西怎么来使用,为下一篇分析做准备,首先看下会用到的几种方法,分为同步方法和异步方法:

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncSeriesHook,
    AsyncSeriesWaterfallHook
} = require("tapable");


一、SyncHook

/**
 * SyncHook 同步 hook  注册使用tap 执行使用call
 */
const syncHook = new SyncHook(["name", "age"]);
// 注册事件
syncHook.tap("frist", (name, age) => {
  console.log("frist:", name, age);
});
syncHook.tap("second", (name, age) => {
  console.log("second:", name, age);
});
syncHook.tap("thrid", (name, age) => {
  console.log("thrid:", name, age);
});
// 触发事件,让监听函数执行
syncHook.call("Mike", 18);


二、SyncBailHook

/**
 * syncBailHook
 * 上一个函数有返回值则终止后续的函数
 */
const syncBailHook = new SyncBailHook(["name", "age"]);
// 注册事件
syncBailHook.tap("frist", (name, age) => {
  console.log("frist:", name, age);
});
syncBailHook.tap("second", (name, age) => {
  console.log("second:", name, age);
  return 'second';
});
syncBailHook.tap("thrid", (name, age) => {
  console.log("thrid:", name, age);
});
// 触发事件,让监听函数执行
syncBailHook.call("Mike", 18);


三、SyncWaterfallHook

/**
 * syncWaterfallHook
 * 依赖上一个函数的返回值,最终调用可获取最后一个事件返回值
 */
const syncWaterfallHook = new SyncWaterfallHook(["name", "age"]);
// 注册事件
syncWaterfallHook.tap("1", (name, age) => {
  console.log("第一个函数事件名称", name, age);
  return 'frist data';
});
syncWaterfallHook.tap("2", (data) => {
  console.log("第二个函数接受上个函数值:", data);
  return 'second data';
});
syncWaterfallHook.tap("3", (data) => {
  console.log("第三个函数接受上个函数值:", data);
  return 'thrid data';
});
// 触发事件,让监听函数执行
const res = syncWaterfallHook.call("Mike", 18);
console.log(res);


四、SyncLoopHook

可循环调用, 不返回 undefined 则循环执行,直到返回 undefined。

/**
 * syncLoopHook(webpack中未使用)
 * 可循环  不返回undefined 则循环执行,直到返回undefined   循环内部 do while循环
 */
const syncLoopHook = new SyncLoopHook(["name", "age"]);
// 定义辅助变量
let total1 = 0;
let total2 = 0;
// 注册事件
syncLoopHook.tap("1", (name, age) => {
  console.log("1", name, age, total1);
  return total1++ < 2 ? true : undefined;
});
syncLoopHook.tap("2", (name, age) => {
  console.log("2", name, age, total2);
  return total2++ < 2 ? true : undefined;
});
syncLoopHook.tap("3", (name, age) => {
  console.log("3", name, age);
});
// 触发事件,让监听函数执行
syncLoopHook.call("Mike", 18);


五、AsyncParallelHook

异步并行函数,可以通过回调函数调用和 promise 方式调用两种形式(此方法 webpack 中没有使用)。

方式一:tapAsync 注册    callAsync 执行

需要注意如果第一个先执行的回调中加了参数,最终的 callAsync 会立即执行,并且可以取到参数值,其他的 tapAsync 也会执行,但是执行完不会再进入最终回调,和书写顺序无关,而是看哪个先走完,传没传参数。

const asyncParallelHook = new AsyncParallelHook(["name", "age"]);
// 注册事件
asyncParallelHook.tapAsync("1", (name, age, done) => {
  setTimeout(() => {
    console.log("1", name, age, new Date());
    done(); //如果此时 done(1111) 加了参数 则此方法执行完立即进入callAsync方法 再进入下一个tapAsync执行
  }, 1000);
});
asyncParallelHook.tapAsync("2", (name, age, done) => {
  setTimeout(() => {
    console.log("2", name, age, new Date());
    done(); /如果此时 done(1111) 加了参数 则此方法执行完立即进入callAsync方法 可以获取这个参数值
  }, 2000);
});

// 触发事件,让监听函数执行
asyncParallelHook.callAsync("Mike", 18, (res) => {
  console.log('函数执行完毕', res);
});

方式二:tapPromise 注册   promise 执行

提示:之前可以在promise函数内部使用 resolve('1') 这样的,在外部使用 asyncParallelHook.promise("Mike", 18).then()

但是现在不可以这样用了,内部 resolve 之后外部 then 无法捕获了。

const asyncParallelHook = new AsyncParallelHook(["name", "age"]);
// peomise方式注册事件
asyncParallelHook.tapPromise("1", (name, age) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("1", name, age, new Date());
      //resolve('第一次成功')  无效了
    }, 1000);
  });
});

asyncParallelHook.tapPromise("2", (name, age) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("2", name, age, new Date());
      //resolve('第二次成功')  无效了
    }, 2000);
  });
});

// 触发事件,让监听函数执行
asyncParallelHook.promise("Mike", 18)


六、AsyncSeriesHook

异步串行执行,具体原理实现可以通过 reduce 函数借助,类似 redux 中间件实现方式。

方式一:tapAsync 注册    callAsync 执行

需要注意如果第一个先执行的回调中加了参数,最终的 callAsync 会立即执行,并且可以取到参数值,其他的 tapAsync 则不会继续执行,注意和上面的方法区别,因为这是串行函数,和书写顺序无关,而是看哪个先走完,传没传参数。

const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
// 注册事件
asyncSeriesHook.tapAsync("1", (name, age, done) => {
  setTimeout(() => {
    console.log("1", name, age, new Date());
    done();
  }, 1000);
});
asyncSeriesHook.tapAsync("2", (name, age, done) => {
  setTimeout(() => {
    console.log("2", name, age, new Date());
    done();
  }, 2000);
});

// 触发事件,让监听函数执行
asyncSeriesHook.callAsync("Mike", 18, () => {
  console.log('执行完成');
});

方式二:tapPromise 注册   promise 执行

asyncSeriesHook 和 asyncParallelHook 相比 promise 的写法有差异:

    1. 必须 resolve 才可以进入下一个tap注册函数

    2. 触发事件可以执行 then 方法,但是无法获取 reslove 中的值

const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);
// 注册事件
asyncSeriesHook.tapPromise("1", (name, age) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("1", name, age, new Date());
      resolve(111);
    }, 1000);
  })
});
asyncSeriesHook.tapPromise("2", (name, age) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("2", name, age, new Date());
      resolve(222);
    }, 2000);
  });
});

// 触发事件,让监听函数执行
asyncSeriesHook.promise("Mike", 18).then(res => {
    console.log('执行完毕,但是res是获取不到的')
});


七、AsyncSeriesWaterfallHook

异步串行阻塞执行。

方式一:tapAsync 注册    callAsync 执行

需要注意如果第一个先执行的回调中加了参数,并且第一个参数不为 null 最终的 callAsync 会立即执行,并且可以取到参数值,其他的 tapAsync 则不会继续执行,如果为 null 则不会阻断,最后一个执行的 tapAsync 中第一个参数的值会给到最终调用的 callAsync 中。

const asyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook(["name", "age"]);
// 注册事件
asyncSeriesWaterfallHook.tapAsync("1", (name, age, done) => {
  setTimeout(() => {
    console.log("1", name, age, new Date());
    // done(null, 11111);  第一个参数为null则可以继续
    done('no error', 11111); // 第一个参数 err 有值则会阻塞后面所有执行 直到执行的那个回调中,将错误信息给出
  }, 1000);
});
asyncSeriesWaterfallHook.tapAsync("2", (data, age, done) => {
  setTimeout(() => {
    console.log("2", data, age, new Date());
    done(2222222222);  //最后一个的这个第一个参数设不设置err无用,会返回给最终回调
  }, 2000);
});

// 触发事件,让监听函数执行
asyncSeriesWaterfallHook.callAsync("Mike", 18, (data) => {
  console.log('执行完成', data);
});

方式二:tapPromise 注册   promise 执行

const asyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook(["name", "age"]);
// 注册事件
asyncSeriesWaterfallHook.tapPromise("1", (name, age) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("1", name, age, new Date());
    //   reject(111);  抛出错误
    resolve(1111)
    }, 1000);
  })
});
asyncSeriesWaterfallHook.tapPromise("2", (name, age) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      //如果上个中resolve值,这里只会取到resolve中传的第一个值 age不会受影响
      console.log("2", name, age, new Date());
      resolve(222);
    }, 2000);
  });
});

// 触发事件,让监听函数执行
asyncSeriesWaterfallHook.promise("Mike", 18).then(res => {
    console.log('执行完毕', res)
}).catch(err => {
    console.log('结束有错误:', err)
})


八、拦截器 & context

所有钩子都提供额外的拦截器 API:

    ① call:实例方法 call 或者 callAsync 之前调用,即使多个 tap 注册此钩子只会执行一次

     register:每次注册 tap 事件触发后就会触发这个,会被触发多次

     loop:循环钩子(LoopHook, 就是类型 Loop)触发

     tap:实例方法 call 或者 callAsync 调用之后,tap 注册函数执行之前此方法被调用

const h1 = new SyncHook(['xxx']);
h1.tap('A', function(args) {
  console.log('a', args);
});
h1.tap({
  name: 'B',
  context: true
}, function() {
  console.log('d');
});

h1.intercept({
  //call之前调用
  call: (...args) => {
    console.log(...args, 'call 方法被执行了~~~~~~');
  },
  //每次注册 tap 事件触发后就会触发这个
  register: (tap) => {
    console.log(tap, '222222');
    return tap;
  },
  loop: (...args) => {
    console.log(...args, '33333');
  },
  //call执行后 先执行这个 再执行 tap 中注册的函数
  tap: (tap) => {
    console.log(tap, '444444');
  }
});

h1.tap('C', function() {
    console.log('e');
  });

h1.call("Mike");

其中在 tap 注册时候可以传入 context 为 true,这样 tap 注册器中或者 intercept 拦截器中各个钩子的第一个参数则可以设置这个 context 的值,全局使用。context 在实现原理上就是一个全局对象,各个方法执行时先判断是否指定了 context 为 true,然后将这个全局对象放在回调中第一个参数上。

本篇介绍了 tapable 中一些方法的使用,下一篇继续进入源码分析。


0人赞

分享到: