找回密码
 会员注册
查看: 18|回复: 0

tapable源码解读

[复制链接]

1

主题

0

回帖

4

积分

新手上路

积分
4
发表于 2024-10-9 18:35:47 | 显示全部楼层 |阅读模式
tapable 源码解读 tapable 源码解读 魏蕊蕊@贝壳找房 贝壳产品技术 贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容 2021年03月24日 11:27 1 tapable 的介绍webpack 内部的工作流程是基于插件机制的,webpack 内部负责编译的 Compiler 对象和负责创建 bundle 的 Compilation 对象都是基于 tapable 中的钩子函数,tapable 是 webpack 内部使用的一个流程管理工具。在阅读 webpack 前需要先了解 tapable。tapable 控制钩子函数的订阅和发布。订阅发布模式旨在降低系统不同模块之间的耦合度,订阅发布模式定义了一种一对多的依赖关系,一指发布者,多指订阅者,依赖是订阅者对发布者的依赖,多个订阅者同时监听一个发布者。当发布者的状态发生变化时,会将该变化通知给订阅者,订阅者据此更新自己的状态。下面是一个同步Hook的例子。//创建实例constsyncHook=newSyncHook(["name","age"]);//注册事件syncHook.tap("1",(name,age,sex)=>{console.log("1",name,age,sex);return1;});syncHook.tap("2",(name,age)=>{console.log("2",name,age);return2;});//触发事件,让监听函数执行syncHook.call("zs",18,'男');//1zs18undefined//2zs18实例化钩子函数时接收一个数组,数组的长度是接收的参数个数,例如调用 call 方法传入的是三个参数,new SyncHook 时是两个参数,实际上 handler 只能接收到两个参数。2 tapable 源码解读源码地址(2.2.0):https://github.com/webpack/tapable2.1 总体介绍tapable 的入口文件是 lib/index.js 文件,在 index.js 文件中只是将模块作了整合导出。tapable 中主要提供了同步与异步两种钩子,下图对每一种 Hook 的功能进行简单介绍。同步与异步的钩子函数都是基于 Hook 生成一个对象,重写对象上的一些方法。本质是将我们传入的参数标准化之后,推入实例的 taps 数组中。taps 存放每次执行 tap 方法生成的 options 对象,是一个有序的列表。事件订阅方法主要包括 tap、tapAsync、tapPromise,事件触发方法主要包括 call、callAsync、promise 方法。2.2 源码方法解析源码方法解析主要从方法订阅和发布两部分讲明。在创建实例时,可以传两个参数,第一个是数组 args,第二个是 name 值。钩子函数创建一个 Hook 对象,将数据参数赋给 _args ,将 name 赋给 参数 name。构造函数指向钩子函数,同步钩子函数覆盖了 hook 的 tabAsync、tapPromise 方法,同步钩子函数不支持 tabAsync() 和 tapPromise() ,所有的钩子函数都必须重写 compile() 。2.2.1 事件订阅同步订阅使用 tap() 方法,异步订阅还可以使用tapAsync() 和 tapPromise() 方法。这三者的注册方式相同,都调用了 Hook 类中的 this._tap() 将内容配置插入到 taps 数组中,不同点是 _tap 方法中的类型值不同,tap 是 sync、tapAsync 是 async、tapPromise 是 promise。_tap(type,options,fn){if(typeofoptions==="string"){options={nameptions.trim()};}elseif(typeofoptions!=="object"||options===null){thrownewError("Invalidtapoptions");}if(typeofoptions.name!=="string"||options.name===""){thrownewError("Missingnamefortap");}if(typeofoptions.context!=="undefined"){deprecateContext();}options=Object.assign({type,fn},options);options=this._runRegisterInterceptors(options);this._insert(options);}三个注册事件的第一个参数是上面代码的 options 形参,该参数只能是一个非空字符串或者非空对象,如果该参数是字符串,改为一个 name 为传入字符串的对象,将传入的参数标准化之后重新保存在 options 参数中。调用 _runRegisterInterceptors() 方法,每次执行事件订阅方法时,传入的 options 都要经过 interceptor.register() 函数的逻辑,将拦截器的内容添加到 options 中。插入注册函数对象时,主要是通过 options 中的 before、stage 属性确定当前事件订阅的回调函数的位置(默认放在 taps 数组的最后一位),调整回调函数的执行顺序。MultiHook 中事件订阅使用 for 循环去调用 Hook.js 中的订阅事件。HookMap 中使用 for 函数去生成新的 Hook,然后再去调用 tap() 事件。2.2.2 事件发布触发事件主要使用 call、callAsync、promise 方法,都调用了 Hook 类中的 _createCall() 方法,只是 type 参数分别为 sync/async/promise,该方法直接返回了 this.compile 方法的执行结果。_createCall(type){returnthis.compile({taps:this.taps,interceptors:this.interceptors,args:this._args,type:type});}这里的 compile 方法是一个抽象方法,需要在具体的钩子函数里面去实现,根据不同的 type 生成各自的可执行函数。例如在 SyncHook.js 中的实现:classSyncHookCodeFactoryextendsHookCodeFactory{content({onError,onDone,rethrowIfPossible}){returnthis.callTapsSeries({onErrori,err)=>onError(err),onDone,rethrowIfPossible,});}}constfactory=newSyncHookCodeFactory();constCOMPILE=function(options){factory.setup(this,options);returnfactory.create(options);};compile() 使用了一个 factory 对象, factory 对象是子类的一个实例,子类都是基于 HookCodeFactory 工厂函数生成的,每个子类只定义了 content 方法。setup() 方法过滤出 taps 数组中的注册函数 fn,保存到调用对象的 _x 变量上。create() 方法根据传入钩子类型 type ,利用 Function 构造函数实例化出一个函数并返回。create()方法的逻辑如下:create(options){this.init(options);//初始化options和_argsletfn;switch(this.options.type){case"sync":fn=newFunction(this.args(),'"usestrict";\n'+this.header()+this.contentWithInterceptors());break;case"async":fn=newFunction(this.args(),'"usestrict";\n'+this.header()+this.contentWithInterceptors());break;case"promise":fn=newFunction(this.args(),code);break;}this.deinit();//重置参数,避免影响其他实例。returnfn;//返回编译生成的函数。}args() 方法生成 Function 构造函数的形参,将 _args 数组中的内容和 before、after 参数拼接在一起转为字符串。header() 方法处理全局变量,将传入 compile 方法的参数赋值一份存为局部变量。contentWithInterceptors() 方法中先检查是否有拦截器。如果有拦截器,在添加插件执行代码之前先加上拦截器。所有子类中都实现了 content() 方法,调用子类中的 content() 方法处理差异部分。例如同步或部分异步串行钩子函数调用 callTapsSeries() 方法;异步并行的钩子函数中使用 callTapsParallel() 方法;循环类的钩子函数中使用 callTapsLooping() 方法。调用 callTapsSeries() 方法的钩子函数中,从后向前遍历,把后面的回调函数字符串粘贴在当前回调函数字符串后面,要么执行 onResult() ,要么执行 onDone() 。调用 callTapsParallel() 方法的钩子函数中, _counter 是注册函数的个数。调用 callTapsLooping() 方法的钩子函数中,使用 do/while 循环,在 onResult 中改变 _loop 变量去控制是否循环,循环内部代码使用 callTapsSeries() 方法。三个方法中都调用了 callTap() 方法,根据类型 type 的不同生成不同的函数内容。3 Function执行代码例子1. 返回结果result不影响事件的执行//同步SyncHook(functionanonymous(name,age){"usestrict";var_context;var_x=this._x;var_fn0=_x[0];_fn0(name,age);var_fn1=_x[1];_fn1(name,age);})();//AsyncParallelHook(functionanonymous(name,age,_callback){"usestrict";var_context;var_x=this._x;do{var_counter=2;var_done=function(){_callback();};if(_counter0){_callback(_err0);_counter=0;}}else{if(--_counter===0)_done();}});if(_counter0){_callback(_err1);_counter=0;}}else{if(--_counter===0)_done();}});}while(false);})()//异步串行:AsyncSeriesHook(functionanonymous(name,age,_callback){"usestrict";var_context;var_x=this._x;function_next0(){var_fn1=_x[1];_fn1(name,age,function(_err1){if(_err1){_callback(_err1);}else{_callback();}});}var_fn0=_x[0];_fn0(name,age,function(_err0){if(_err0){_callback(_err0);}else{_next0();}});})()2. 返回结果 result 不为 undefined,跳过剩下所有逻辑。//同步SyncBailHook(functionanonymous(name,age){"usestrict";var_context;var_x=this._x;var_fn0=_x[0];var_result0=_fn0(name,age);if(_result0!==undefined){return_result0;}else{var_fn1=_x[1];var_result1=_fn1(name,age);if(_result1!==undefined){return_result1;}else{}}}})();//AsyncParallelBailHook(functionanonymous(name,age,_callback){"usestrict";var_context;var_x=this._x;var_results=newArray(2);var_checkDone=function(){for(vari=0;i0){if(00){if(0=_results.length){if(--_counter===0)_done();}else{var_fn1=_x[1];_fn1(name,age,function(_err1,_result1){if(_err1){if(_counter>0){if(10){if(1
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-31 05:54 , Processed in 0.641709 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表