likes
comments
collection
share

js如何根据需求动态生成函数?

作者站长头像
站长
· 阅读数 26

前言

承接上文,本文是关于tapable的第二篇文章。上文我们简单认识了tapable,了解到tapable有着各种hook,那么本文就来揭开这个些hook的神秘面纱。

深入认识tapable

上文中我们在认识tapable的时候,看了很多例子,大概就是通过调用hook的tap方法注册一些函数,最后tapable就按照hook自身的规则去执行这些注册的函数。

不知道大家在看到这些例子和最后结果表现的时候,是否会觉得这个现象很神奇,它似乎可塑性很高,最后的效果基本都是由使用者决定的。

那么这种神奇的效果,tapable又是如何实现的呢?我们来简单调试下tapable的代码。

我们拿最简单的SyncHook为例子去调试。

const { SyncHook } = require('tapable');
// 创建
// 这个name和age无实际意义,其实就是表示接受多少个参数而已
const hook = new SyncHook(['name', 'age']);
// 注册
hook.tap('1', (name, age) => {
  console.log('1', name, age);
  // 就算有返回值也会无视继续走下去
});
hook.tap('2', (name, age) => {
  console.log('2', name, age);
});
hook.tap('3', (name, age) => {
  console.log('3', name, age);
});
debugger;
// 调用
hook.call('Rippi', 18);

我们使用vscode自带的调试工具进行调试。

js如何根据需求动态生成函数?

中间步骤可以简单看看,很多代码看不懂没关系,一路简单调试过来(也可直接跳过),然后来到下图这个位置。

js如何根据需求动态生成函数?

可以看出,在tapable根据所使用的hook的规则,生成了上图这个函数,而我们从上到下注册的三个函数分别对应上图的 _fn0、_fn1、_fn2。然后tapable执行这个生成的函数。

tapable的实现思考

举一反三,另外几种hook肯定也是如此,显示根据hook的规则生成一个函数,然后执行这个函数。值得注意的是,我们打debugger的地方是hook.call方法这,结合生成的代码可以推断出以下两点:

  • tap方法为注册方法,主要是存储注册的函数(存储_x中)。
  • call方法为生成函数的方法,主要是从_x中拿注册的函数。

那么,tapable是如何根据注册的函数和使用的hook去动态生成这个可执行的函数的呢?

上面经过调试的朋友,一定会看到下面这样一幕。

js如何根据需求动态生成函数?

图中这个code += "var _content;\n"code += _x = this._x;\n不就是最后生成的函数的前两句吗,那这张图的代码是在干嘛?

是的,拼接字符串!

这里我们回顾一下基础知识,因为大家在工作中基本不会使用这种方法去创建函数 ———— new Function。

new Function很简单,一般穿两个参数,第一个是生成函数的参数,第二个是生成函数的函数体。我们来看下下面的小例子。

const log = new Function(
  'a, b',
  'console.log(a, b)'
);
log(1, 2);

// 这里的log其实相当于这样
function log(a, b) {
  console.log(a, b);
};

js如何根据需求动态生成函数?

回顾了这个小知识点后,我们可以很肯定,tapable动态生成函数的关键其实就是通过new Function的方式去创建函数,而new Function其实就是简单的拼接字符串。

SyncHook的实现

拼接字符串想必各位都是非常熟悉且觉得简单的,那么我们就直接开始简单实现一个SyncHook好了。

我们的目标就是要像tapable的SyncHook一样,都动态生成一个函数,至于中间的一些校验本文就不过多赘述或者实现了。

先说明下文件目录。

js如何根据需求动态生成函数?

Hook基类

从上文的各种hook的例子,以及上面对最后生成的函数的分析我们可以得出以下几点:

  • 基本都会注册形参(即是下图这一步)

js如何根据需求动态生成函数?

  • 都有tap和call方法
  • tap注册的函数都会存放到_x中,也就是需要一个存储注册的函数的变量

既然以上几点是所有Hook都有的,那么我们就将他们都写到Hook这个基类中。

class Hook {
  constructor(args = []) {
    // 形参列表
    this.args = args;
    // 注册的回调函数  taps就是_x的前身
    this.taps = [];
    // 调用方法
    this.call = CALL_DELEGATE;
  }
  // 如果通过tap方法注册的回调,type就是sync,表示要以同步的方式调用
  tap(options, fn) {
    this._tap('sync', options, fn);
  }
  // 统一的注册方法
  _tap(type, options, fn) {
    if (typeof options === 'string') {
      options = { name: options };
    }
    const tapInfo = { ...options, type, fn };
    this._insert(tapInfo);
  }
  // 将注册的函数存储到taps这个变量中
  _insert(tapInfo) {
    this.taps.push(tapInfo);
  }
  // 动态生成函数(不用在意compile方法怎么来的,下文会讲到)
  _createCall(type) {
    return this.compile({
      taps: this.taps,
      args: this.args,
      type
    });
  }
}

const CALL_DELEGATE = function (...args) {
  // 动态创建一个Sync类型的call方法
  this.call = this._createCall('sync');
  return this.call(...args);
}

module.exports = Hook;

上面的代码中暂时写死的都是Sync类型,只要是调用tap方法注册的都会是Sync类型,至于其他类型我们在之后的文章中揭晓,敬请期待。

那么,Hook这个基类就搞定了。

HookCodeFactory生成动态函数的工厂类

我们再仔细看看之前tapable生成的函数。这个函数可以分成以下三部分:

  • 参数部分即name, age
  • 头部部分,即是生成_x那部分
  • 主要内容部分即是var _x = this._x以下的部分

参数部分

参数部分很简单,就是拿到前面注册的形参,然后拼接成字符串。就是args.join(',')

头部部分

头部共三句,其中"use strict;"var _context;暂时还没用,我们可以不加,那么就只剩下var _x = this._x了。

所谓的_x其实很简单,就是将我们的注册的函数都放到_x中。

_x = taps.map(tapInfo => tapInfo.fn)

内容部分

内容部分也不难,就是遍历一次taps(注册的函数集合),每次遍历都生成var _fn[i] = _x[i];_fn[i ](args)

完整代码

class HookCodeFactory {
  // hookInstance即是我们new 出来的hook,我们注册的函数、形参等数据都会存在这个实例里的
  // 这个options参数可暂时略过,这里只要知道这个options能拿到注册的函数即可
  setup(hookInstance, options) {
    hookInstance._x = options.taps.map(tapInfo => tapInfo.fn);
  }
  // 初始化,主要是为了临时存一份options,这个options和上面的options是一样的
  init(options) {
    this.options = options;
  }
  // 参数部分
  args() {
    return this.options.args.join(',');
  }
  // 头部部分
  header() {
    return 'var _x = this._x;\n';
  }
  // 工厂类的主要方法,生成动态函数
  create(options) {
    this.init(options);
    let fn;
    switch (options.type) {
      case 'sync':
        fn = new Function(
          this.args(),
          // 这里的content方法可暂时忽略,只要知道它会调用callTapsSeries即可
          this.header() + this.content(),
        );
        break;
    }
    this.deInit();
    return fn;
  }
  // 清空临时存的options
  deInit() {
    this.options = null;
  }
  // 遍历注册的函数,生成函数内容部分
  callTapsSeries() {
    let code = '';
    for (let j = 0; j < this.options.taps.length; j++) {
      const tapContent = this.callTap(j);
      code += tapContent;
    }
    return code;
  }
  // 每次遍历生成的内容
  callTap(tapIndex) {
    let code = '';
    // 取出回调函数
    code += `var _fn${tapIndex} = _x[${tapIndex}];\n`;
    let tapInfo = this.options.taps[tapIndex];
    // 调用回调函数
    switch (tapInfo.type) {
      case 'sync':
        code += `_fn${tapIndex}(${this.args()});\n`;
        break;
    }
    return code;
  }
}

module.exports = HookCodeFactory;

SyncHook

上文中我们讲到Hook这个基类的时候,我们跳过了compile方法,这个compile方法其实是每一种hook自有的,也正是因为这点,我们的才能根据所使用的hook生成不同的函数。每一种hook自身的compile就是生成函数的规则。这是一种子类给基类(父类)扩展方法的方式。

同理的,在工厂类的代码中有一个content方法,那也是基于此原理,子类给基类扩展方法。

一般这种子类给父类扩展方法的场景主要是出现在父类完全只是用于继承,不会被实例化,因为父类并没有那些扩展的方法,实例化后调用那些方法会报错的,但子类继承了父类之后,子类拥有着那些扩展的方法,通过子类去调用就没问题。

回到正题,我们继续实现SyncHook,跟上面几个类的节奏不同,这里我们先看完整代码。

const Hook = require('./Hook');
const HookCodeFactory = require('./HookCodeFactory');
class SyncHookCodeFactory extends HookCodeFactory {
  content() {
    return this.callTapsSeries();
  }
}
const factory = new SyncHookCodeFactory();

class SyncHook extends Hook {
  compile(options) {
    // 初始化代码工厂
    factory.setup(this, options);
    return factory.create(options);
  }
}

module.exports = SyncHook;

SyncHook自身的代码很简单,一是给工厂类扩展content方法并实例化它,二是给Hook基类扩展compile方法并实例化它。

那么整个SyncHook就完成了,最后我们在入口文件中到处这个SyncHook即可。

// index.js
const SyncHook = require('./SyncHook');

module.exports = {
  SyncHook,
};

效果

我们继续使用那个例子的代码,不同的是将tapable改成我们自己写的这个。

const { SyncHook } = require('../tapable');
// 创建
// 这个name和age无实际意义,其实就是表示接受多少个参数而已
const hook = new SyncHook(['name', 'age']);
// 注册
hook.tap('1', (name, age) => {
  console.log('1', name, age);
});
hook.tap('2', (name, age) => {
  console.log('2', name, age);
});
hook.tap('3', (name, age) => {
  console.log('3', name, age);
});
debugger;
// 调用
hook.call('Rippi', 18);

同样的,我们打上debugger,看看最后生成的函数。

js如何根据需求动态生成函数?

如图所示,效果很完美,生成的函数与tapable生成的基本一致,除了我去掉的前两句外。

结尾

本文是tapable相关的第二篇文章,本文主要通过调试tapable的SyncHook代码然后从中分析出实现原理,最后动手实现了一个SyncHook。

总体上来看,SyncHook的实现比较简单,主要是学习到了这种动态生成函数的思路,最后希望大家看完都有所收获。

下集内容预告:认识异步Hook并实现它们。