webpack插件开发必会Tapable

简介: webpack插件开发必会Tapable

tapable是官方提供的一个核心工具,不仅可以用于wepback当中,还可以使用在任何你需要的地方,webpack很多类都扩展自tapable,所以学习它对于学习webpack有很大的能帮助,文档在这=>Tapable


认识Tapable


官方对tapable的定义是钩子,业界很多写tapable的觉得它是发布订阅模式,它确实很像发布订阅模式,但是不完全是,至于官方说的钩子,个人感觉在webpack内部可以称作钩子,因为是官方定义的,所以肯定是按照设计的想法来使用的,如果是你自己使用,操作不当可能就不是钩子了,毕竟只是一个工具,你拿扳手当锤子也没人说你什么,以上是个人见解,下面附上一些验证的思路。


不喜欢论证,喜欢看使用的可以跳过这个小标题,到第二个标题 tapable使用详解


1. 发布订阅模式


先来看看什么是发布订阅,发布订阅是需要分为两个部分,一个是发布,一个是订阅。


  • 发布

发布者就是我,我写下这篇文章,点击发布就是发布了。

  • 订阅

订阅者是你,但是你要先关注我,关注我之后我发布文章就会通知你。

这个中间还需要有一个平台来维护我们之间的关系,这个就是掘金,一顿分析下来,模型就长下面这样,图画的有点丑,应该不影响观看

image.png

对应转化成代码应该是下面这样的:


/**
 * 发布订阅模式
 */
// 掘金是调度中心
class Jujin {
    // 掘金的所有用户
    users = {};
    // 注册用户
    registerUser(userInfo) {
        const user = new User(userInfo);
        // 内部注册的用户才有权限订阅
        user.subscribe = (uuid, callback) => {
            user.subscribes[uuid] = callback;
        }
        // 内部注册的用户才有权限发布消息
        user.publish = (message) => {
            this.#publish(user.uuid, message);
        }
        this.users[user.uuid] = user;
        return user;
    }
    #publish(uuid, message) {
        const users = Object.values(this.users);
        for (let user of users) {
            const callback = user.subscribes[uuid];
            if (callback) {
                callback(message);
            }
        }
    }
}
class User {
    constructor(userInfo) {
        this.name = userInfo.name;
        this.age = userInfo.age;
        this.uuid = Math.random().toString(36).substr(2);
        // 订阅的用户
        this.subscribes = {};
    }
}
// 掘金上线啦
const jujin = new Jujin();
// 我在掘金上注册了一个账号
const 田八 = jujin.registerUser({
    name: '田八',
    age: 18
});
// 你在掘金上注册了一个账号
const 你 = jujin.registerUser({
    name: '你',
    age: 18
});
// 你关注了我
你.subscribe(田八.uuid, (message) => {
    console.log(message);
});
// 我发布了一篇文章
田八.publish('你好,我是田八!');
// 路人甲在掘金上注册了一个账号
const 路人甲 = jujin.registerUser({
    name: '路人甲',
    age: 18
});
// 路人甲发布了一篇文章
路人甲.publish('你好,我是路人甲!');
// 可惜路人甲没有人关注,所以没有人看到他的文章

执行结果

image.png

代码不是正儿八经的发布订阅模式写的,但是思想还是差不多的,主要是为了还原我自己画的图,正儿八经的发布订阅模式有三个模块,一个是发布者,一个是订阅者,一个调度中心,我这里发布者和订阅者合并到一起了,调度中心还分管着权限。


这里主要看使用方式,最典型的就是domevents事件:


const btn = document.getElementById('button');
btn.addEventListener('click', () => {
    console.log('订阅点击事件')
});
// 发布点击事件
btn.click();

等会再来讨论这个代码,接下来看看钩子。


2. 钩子


钩子函数听得比较多,主要来源是React,还有Vue3composition api,钩子函数是一种消息处理机制,本质是用来处理系统消息的,通过应用系统调用分配,将其挂入应用系统,看看百度百科的解释(不需要你看文档,稍微会点百度也可以知道):钩子


来看上面写的其实就知道钩子是和应用程序挂钩的,是由应用程序提供的,简单的实现一下:


class HooksApp {
    hooks = {
        'onBeforeCreated': [],
        'onCreated': [],
        'onBeforeDestroyed': [],
        'onDestroyed': []
    };
    onHooks(hookName, callback) {
        if (this.hooks[hookName]) {
            this.hooks[hookName].push(callback);
        }
    }
    created() {
        this.hooks.onBeforeCreated.forEach((callback) => {
            callback();
        });
        // 创建需要一秒钟
        const now = new Date();
        while (new Date() - now < 1000) ;
        this.hooks.onCreated.forEach((callback) => {
            callback();
        });
    }
    update() {
        console.log('我有一个update,我不提供钩子!')
    }
    destroy() {
        this.hooks.onBeforeDestroyed.forEach((callback) => {
            callback();
        });
        // 销毁需要一秒钟
        const now = new Date();
        while (new Date() - now < 1000) ;
        this.hooks.onDestroyed.forEach((callback) => {
            callback();
        });
    }
}
const hooksApp = new HooksApp();
// 注册两次 onBeforeCreated 钩子
hooksApp.onHooks('onBeforeCreated', () => {
    console.log('onBeforeCreated1');
});
hooksApp.onHooks('onBeforeCreated', () => {
    console.log('onBeforeCreated2');
});
// 注册 onCreated 钩子
hooksApp.onHooks('onCreated', () => {
    console.log('onCreated3');
});
// 注册 onBeforeDestroyed 钩子
hooksApp.onHooks('onBeforeDestroyed', () => {
    console.log('onBeforeDestroyed4');
});
// 注册 onDestroyed 钩子
hooksApp.onHooks('onDestroyed', () => {
    console.log('onDestroyed5');
});
hooksApp.created();
hooksApp.update();
hooksApp.destroy();

执行结果

image.png

这个代码没写多少注释,而且这个代码也很简单,钩子也比较简单好理解,钩子的数量多寡要看应用程序的开发者提供多少,像我这个就不提供update的钩子,你就不能干预我update的流程。


tapable


来看看tapable的使用,这个就没有什么源码和设计了,直接是使用了。


const { SyncHook } = require("tapable");
// 实例化同步钩子
const syncHook = new SyncHook(["name", "age"]);
// 注册事件
syncHook.tap('abc', (name, age) => {
    console.log(name, age);
})
// 注册事件
syncHook.tap('def', (name, age) => {
    console.log(name, age);
})
// 注册事件
syncHook.tap('abc', (name, age) => {
    console.log(name, age);
})
// 触发事件
syncHook.call('zhangsan', 18);

执行结果

image.png

三者的区别


  1. 使用方式


  • 发布订阅模式需要先订阅,才能收到订阅消息;
  • 钩子要先注册钩子,才能在触发钩子对应的回调;
  • tapable需要注册事件才能触发对应的回调;
  • 代码书写上大致相同,不分伯仲;


  1. 系统架构


  • 发布订阅模式主要是依靠调度中心发送通知,需要有一个发布者和订阅者配合使用;
  • 钩子主要是应用系统级的事件回调,需要应用接入者和对应的钩子对接,钩子的个数和种类由应用系统决定;
  • tapable是一个工具包,将事件处理抽象出来,形成一个单独的模块,这个模块是钩子还是发布订阅先不下结论。


  1. 使用效果


  • 发布订阅模式下,只有订阅者才能收到发布者发布的消息;
  • 钩子下,只有注册了对应的钩子才能触发对应的事件;
  • tapable下,主要注册了这个类型的,都可以触发事件;


这个可以参考我上面截图的运行效果,也可以自己运行一下上面的代码对比一下结果。


结论


现在我讲一下我目前看到的结果,tapable肯定不是发布订阅模式,只是长得比较像而已,来看看下面的代码对比


// 发布订阅模式使用
const btn = document.querySelector('button');
btn.addEventListener('click', () => {
    console.log('click');
});
btn.addEventListener('dblclick', () => {
    console.log('click');
});
btn.click();
// 运行结果:
// click
// tapable使用
const { SyncHook } = require("tapable");
const syncHook = new SyncHook();
syncHook.tap('click', () => {
    console.log('click');
});
syncHook.tap('dblclick', () => {
    console.log('dblclick');
});
syncHook.call();
// 运行结果:
// click
// dblclick


上面的代码可以说几乎是一样的了,但是运行结果却是截然不同,所以肯定不是发布订阅模式了,网上的博文是是发布订阅模式不知道从哪看的。


再来看看钩子,其实和钩子很像,使用方式很像,架构也很像,但是它可以和钩子一样,也可以不一样,具体就要看怎么使用的了,钩子就拿我上面代码写的示例:


const hooksApp = new HooksApp();
// 注册两次 onBeforeCreated 钩子
hooksApp.onHooks('onBeforeCreated', () => {
    console.log('onBeforeCreated1');
});
hooksApp.onHooks('onBeforeCreated', () => {
    console.log('onBeforeCreated2');
});
// 注册 onCreated 钩子
hooksApp.onHooks('onCreated', () => {
    console.log('onCreated3');
});
// 注册 onBeforeDestroyed 钩子
hooksApp.onHooks('onBeforeDestroyed', () => {
    console.log('onBeforeDestroyed4');
});
// 注册 onDestroyed 钩子
hooksApp.onHooks('onDestroyed', () => {
    console.log('onDestroyed5');
});
// 这里需要调用两个方法
hooksApp.created();
hooksApp.destroy();
// 运行结果:
// onBeforeCreated1
// onBeforeCreated2
// onCreated3
// onBeforeDestroyed4
// onDestroyed5
// tapable使用
const { SyncHook } = require("tapable");
const syncHook = new SyncHook();
syncHook.tap('onBeforeCreated', () => {
    console.log('onBeforeCreated1');
});
syncHook.tap('onBeforeCreated', () => {
    console.log('onBeforeCreated2');
});
syncHook.tap('onCreated', () => {
    console.log('onCreated');
});
syncHook.tap('onBeforeDestroyed', () => {
    console.log('onBeforeDestroyed');
});
syncHook.tap('onDestroyed', () => {
    console.log('onDestroyed');
});
// 这里只调用了一次
syncHook.call();
// 运行结果:
// onBeforeCreated1
// onBeforeCreated2
// onCreated
// onBeforeDestroyed
// onDestroyed

使用钩子如果要运行完所有的注册的钩子事件,需要将应用的整个涉及到钩子的流程都执行一遍,而tapable只需要调用一次call方法就可以了,而且tapable一次调用,所有的注册的事件全部执行,没有流程之分。


那么如果需要tapable实现和钩子一样的效果怎么办?那就创建多个实例呗,所以这也是我说为什么也不完全是钩子的原因,现在还是用我上面钩子的示例,使用tapable改造一下:


const {SyncHook} = require('tapable');
class HooksApp {
    hooks = {
        'onBeforeCreated': new SyncHook(),
        'onCreated': new SyncHook(),
        'onBeforeDestroyed': new SyncHook(),
        'onDestroyed': new SyncHook()
    };
    created() {
        this.hooks.onBeforeCreated.call();
        // 创建需要一秒钟
        const now = new Date();
        while (new Date() - now < 1000) ;
        this.hooks.onCreated.call();
    }
    update() {
        console.log('我有一个update,我不提供钩子!')
    }
    destroy() {
        this.hooks.onBeforeDestroyed.call();
        // 销毁需要一秒钟
        const now = new Date();
        while (new Date() - now < 1000) ;
        this.hooks.onDestroyed.call();
    }
}
const hooksApp = new HooksApp();
hooksApp.hooks.onBeforeCreated.tap('onBeforeCreated', () => {
    console.log('onBeforeCreated')
});
hooksApp.hooks.onCreated.tap('onCreated', () => {
    console.log('onCreated')
});
hooksApp.hooks.onBeforeDestroyed.tap('onBeforeDestroyed', () => {
    console.log('onBeforeDestroyed')
});
hooksApp.hooks.onDestroyed.tap('onDestroyed', () => {
    console.log('onDestroyed')
});
hooksApp.created();
hooksApp.destroy();
// 运行结果:
// onBeforeCreated
// onCreated
// onBeforeDestroyed
// onDestroyed

到这里,不知道大家有没有一个疑惑,就是使用tapable时,前面的字符串应该传什么好?我也有这个疑问,我还找过资料,看了SyncHooks的源码,发现这个玩意没有被使用到,这一块就留个疑问,毕竟自己也是刚学,应该是很重要的,在源码中是必填的。


tapable使用详解


上面的论证的案例里使用的是最简单的SyncHooks,下面是完整的列表:


名称 解释
SyncHook 同步钩子
SyncBailHook 同步熔断钩子
SyncWaterfallHook 同步瀑布流钩子
SyncLoopHook 同步循环钩子
AsyncParallelHook 异步并行钩子
AsyncParallelBailHook 异步并行熔断钩子
AsyncSeriesHook 异步串行钩子
AsyncSeriesBailHook 异步串行熔断钩子
AsyncSeriesLoopHook 异步串行循环钩子
AsyncSeriesWaterfallHook 异步串行瀑布流钩子


SyncHook


const {SyncHook} = require('tapable');
// 实例化时可以传入参数,参数为数组,数组中的每一项为字符串,表示参数的名称
const syncHook = new SyncHook(['name', 'age']);
syncHook.tap('1', (name, age) => {
    console.log(1, name, age);
});
console.log('开始执行')
syncHook.call('panda', 18);
console.log('执行第一个结束');
syncHook.call('monkey', 80);
console.log('执行第二个结束');
// 执行结果:
// 开始执行
// 1 panda 18
// 执行第一个结束
// 1 monkey 80
// 执行第二个结束

同步钩子是最简单最好理解的,这个就不过多解读了,看的再多不如自己亲手尝试一下。


SyncBailHook


const {SyncBailHook} = require('tapable');
// 同步熔断钩子,当某个监听函数返回值不为undefined时,后续的监听函数不再执行
const syncBailHook = new SyncBailHook();
syncBailHook.tap('1', () => {
    console.log(1);
});
syncBailHook.tap('2', () => {
    console.log(2);
    // 返回值非 undefined 时,后续的监听函数不再执行
    return 2;
});
syncBailHook.tap('3', () => {
    console.log('这个不会执行');
});
syncBailHook.call();
// 执行结果:
// 1
// 2

同步熔断钩子,和SyncHook使用方式相同,不同的是如果返回值非undefined时,后面注册的监听函数都不会再执行。


SyncWaterfallHook


const {SyncWaterfallHook} = require('tapable');
// 同步串行钩子,监听函数的返回值会作为参数传递给下一个监听函数
const syncWaterfallHook = new SyncWaterfallHook(['name', 'age']);
syncWaterfallHook.tap('1', (name, age) => {
    console.log(1, name, age);
    return {
        name: 'panda',
        age: 18
    }
})
syncWaterfallHook.tap('2', (data) => {
    console.log(2, data.name, data.age);
    return {
        name: 'monkey',
        age: 80
    }
});
syncWaterfallHook.tap('3', (data) => {
    console.log(3, data.name, data.age);
});
syncWaterfallHook.call('cat', 8);
// 执行结果:
// 1 cat 8
// 2 panda 18
// 3 monkey 80

同步串行钩子,和SyncHook使用方式相同,不同的是监听函数的返回值会作为参数传递给下一个监听函数。


SyncLoopHook


const {SyncLoopHook} = require('tapable');
// 同步循环钩子,当监听函数返回值为true时,会重复执行监听函数
const syncLoopHook = new SyncLoopHook(['name', 'age']);
let count = 0;
syncLoopHook.tap('1', (name, age) => {
    console.log(1, name, age);
    return ++count === 3 ? undefined : '继续执行';
});
syncLoopHook.tap('2', (name, age) => {
    console.log(2, name, age);
});
syncLoopHook.call('panda', 18);
// 执行结果:
// 1 panda 18
// 1 panda 18
// 1 panda 18
// 2 panda 18

同步循环钩子,和SyncHook使用方式相同,当监听函数返回值为true时,会重复执行监听函数。


AsyncParallelHook


const {AsyncParallelHook} = require('tapable');
// 异步并行钩子,监听函数的执行是并行的,不会等待监听函数执行完毕
const asyncParallelHook = new AsyncParallelHook(['name', 'age']);
asyncParallelHook.tapAsync('1', (name, age, callback) => {
    setTimeout(() => {
        console.log(1, name, age);
        callback();
    }, 1000);
});
asyncParallelHook.tapAsync('2', (name, age, callback) => {
    setTimeout(() => {
        console.log(2, name, age);
        callback();
    }, 2000);
});
console.log('开始执行');
asyncParallelHook.callAsync('panda', 18, () => {
    console.log('执行结束');
});
console.log('执行还没结束');
// 执行结果:
// 开始执行
// 执行还没结束
// 1 panda 18
// 2 panda 18
// 执行结束

上面的钩子都是Sync开头的,表示同步钩子,现在是Async开头的,表示异步钩子,异步钩子执行的回调函数中,最后一个参数是callback,必须调用它,表示监听的函数执行完成。


同时异步的钩子还可以Promise化,如下代码:


const {AsyncParallelHook} = require('tapable');
const asyncParallelHook = new AsyncParallelHook();
asyncParallelHook.tapPromise('1', () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(1);
            resolve();
        }, 100);
    });
});
asyncParallelHook.tapPromise('2', () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(2);
            resolve();
        }, 200);
    });
});
asyncParallelHook.callAsync(() => {
    console.log('执行结束');
});

AsyncParallelBailHook


const {AsyncParallelBailHook} = require('tapable');
// 异步并行熔断钩子,当监听函数的返回值不为undefined时,后续的监听函数不再执行
const asyncParallelBailHook = new AsyncParallelBailHook(['name', 'age']);
asyncParallelBailHook.tapAsync('1', (name, age, callback) => {
    setTimeout(() => {
        console.log(1, name, age);
        callback();
    }, 1000);
});
asyncParallelBailHook.tapAsync('2', (name, age, callback) => {
    setTimeout(() => {
        console.log(2, name, age);
        callback(2);
    }, 2000);
});
asyncParallelBailHook.tapAsync('3', (name, age, callback) => {
    setTimeout(() => {
        console.log(3, name, age);
        callback();
    }, 3000);
});
asyncParallelBailHook.callAsync('panda', 18, () => {
    console.log('执行结束');
});
// 执行结果:
// 1 panda 18
// 2 panda 18
// 执行结束
// 3 panda 18

这个和同步熔断钩子的解释相同,使用和异步并行钩子相同,但是熔断了,却没有完全熔断,应该算是bug吧。

AsyncSeriesHook

const {AsyncSeriesHook} = require('tapable');
// 异步串行钩子,监听函数的执行是串行的,会等待监听函数执行完毕
const asyncSeriesHook = new AsyncSeriesHook(['name', 'age']);
asyncSeriesHook.tapAsync('1', (name, age, callback) => {
    setTimeout(() => {
        console.log(1, name, age);
        callback();
    }, 1000);
});
asyncSeriesHook.tapAsync('2', (name, age, callback) => {
    setTimeout(() => {
        console.log(2, name, age);
        callback();
    }, 2000);
});
asyncSeriesHook.tapAsync('3', (name, age, callback) => {
    setTimeout(() => {
        console.log(3, name, age);
        callback();
    }, 3000);
});
asyncSeriesHook.callAsync('panda', 18, () => {
    console.log('执行结束');
});
// 执行结果:
// 1 panda 18
// 2 panda 18
// 3 panda 18
// 执行结束

异步串行钩子,监听函数的执行是串行的,会等待监听函数执行完毕,也就是上面的代码整个执行时间需要6s。


剩下还有三个钩子,根据名称就可以推测出他们的作用了,也可以自己去写代码尝试一下,它的运行效果和执行逻辑。


实践


使用tapable可以不一定要用在webpack的插件开发上面,可以构建自己的应用系统上面,就比如我上面写钩子的示例,那个钩子没啥内容,现在就搞一个文件上传的tapable版本:


const {SyncHook, SyncBailHook} = require('tapable');
class UploadFile {
    hooks = {
        // 可以在这个阶段控制临时修改选择文件的类型
        openSelectDialog: new SyncHook(),
        // 选择文件
        selectFile: new SyncHook(),
        // 上传文件之前
        beforeUpload: new SyncBailHook(),
        // 上传文件之后
        afterUpload: new SyncHook(),
    };
    accept = '*';
    maxFileSize = 1024 * 1024 * 10;
    fileList = [];
    constructor() {
    }
    // 选择文件
    selectFile() {
        this.hooks.openSelectDialog.call();
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = this.accept;
        input.onchange = (e) => {
            if (this.maxFileSize && e.target.files[0].size > this.maxFileSize) {
                console.log('文件超过限制');
                return;
            }
            this.fileList.push(e.target.files[0]);
            this.hooks.selectFile.call(e.target.files[0], this.fileList);
        };
        input.click();
        this.hooks.selectFile();
    }
    // 上传文件
    uploadFile() {
        // 通过钩子控制是否上传
        this.hooks.afterUpload.tap('uploadFile', () => {
            const formData = new FormData();
            this.fileList.forEach((file) => {
                formData.append('file', file);
            });
            // 上传文件
            fetch('http://localhost:3000/upload', {
                method: 'POST',
                body: formData,
            }).then((res) => {
                console.log(res);
            }).finally(() => {
                this.hooks.afterUpload.call();
            });
        });
    }
}

上面是没有经过测试的代码,很多东西都没有考虑,只是一个示例思路,肯定是一堆bug的。


总结


tapable只是一个工具,具体怎么使用还是看开发者,不一定要使用在webpack,但是对于学习webpack,深入构建流程,了解插件开发是必不可少的一个环节,努力加油吧。


目录
相关文章
|
6天前
|
缓存 JavaScript 前端开发
js开发:请解释什么是Webpack,以及它在项目中的作用。
Webpack是开源的JavaScript模块打包器,用于前端项目构建,整合并优化JavaScript、CSS、图片等资源。它实现模块打包、代码分割以提升加载速度,同时进行资源优化和缓存。借助插件机制扩展功能,并支持热更新,加速开发流程。
27 4
|
8月前
|
资源调度 前端开发 JavaScript
深入理解Webpack:现代Web开发的打包工具
Webpack是一款强大的开源JavaScript模块打包工具,它在现代Web开发中扮演着至关重要的角色。通过Webpack,开发者可以有效地管理和打包项目中的各种资源,包括JavaScript、CSS、图片等,使Web应用程序更具可维护性和性能。在本博客中,我们将深入研究Webpack的核心概念、配置、加载器、插件和最佳实践,以帮助您更好地掌握这个强大的工具。
43 0
|
4天前
|
前端开发 测试技术 开发者
深入理解 Webpack 热更新原理:提升开发效率的关键
深入理解 Webpack 热更新原理:提升开发效率的关键
|
4天前
|
JavaScript
webpack-devtool选项 --生成source-map便于开发调试(二)
webpack-devtool选项 --生成source-map便于开发调试(二)
6 0
|
6天前
|
前端开发
【专栏】`webpack` 的 `DefinePlugin` 插件用于在编译时动态定义全局变量,实现环境变量差异化、配置参数动态化和条件编译
【4月更文挑战第29天】`webpack` 的 `DefinePlugin` 插件用于在编译时动态定义全局变量,实现环境变量差异化、配置参数动态化和条件编译。通过配置键值对,如 `ENV: JSON.stringify(process.env.NODE_ENV)`,可以在代码中根据环境执行相应逻辑。实际应用包括动态加载资源、动态配置接口地址和条件编译优化代码。注意变量定义的合法性和避免覆盖,解决变量未定义或值错误的问题,以提升开发效率和项目质量。
|
6天前
|
缓存 JavaScript 前端开发
js开发:请解释什么是Webpack,以及它在项目中的作用。
Webpack是开源的JavaScript模块打包器,用于前端项目构建,整合并优化JavaScript、CSS、图片等资源。它实现模块打包、代码分割以提升加载速度,同时进行资源优化和缓存。Webpack的插件机制可扩展功能,支持热更新以加速开发流程。
21 2
|
6天前
|
缓存 前端开发 JavaScript
|
6天前
|
JSON 监控 测试技术
《Webpack5 核心原理与应用实践》学习笔记-> 提升插件健壮性
《Webpack5 核心原理与应用实践》学习笔记-> 提升插件健壮性
54 0
|
6天前
|
缓存 前端开发 API
《Webpack5 核心原理与应用实践》学习笔记-> webpack插件开发基础
《Webpack5 核心原理与应用实践》学习笔记-> webpack插件开发基础
70 0
|
6天前
|
存储 缓存 JavaScript
《Webpack5 核心原理与应用实践》学习笔记-> webpack的loader开发技巧
《Webpack5 核心原理与应用实践》学习笔记-> webpack的loader开发技巧
47 1