Rollup插件源码探究

459 阅读14分钟

Rollup 插件在源码的位置:

  • src/utils/PluginCache.ts:插件缓存模块
  • src/utils/PluginContext.ts:插件上下文模块
  • src/utils/PluginDriver.ts:插件驱动器

我们知道 Rollup 的插件由许多 hook 组成,举个🌰:

plugins: [
	{
		name: 'plugin-1',
		async resolveId(source, importer, options) {},
		load(id) {},
	},
	{
		name: 'plugin-2',
		options(inputOptions) {},
	}
]

如上所示,我们定义了两个插件,前者有两个 hook,后者有一个 hook。

Rollup 的 hook 分为两大类:输入构建 hook 和输出生成 hook

前者在构建阶段运行,它们主要关注在 Rollup 处理输入文件之前的转换等操作,如 resolveId,transform 等。

后者在输出阶段运行,可以提供生成的 bundle 的信息等,如 renderChunk,writeBundle 等

PluginDriver

这是插件的核心,定义了不同的 hook 的执行方式:

  • hookFirst:异步串行,出现第一个返回值不为空的插件,就停止执行。
  • hookFirstSync:同步串行,出现第一个返回值不为空的插件,就停止执行。
  • hookParallel:异步并行,忽略返回值。
  • hookReduceArg0:异步串行,对 arg0 进行 reduce 操作,如果返回为空就停止执行,并返回 arg0。
  • hookReduceArg0Sync:同步串行,对 arg0 进行 reduce 操作,如果返回为空就停止执行,并返回 arg0。
  • hookReduceValue:异步串行,对传入的初始值进行 reduce 操作,如果返回为空就停止执行,并返回 value。
  • hookReduceValueSync:同步串行,对传入的初始值进行 reduce 操作,如果返回为空就停止执行,并返回 value。
  • hookSeq:异步串行,忽略返回值。
  • hookSeqSync:同步串行,忽略返回值。

PluginDriver 的初始化过程在 Graph 中:

Graph 是全局唯一的图,包含入口以及各种依赖的相互关系,操作方法,缓存等。是 Rollup 的核心

export default class Graph {
	constructor(options, watcher) {
    // 创建 this.pluginCache,将在下文分析

    // 初始化插件驱动器
    this.pluginDriver = new PluginDriver(this, options, options.plugins, this.pluginCache);
  }
}

接着我们整体看下 PluginDriver 的整体定义:

export class PluginDriver {
  // ...

  pluginCache; // 插件缓存
  pluginContexts = new Map<Plugin, PluginContext>(); // 插件上下文Map
  plugins; // 插件数组

  constructor() {
    // ...
  }

  // chains, first non-null result stops and returns
  hookFirst(hookName, args, replaceContext, skipped) {
    // ...
  }

  // chains synchronously, first non-null result stops and returns
  hookFirstSync(hookName, args, replaceContext) {
    // ...
  }

  // parallel, ignores returns
  hookParallel(hookName, args, replaceContext) {
    // ...
  }

  // chains, reduces returned value, handling the reduced value as the first hook argument
  hookReduceArg0(hookName, [arg0, ...rest], reduce, replaceContext) {
    // ...
  }

  // chains synchronously, reduces returned value, handling the reduced value as the first hook argument
  hookReduceArg0Sync(hookName, [arg0, ...rest], reduce, replaceContext) {
    // ...
  }

  // chains, reduces returned value to type T, handling the reduced value separately. permits hooks as values.
  hookReduceValue(hookName, initialValue, args, reduce, replaceContext) {
    // ...
  }

  // chains synchronously, reduces returned value to type T, handling the reduced value separately. permits hooks as values.
  hookReduceValueSync(hookName, initialValue, args, reduce,  replaceContext) {
    // ...
  }

  // chains, ignores returns
  hookSeq(hookName, args, replaceContext) {
    // ...
  }

  // chains synchronously, ignores returns
  hookSeqSync(hookName, args, replaceContext) {
    // ...
  }

  private runHook(hookName, args, plugin, permitValues, hookContext) {
    // ...
  }

  private runHookSync(hookName, args, plugin, hookContext) {
    // ...
  }
}

构造函数

我们目前只需要关心的是在构造函数中通过 getPluginContext 为每个 Plugin 创建了一个 Context

constructor(
  graph, // 图
  options, // 配置
  userPlugins, // 用户配置
  pluginCache, // 插件缓存
  basePluginDriver // 创建 OutputPluginDriver 时传入的 InputPluginDriver
) {
  // 插件缓存
  this.pluginCache = pluginCache;
  // 创建 FileEmitter 实例,实现了一些文件相关的方法,比如 emitFile 生产一个包含在构建输出中的新文件
  this.fileEmitter = new FileEmitter(
    graph,
    options,
    basePluginDriver &amp;&amp; basePluginDriver.fileEmitter
  );
  // ...
  
  // 合并所有的 plugins
  this.plugins = userPlugins.concat(basePluginDriver ? basePluginDriver.plugins : []);
  
  const existingPluginNames = new Set<string>();
  // 遍历插件,为每个插件创建 Context
  for (const plugin of this.plugins) {
    this.pluginContexts.set(
      plugin,
      getPluginContext(plugin, pluginCache, graph, options, this.fileEmitter, existingPluginNames)
    );
  }
  
  // 如果存在 basePluginDriver,则说明当前的 PluginDriver 是用来处理 Output 相关的插件
  // 因此如果存在 Input 的 Hook 则会警告
  if (basePluginDriver) {
    for (const plugin of userPlugins) {
      for (const hook of inputHooks) {
        if (hook in plugin) {
          options.onwarn(errInputHookInOutputPlugin(plugin.name, hook));
        }
      }
    }
  }
}

runHook

异步执行 hook

private runHook(
  hookName, // hook 名
  args, // 调用 hook 时传入的参数
  plugin, // 当前插件
  permitValues, // 布尔值,当为 true 时,允许 hook 不是一个函数且直接作为返回值返回,这只在 hookReduceValue 用到
  hookContext // 如果传入的话则会替换插件的 Context
) {
  // 从插件中拿到 hook
  const hook = plugin[hookName];
  if (!hook) return undefined;

  // 拿到当前插件的 Context,以及执行替换 Context 逻辑
  let context = this.pluginContexts.get(plugin)!;
  if (hookContext) {
    context = hookContext(context, plugin);
  }

  // 用来保存当前hook执行时的信息,可以在 process.on('exit', () => {}) 回调中打印出未执行完的hook信息
  let action = null;
  return Promise.resolve()
    .then(() => {
      // 如果hook不是一个函数且permitValues为true,那么将hook直接返回,否则报错
      if (typeof hook !== 'function') {
        if (permitValues) return hook;
        return throwInvalidHookError(hookName, plugin.name);
      }
      // 执行hook,绑定this为Context,且传入args参数
      const hookResult = hook.apply(context, args);

      // 如果返回值为空或者不是一个Promise,那么直接返回结果
      if (!hookResult || !hookResult.then) {
        return hookResult;
      }

      // 通过保存当前 hook 执行时的信息,以便当 unfulfilled promise 导致 rollup 崩溃,或者以成功的状态码 0 退出时,
      // 可以在 process.on('exit', () => {}) 回调中打印出未执行完的hook信息
      action = [plugin.name, hookName, args];
      addUnresolvedAction(action);

      const promise = Promise.resolve(hookResult);
      return promise.then(() => {
        // promise resolve 时清除action信息
        resolveAction(action);
        return promise;
      });
    })
    .catch(err => {
      if (action !== null) {
        // 错误被处理时也清除action信息
        resolveAction(action);
      }
      return throwPluginError(err, plugin.name, { hook: hookName });
    });
}

异步执行 Hook 其实非常简单,主要是做了三件事:

  • 如果hook不是一个函数且 permitValues 为true,那么将hook直接返回,否则报错
  • 通过 hook.apply(context, args) 执行 hook
  • 管理hook的上下文信息

runHookSync

同步执行 hook

private runHookSync(
  hookName, // hook 名
  args, // 调用 hook 时传入的参数
  plugin, // 当前插件
  hookContext // 如果传入的话则会替换插件的 Context
) {
  // 从插件中拿到 hook
  const hook = plugin[hookName];
  if (!hook) return undefined as any;

  // 拿到当前插件的 Context,以及执行替换 Context 逻辑
  let context = this.pluginContexts.get(plugin)!;
  if (hookContext) {
    context = hookContext(context, plugin);
  }

  try {
    // 如果 hook 不是函数则报错
    if (typeof hook !== 'function') {
      return throwInvalidHookError(hookName, plugin.name);
    }
    // 执行hook,绑定this为Context,且传入args参数
    return hook.apply(context, args);
  } catch (err: any) {
    return throwPluginError(err, plugin.name, { hook: hookName });
  }
}

其实对比 runHook 我们可以发现,前者只不过多了一些上下文信息的处理。

hookFirst

异步串行,出现第一个返回值不为空的插件,就停止执行。

// chains, first non-null result stops and returns
hookFirst(
  hookName, // hook 名,例如 resolveId
  args, // 参数
  replaceContext, // 如果传入的话则会替换那个插件的 Context
  skipped // 一个 Set<Plugin> 数据结构,指定哪些插件可以跳过执行
) {
  // 初始化一个 promise
  let promise = Promise.resolve(undefined);
  // 遍历插件
  for (const plugin of this.plugins) {
    // 是否需要跳过插件
    if (skipped && skipped.has(plugin)) continue;
	 // 串行promise
    promise = promise.then(result => {
      // 返回非null时,停止运行,返回结果
      if (result != null) return result;
      // 异步执行 hook
      return this.runHook(hookName, args, plugin, false, replaceContext);
    });
  }
  return promise;
}

比如 resolveId 这个 hook 就是 hookFirst 的,我们来看下它的调用方式:

pluginDriver.hookFirst(
  'resolveId',
  [source, importer, { custom: customOptions, isEntry }],
  replaceContext,
  skipped
);

hookFirstSync

同步串行,出现第一个返回值不为空的插件,就停止执行。

// chains synchronously, first non-null result stops and returns
hookFirstSync(
  hookName, // hook 名
  args, // 参数
  replaceContext // 如果传入的话则会替换那个插件的 Context
) {
  for (const plugin of this.plugins) {
    // 同步执行 hook
    const result = this.runHookSync(hookName, args, plugin, replaceContext);
    if (result != null) return result;
  }
  return null;
}

hookParallel

异步并行,忽略返回值。主要是利用了 Promise.all 实现

// parallel, ignores returns
hookParallel(
  hookName, // hook 名
  args, // 参数
  replaceContext // 如果传入的话则会替换那个插件的 Context
) {
  const promises = [];
  for (const plugin of this.plugins) {
    const hookPromise = this.runHook(hookName, args, plugin, false, replaceContext);
    if (!hookPromise) continue;
    promises.push(hookPromise);
  }
  // Promise.all
  return Promise.all(promises).then(() => {});
}

hookReduceArg0

异步串行,对 arg0 进行 reduce 操作,如果返回为空就停止执行,并返回 arg0。

// chains, reduces returned value, handling the reduced value as the first hook argument
hookReduceArg0(
  hookName, // hook 名
  [arg0, ...rest], // 取出传入的数组的第一个参数,将剩余的置于一个数组中
  reduce, // reduce 处理函数,传入 arg0 和 hook 的返回值作为参数,并返回处理后的 arg0
  replaceContext // 如果传入的话则会替换那个插件的 Context
) {
  // 创建一个 arg0 的 promise
  let promise = Promise.resolve(arg0);
  
  // 遍历插件
  for (const plugin of this.plugins) {
    // 串行promise
    promise = promise.then(arg0 => {
      const args = [arg0, ...rest];
      // 异步执行 hook,传入 args 作为参数
      const hookPromise = this.runHook(hookName, args, plugin, false, replaceContext);
      // hook 返回了空,则停止执行
      if (!hookPromise) return arg0;
      return hookPromise.then(result =>
        // 调用 reduce 函数,传入 arg0 和 hook的返回值作为参数,reduce 的返回值将作为下一次的 arg0
        reduce.call(this.pluginContexts.get(plugin), arg0, result, plugin)
      );
    });
  }
  return promise;
}

这个 hook 稍微有点绕,画个图理解:

graph LR
    A["[arg0, ...rest]"] -->| 入参 | B(前一个hook)
    B -->| hook的返回值和arg0入参 | C(reduce)
    C -->| reduce返回值作为新的arg0 | D("[arg0, ...rest]")
		D -->| 入参 | E(下一个hook)

hookReduceArg0Sync

同步串行,对 arg0 进行 reduce 操作,如果返回为空就停止执行,并返回 arg0。

// chains synchronously, reduces returned value, handling the reduced value as the first hook argument
hookReduceArg0Sync(
  hookName, // hook 名
  [arg0, ...rest], // 取出传入的数组的第一个参数,将剩余的置于一个数组中
  reduce, // reduce 处理函数,传入 arg0 和 hook 的返回值作为参数,并返回处理后的 arg0
  replaceContext // 如果传入的话则会替换那个插件的 Context
) {
  // 遍历插件
  for (const plugin of this.plugins) {
    const args = [arg0, ...rest];
    // 同步执行hook
    const result = this.runHookSync(hookName, args, plugin, replaceContext);
    // 调用 reduce 函数,传入 arg0 和 hook的返回值作为参数,reduce 的返回值将作为下一次的 arg0
    arg0 = reduce.call(this.pluginContexts.get(plugin), arg0, result, plugin);
  }
  return arg0;
}

hookReduceValue

异步串行,对传入的初始值进行 reduce 操作,如果返回为空就停止执行,并返回 value。

// chains, reduces returned value to type T, handling the reduced value separately. permits hooks as values.
hookReduceValue(
  hookName, // hook 名
  initialValue, // 初始值
  args, // hook 的参数
  reduce, // reduce 处理函数,传入 value 和 hook 的返回值作为参数,reduce 的返回值作为下一个 value
  replaceContext //  如果传入的话则会替换那个插件的 Context
) {
  // 创建一个 initialValue 的 promise
  let promise = Promise.resolve(initialValue);
  // 遍历插件
  for (const plugin of this.plugins) {
    promise = promise.then(value => {
      // 异步执行 hook,注意这里第四个参数是 true
      const hookPromise = this.runHook(hookName, args, plugin, true, replaceContext);
      // hook 返回空,则终止执行,返回当前的 value
      if (!hookPromise) return value;
      return hookPromise.then(result =>
        // 调用 reduce 处理函数,传入 value 和 hook 的返回值作为参数,reduce 的返回值作为下一个 value
        reduce.call(this.pluginContexts.get(plugin), value, result, plugin)
      );
    });
  }
  return promise;
}

hookReduceArg0 不一样的是,这个 reduce 的是 value:

graph LR
    A[args] -->|入参| B(前一个hook)
    B -->|hook的返回值和value入参| C(reduce)
    C -->|reduce返回值作为新的value| D(args)
		D -->|入参| E(下一个hook)

另外还有一点值得一提的是,这个 hook 执行时传入的第四个参数是 true,这意味着此 hook 可以不作为函数。

hookReduceValueSync

同步串行,对传入的初始值进行 reduce 操作,如果返回为空就停止执行,并返回 value。

// chains synchronously, reduces returned value to type T, handling the reduced value separately. permits hooks as values.
hookReduceValueSync(
  hookName, // hook 名
  initialValue, // value 初始值
  args, // hook 的参数
  reduce, // reduce 处理函数,传入 value 和 hook 的返回值作为参数,reduce 的返回值作为下一个 value
  replaceContext //  如果传入的话则会替换那个插件的 Context
) {
  let acc = initialValue;
  // 遍历插件
  for (const plugin of this.plugins) {
    // 同步执行hook
    const result = this.runHookSync(hookName, args, plugin, replaceContext);
    // 调用 reduce 处理函数,传入 value 和 hook 的返回值作为参数,reduce 的返回值作为下一个 value
    acc = reduce.call(this.pluginContexts.get(plugin), acc, result, plugin);
  }
  return acc;
}

hookSeq

异步串行,忽略返回值。

// chains, ignores returns
hookSeq(
  hookName, // hook 名
  args, // hook 的参数
  replaceContext //  如果传入的话则会替换那个插件的 Context
) {
  let promise = Promise.resolve();
  for (const plugin of this.plugins) {
    // 串行 promise
    promise = promise.then(
      // 异步执行 hook
      () => this.runHook(hookName, args, plugin, false, replaceContext)
    );
  }
  return promise;
}

hookSeqSync

同步串行,忽略返回值。

// chains synchronously, ignores returns
hookSeqSync(
  hookName, // hook 名
  args, // hook 的参数
  replaceContext //  如果传入的话则会替换那个插件的 Context
) {
  // 遍历插件
  for (const plugin of this.plugins) {
    // 同步执行hook
    this.runHookSync(hookName, args, plugin, replaceContext);
  }
}

PluginContext

通过上文我们知道在 PluginDriver 的构造函数中通过 getPluginContext 为每一个 plugin 创建了一个 context。

const existingPluginNames = new Set<string>();
for (const plugin of this.plugins) {
  this.pluginContexts.set(
    plugin,
    getPluginContext(plugin, pluginCache, graph, options, this.fileEmitter, existingPluginNames)
  );
}

创建出的 context 定义了一些方法,可以在 hook 执行阶段通过 this.xxx 访问,我们来看下 getPluginContext 的定义:

export function getPluginContext(
  plugin, // 插件
  pluginCache, // 插件缓存
  graph, // 图
  options, // 配置
  fileEmitter, // 文件管理类
  existingPluginNames // 已存在的插件
) {
  // 缓存的处理,下文将展开分析...
  let cacheInstance;

  const context = {
    // 一些废弃方法的定义,如果访问将发出警告
	
    // 动态添加对文件的监听
    addWatchFile(id) {...},
	
    // 缓存实例 
    cache: cacheInstance,
	
    // 创建一个文件到最后的打包中,并且会返回一个 referenceId
    emitFile: fileEmitter.emitFile.bind(fileEmitter),
	
    // 抛出一个错误
    error(err) {...},

    // 获取通过 emitFile 创建的 chunk 或资源的文件名,文件名将相对于 `outputOptions.dir` 
    getFileName: fileEmitter.getFileName,
	
    // 获取所有模块完整路径
    getModuleIds: () => graph.modulesById.keys(),
	
    // 获取模块信息,搭配 getModuleIds 使用
    getModuleInfo: graph.getModuleInfo,
	
    // 获取所有被监听变化的文件路径id
    getWatchFiles: () => Object.keys(graph.watchFiles),
	
    // 加载并解析给定id对应的模块
    load(resolvedId) {...},
	
    // 元数据
    meta: {
      rollupVersion,
      watchMode: graph.watchMode
    },
	
    // 用于编译代码,并返回ast
    parse: graph.contextParse.bind(graph),
	
    // 用于解析传入参数的完整路径,它会经过所有的 `resolveId hook` 处理
    resolve(source, importer, { custom, isEntry, skipSelf } = BLANK) {...},
	
    // 设置assets资源的代码,例如我们通过 `this.emitFile` 创建一个文件,返回了referanceId,可以通过这个id修改资源代码
    setAssetSource: fileEmitter.setAssetSource,
	
    // 发出一个警告
    warn(warning) {...}
  };
  return context;
}

可以看到 getPluginContext 干了两件事:

  • 处理插件缓存逻辑,这将在下文分析
  • 创建一个 context 对象并返回,此对象定义了一些可以在 hook 执行时通过 this.xxx 访问的方法(hook 执行时的 this 绑定给了 context)

context 定义的方法详细的使用可以移步官方文档

PluginCache

插件缓存提供了当多次构建时相同插件的不同实例间共享数据的能力,在进入源码之前,我们先看下如何使用缓存:

function testPlugin() {
  return {
    name: "test-plugin",
    buildStart() {
      if (!this.cache.has("name")) {
        this.cache.set("name", "samuelsli");
      } else {
        // samuelsli
        console.log(this.cache.get("name"));
      }
    },
  };
}
let cache;
async function build() {
  const chunks = await rollup.rollup({
    input: "src/main.js",
    plugins: [testPlugin()],
    // 需要传递上次的打包结果
    cache,
  });
  cache = chunks.cache;
}

build().then(() => {
  build();
});

可以看到当我们执行多次 rollup.rollup() 构建时,可以通过 cache 访问上一次设置的数据。

接下来我们进入源码来学习实现原理,还记得我们在 Graph 构造函数中初始化 PluginDriver 时传入的 this.pluginCache 吗?他的数据结构如图所示:

graph LR
    A[pluginCache对象] --> B(key: 插件名)
	    A[pluginCache对象] --> C[value: cache对象]
    C[value: cache对象] --> D(key: cache名)
	    C[value: cache对象] --> E["value: [0, value]"]
	    E["value: [0, value]"] --> F{第0项代表的是访问次数是一个累加值<br>第1项代表的是真实的value值}

现在让我们结合上面的例子以及源码来走一遍流程:

export default class Graph {
	constructor(options, watcher) {
    // 如果禁用了缓存,那么 this.pluginCache 就是 undefined
    if (options.cache !== false) {
      // 处理 cacheModule 相关,暂时不用关心...
	   
      // options.cache.plugins 就是 pluginCache,下文会分析
      // 第一次运行时 pluginCache 是 undefined,则创建了一个空对象
      this.pluginCache = options.cache?.plugins || Object.create(null);

      // 遍历每个插件的 cache
      for (const name in this.pluginCache) {
        const cache = this.pluginCache[name];
        // 累加此插件的 cache 的访问次数
        for (const value of Object.values(cache)) value[0]++;
      }
    }

    // 初始化插件驱动器
    this.pluginDriver = new PluginDriver(this, options, options.plugins, this.pluginCache);
  }
}

第一次运行时,pluginCache 是 undefined,那么此时通过 Object.create(null) 创建了一个空对象并赋值给 this.pluginCache

graph LR
    A[pluginCache对象] --> B{空对象}

接着第二步是遍历每个插件的 cache 对象,并对 cache 进行累加操作。第一次构建时这里执行不到。

接着我们通过 new PluginDriver 初始化插件驱动器,在插件驱动器的构造函数中,我们会通过 getPluginContext 为每个 plugin 创建 context,下面进入 getPluginContext 逻辑展开我们刚刚略过的代码瞅一瞅:

export function getPluginContext(
  plugin, // 插件
  pluginCache, // 插件缓存
  graph, // 图
  options, // 配置
  fileEmitter, // 文件管理类
  existingPluginNames // 已存在的插件
) {
  // 是否可缓存
  let cacheable = true;
  if (typeof plugin.cacheKey !== 'string') {
    if (
      // 构建阶段的插件无命名时的 name 会以 ANONYMOUS_PLUGIN_PREFIX 开头
      plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) ||
      // 输出阶段的插件无命名时的 name 会以 ANONYMOUS_OUTPUT_PLUGIN_PREFIX 开头
      plugin.name.startsWith(ANONYMOUS_OUTPUT_PLUGIN_PREFIX) ||
      // 有命名冲突插件的情况
      existingPluginNames.has(plugin.name)
    ) {
      // 以上三种情况时是不可缓存的
      cacheable = false;
    } else {
      // 防止插件命名冲突
      existingPluginNames.add(plugin.name);
    }
  }

  // 对缓存进行操作,get,set,has,delete
  let cacheInstance;
  if (!pluginCache) { // 没有 pluginCache 时说明我们配置了 options.cache 为 falsy
    cacheInstance = NO_CACHE;
  } else if (cacheable) { // 可缓存时
    const cacheKey = plugin.cacheKey || plugin.name;
    // 通过 Object.create(null) 创建缓存对象,并传入 createPluginCache 方法
    // createPluginCache 提供了一个操作缓存对象的能力:get,set,has,delete
    cacheInstance = createPluginCache(
      pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null))
    );
  } else { // 不可缓存时的警告处理
    cacheInstance = getCacheForUncacheablePlugin(plugin.name);
  }

  const context = {
    // 缓存实例 
    cache: cacheInstance,
    // ...
  };
  return context;
}

首先是当插件没有定义 cacheKey 时会判断插件是否可缓存,以下三种情况不可缓存:

  • 构建阶段的无命名的插件
  • 输出阶段的无命名的插件
  • 存在命名冲突的插件

当此插件可缓存时,我们通过 Object.create(null) 创建了一个缓存对象并保存到了 pluginCache 中:

graph LR
    A[pluginCache对象] --> B(key: 'test-plugin')
	    A[pluginCache对象] --> C[value: 空对象]

紧接着我们将缓存对象传入了一个 createPluginCache 的方法中:

export function createPluginCache(cache) {
  return {
    delete(id) {
      return delete cache[id];
    },
    get(id) {
      const item = cache[id];
      if (!item) return undefined;
      item[0] = 0; // 当开发者主动访问时,累计器归0
      return item[1];
    },
    has(id) {
      const item = cache[id];
      if (!item) return false;
      item[0] = 0; // 当开发者主动访问时,累计器归0
      return true;
    },
    set(id, value) {
      cache[id] = [0, value];  // 数组,第0项代表累加器,第1项存储真实的value值
    }
  };
}

可以看到 createPluginCache 利用闭包特性返回了一个可操作缓存对象的对象,此对象提供了四个方法分别是:delete,get,has,set

回到我们的例子中,当通过 this.cache.set("name", "samuelsli") 操作后,我们的数据结构变成了这样:

graph LR
    A[pluginCache对象] --> B(key: 'test-plugin')
	    A[pluginCache对象] --> C[value: cache对象]
    C[value: cache对象] --> D(key: 'name')
	    C[value: cache对象] --> E["value: [0, 'samuelsli']"]

此时我们的第一次构建就结束了。

在上述例子中当第一次构建结束时,我们通过 chunks.cache 访问实际上执行的是 Graph 实例的 getCache 方法:

export default class Graph {
  // ...
  
  getCache() {
    // 如果累加值超过配置的 experimentalCacheExpiry 限制,那么删除缓存
    for (const name in this.pluginCache) {
      const cache = this.pluginCache[name];
      let allDeleted = true;
      for (const [key, value] of Object.entries(cache)) {
        if (value[0] >= this.options.experimentalCacheExpiry) delete cache[key];
        else allDeleted = false;
      }
      if (allDeleted) delete this.pluginCache[name];
    }

    return {
      // ...
      plugins: this.pluginCache
    };
  }

}

简单来说我们通过 chunks.cache 访问的实际上就是 this.pluginCache

当第二次构建开始时,我们将上一次的 this.pluginCache 传入,流程和上面分析的一样,笔者这里就不重复赘述了,交给读者自行带入流程分析吧~

小结

这一章我们分析了 PluginDriver 核心文件,了解了 Rollup 的不同 hook 都有哪些执行方式。

还分析了插件缓存 PluginCache 的用法和原理,以及简单介绍了 PluginContext 上下文。

后续会分析 Rollup 各个 hook 的调用时机,以及整体的构建流程分析,尽情期待。