clipboard.js 与 Vue
相关参考资料:
Clipboard.js
一个专职于“复制文本”操作的 js
库,以其短小精悍、简单实用的功能而被广泛应用
写这边文章的目的是为了说明:Clipboard.js
的事件绑定存在直接绑定和事件委托两种方式
其实官网有说明使用方法和注意事项,不过鉴于以前没用过这个库,我并没有第一时间去看文档,反而是看过源码之后才发现文档里面居然已经写明了:
所以,看文档的好习惯,还是不能丢了···,那下面纯粹记录一下bug
解决过程
一:Bug 描述
维护一个Vue2
+Element
的项目,调用Clipboard.js
初始化的复制按钮;页面第一次进入后点击正常,后续退出、再进入,每次点击会累计多触发一次
看看初始化是怎么写的:
if (!this.jsonClipboard) { this.jsonClipboard = new Clipboard('.json-btn'); this.jsonClipboard.on('success', (e) => { this.$message.success('复制成功'); }) }
这么写好像没问题;保证每次进入页面只初始化一次
而且每次页面进入,dom
都是重新生成,不存在重复绑定同一个dom
的情况
难道,他绑定的事件不是绑定在按钮dom
?
二:源码查阅
GitHub:zenorocha/clipboard.js: Modern copy to clipboard. No Flash. Just 3kb gzipped (github.com)
文件:/src/clipboard.js
// https://github.com/zenorocha/clipboard.js/blob/master/src/clipboard.js#L22 /** * Base class which takes one or more elements, adds event listeners to them, * and instantiates a new `ClipboardAction` on each click. */ class Clipboard extends Emitter { /** * @param {String|HTMLElement|HTMLCollection|NodeList} trigger * @param {Object} options */ constructor(trigger, options) { super(); this.resolveOptions(options); this.listenClick(trigger); // 事件监听 } // ... // 此处省略 1w 字 // ... // https://github.com/zenorocha/clipboard.js/blob/master/src/clipboard.js#L58 /** * Adds a click event listener to the passed trigger. * @param {String|HTMLElement|HTMLCollection|NodeList} trigger */ listenClick(trigger) { this.listener = listen(trigger, 'click', (e) => this.onClick(e)); } // ... // 此处省略 1w 字 // ... }
listen
方法来自于组件good-listener
,出自他自己之手
GitHub:zenorocha/good-listener: A more versatile way of adding & removing event listeners (github.com)
文件:/src/listen.js
// https://github.com/zenorocha/good-listener/blob/master/src/listen.js#L4 /** * Validates all params and calls the right * listener function based on its target type. * * @param {String|HTMLElement|HTMLCollection|NodeList} target * @param {String} type * @param {Function} callback * @return {Object} */ function listen(target, type, callback) { if (!target && !type && !callback) { throw new Error("Missing required arguments"); } if (!is.string(type)) { throw new TypeError("Second argument must be a String"); } if (!is.fn(callback)) { throw new TypeError("Third argument must be a Function"); } if (is.node(target)) { return listenNode(target, type, callback); } else if (is.nodeList(target)) { return listenNodeList(target, type, callback); } else if (is.string(target)) { return listenSelector(target, type, callback); } else { throw new TypeError( "First argument must be a String, HTMLElement, HTMLCollection, or NodeList" ); } } /** * Adds an event listener to a HTML element * and returns a remove listener function. * * @param {HTMLElement} node * @param {String} type * @param {Function} callback * @return {Object} */ function listenNode(node, type, callback) { node.addEventListener(type, callback); return { destroy: function() { node.removeEventListener(type, callback); } } } /** * Add an event listener to a list of HTML elements * and returns a remove listener function. * * @param {NodeList|HTMLCollection} nodeList * @param {String} type * @param {Function} callback * @return {Object} */ function listenNodeList(nodeList, type, callback) { Array.prototype.forEach.call(nodeList, function(node) { node.addEventListener(type, callback); }); return { destroy: function() { Array.prototype.forEach.call(nodeList, function(node) { node.removeEventListener(type, callback); }); } } } /** * Add an event listener to a selector * and returns a remove listener function. * * @param {String} selector * @param {String} type * @param {Function} callback * @return {Object} */ function listenSelector(selector, type, callback) { return delegate(document.body, selector, type, callback); }
这里这哥们又调用了一个自己的库:delegate
Github:zenorocha/delegate: Lightweight event delegation (github.com)
三:解决方案
从上面的源码已经很清楚:
参数为node
、nodeList
时,直接使用addEventListener
进行事件绑定;
参数为字符串时,会用事件委托实现选择器可能的所有dom
的操作事件
而出现bug
的原因自然是:
// 此处使用类选择器 .json-btn this.jsonClipboard = new Clipboard('.json-btn');
所以在使用字符串/选择器做参数时,一定要在页面销毁/退出时,主动销毁绑定的委托事件:
beforeDestroy() { if (this.jsonClipboard) this.jsonClipboard.destroy(); },
四:总结
总的来说,依托外部组件进行的事件绑定,一定要清楚它是如何绑定、如何解绑
在Vue
中使用Clipboard.js
,不管用何种方式进行初始化,它绑定的事件会一直存在
所以任何时候都应在组件销毁(前)进行手动销毁,即使指定Dom
的事件绑定不会出现类似bug
,但绑定的事件会一直存在于内存中,对性能也是一种负担