由自定义事件到vue数据响应

发布日期:2019-06-13

前言

除了大家经常提到的自定义事件之外,浏览器本身也支持我们自定义事件,我们常说的自定义事件一般用于项目中的一些通知机制。最近正好看到了这部分,就一起看了下自定义事件不同的实现,以及vue数据响应的基本原理。

浏览器自定义事件

定义

除了我们常见的click,touch等事件之外,浏览器支持我们定义和分发自定义事件。创建也十分简单:

//创建名为test的自定义事件var event = new Event("test")//如果是需要更多参数可以这样var event = new CustomEvent("test", { "detail": elem.dataset.time });

大多数现代浏览器对new Event/CustomEvent 的支持还算可以(IE除外),可以看下具体情况:可以放心大胆的使用,如果非要兼容IE那么有下面的方式

var event = document.createEvent("Event");//相关参数event.initEvent("test", true, true);

自定义事件的触发和原生事件类似,可以通过冒泡事件触发。

<form> <textarea></textarea></form>

触发如下,这里就偷个懒,直接拿mdn的源码来示例了,毕竟清晰易懂。

const form = document.querySelector("form");const textarea = document.querySelector("textarea");//创建新的事件,允许冒泡,支持传递在details中定义的所有数据const eventAwesome = new CustomEvent("awesome", { bubbles: true, detail: { text: () => textarea.value }}); //form元素监听自定义的awesome事件,打印text事件的输出 // 也就是text的输出内容form.addEventListener("awesome", e => console.log(e.detail.text())); // // textarea当输入时,触发awesometextarea.addEventListener("input", e => e.target.dispatchEvent(eventAwesome));

上面例子很清晰的展示了自定义事件定义、监听、触发的整个过程,和原生事件的流程相比看起来多了个触发的步骤,原因在原生事件的触发已经被封装无需手动处理而已。

应用

各大js类库

各种js库中用到的也比较多,例如zepto中的tap,原理就是监听touch事件,然后去触发自定的tap事件(当然这种成熟的框架做的是比较严谨的)。可以看下部分代码:

//这里做了个event的map,来将原始事件对应为自定义事件以便处理// 可以只关注下ontouchstart,这里先判断是否移动端,移动端down就对应touchstart,up对应touchend,后面的可以先不关注eventMap = (__eventMap && ("down" in __eventMap)) ? __eventMap : ("ontouchstart" in document ? { "down": "touchstart", "up": "touchend", "move": "touchmove", "cancel": "touchcancel" } : "onpointerdown" in document ? { "down": "pointerdown", "up": "pointerup", "move": "pointermove", "cancel": "pointercancel" } : "onmspointerdown" in document ? { "down": "MSPointerDown", "up": "MSPointerUp", "move": "MSPointerMove", "cancel": "MSPointerCancel" } : false) //监听事件 $(document).on(eventMap.up, up) .on(eventMap.down, down) .on(eventMap.move, move) //up事件即touchend时,满足条件的会触发tap var up = function (e) { /* 忽略 */ tapTimeout = setTimeout(function () { var event = $.Event("tap") event.cancelTouch = cancelAll if (touch.el) touch.el.trigger(event); },0) } //其他

发布订阅

和原生事件一样,大部分都用于观察者模式中。除了上面的库之外,自己开发过程中用到的地方也不少。举个例子,一个输入框表示单价,另一个div表示五本的总价,单价改变总价也会变动。借助自定义事件应该怎么实现呢。html结构比较简单

<div >一本书的价格:<input type="text" id="el" value=10 /></div><div >5本书的价格:<span id="el2">50</span>元</div>

当改变input值得时候,效果如下demo地址 :

大概思路捋一下:1、自定义事件,priceChange,用来监听改变price的改变2、 加个监听事件,priceChange触发时改变total的值。3、input value改变的时候,触发priceChange事件代码实现如下:

const count = document.querySelector("#el"), total1 = document.querySelector("#el2"); const eventAwesome = new CustomEvent("priceChange", { bubbles: true, detail: { getprice: () => count.value } }); document.addEventListener("priceChange", function (e) { var price = e.detail.getprice() || 0 total1.innerHTML=5 * price }) el.addEventListener("change", function (e) { var val = e.target.value e.target.dispatchEvent(eventAwesome) });

代码确实比较简单,当然实现的方式是多样的。但是看起来是不是有点vue数据响应的味道。确实目前大多数框架中都会用到发布订阅的方式来处理数据的变化。例如vue,react等,以vue为例子,我们可以来看看其数据响应的基本原理。

自定义事件

这里的自定义事件就是前面提到的第二层定义了,非基于浏览器的事件。这种事件也正是大型前端项目中常用到。对照原生事件,应该具有on、trigger、off三个方法。分别看一下

    对照原生事件很容易理解,绑定一个事件,应该有对应方法名和回调,当然还有一个事件队列

class Event1{ constructor(){ // 事件队列 this._events = {} } // type对应事件名称,call回调 on(type,call){ let funs = this._events[type] // 首次直接赋值,同种类型事件可能多个回调所以数组 // 否则push进入队列即可 if(funs){ funs.push(call) }else{ this._events.type=[] this._events.type.push(call) } }}

    触发事件trigger

// 触发事件 trigger(type){ let funs = this._events.type, [first,...other] = Array.from(arguments) //对应事件类型存在,循环执行回调队列 if(funs){ let i = 0, j = funs.length; for (i=0; i < j; i++) { let cb = funs[i]; cb.apply(this, other); } } }

    解除绑定:

// 取消绑定,还是循环查找 off(type,func){ let funs = this._events.type if(funs){ let i = 0, j = funs.length; for (i = 0; i < j; i++) { let cb = funs[i]; if (cb === func) { funs.splice(i, 1); return; } } } return this } }

这样一个简单的事件系统就完成了,结合这个事件系统,我们可以实现下上面那个例子。html不变,绑定和触发事件的方式改变一下就好

// 初始化 event1为了区别原生Event const event1 = new Event1() // 此处监听 priceChange 即可 event1.on("priceChange", function (e) { // 值获取方式修改 var price = count.value || 0 total1.innerHTML = 5 * price }) el.addEventListener("change", function (e) { var val = e.target.value // 触发事件 event1.trigger("priceChange") });

这样同样可以实现上面的效果,实现了事件系统之后,我们接着实现一下vue里面的数据响应。

vue的数据响应

说到vue的数据响应,网上相关文章简直太多了,这里就不深入去讨论了。简单搬运一下基本概念。详细的话大家可以自行搜索。

基本原理

直接看图比较直观:就是通过观察者模式来实现,不过其通过数据劫持方式实现的更加巧妙。数据劫持是通过Object.defineProperty()来监听各个属性的变化,从而进行一些额外操作。举个简单例子:

let a = { b:"1" }Object.defineProperty(a,"b",{ get(){ console.log("get>>>",1) return 1 }, set(newVal){ console.log("set>>>11","设置是不被允许的") return 1 } })a.b //"get>>>1"a.b = 11 //set>>>11 设置是不被允许的

所谓数据劫持就是在get/set操作时加上额外操作,这里是加了些log,如果在这里去监听某些属性的变化,进而更改其他属性也是可行的。要达到目的,应该对每个属性在get是监听,set的时候出发事件,且每个属性上只注册一次。另外应该每个属性对应一个监听者,这样处理起来比较方便,如果和上面那样全放在一个监听实例里面,有多个属性及复杂操作时,就太难维护了。

//基本数据let data = { price: 5, count: 2 },callb = null

可以对自定义事件进行部分改造,不需要显式指定type,全局维护一个标记即可事件数组一维即可,因为是每个属性对应一个示例

class Events { constructor() { this._events = [] } on() { //此处不需要指定tyep了 if (callb && !this._events.includes(callb)) { this._events.push(callb) } } triger() { this._events.forEach((callb) => { callb && callb() }) } }

对应上图中vue的Data部分,就是实行数据劫持的地方

Object.keys(data).forEach((key) => { let initVlue = data[key] const e1 = new Events() Object.defineProperty(data, key, { get() { //内部判断是否需要注册 e1.on() // 执行过置否 callb = null // get不变更值 return initVlue }, set(newVal) { initVlue = newVal // set操作触发事件,同步数据变动 e1.triger() } }) })

此时数据劫持即事件监听准备完成,大家可能会发现callback始终为null,这始终不能起作用。为了解决该问题,下面的watcher就要出场了。

function watcher(func) { // 参数赋予callback,执行时触发get方法,进行监听事件注册 callb = func // 初次执行时,获取对应值自然经过get方法注册事件 callb() // 置否避免重复注册 callb = null } // 此处指定事件触发回调,注册监听事件 watcher(() => { data.total = data.price * data.count })

这样就保证了会将监听事件挂载上去。到这里,乞丐版数据响应应该就能跑了。再加上dom事件的处理,双向绑定也不难实现。可以将下面的完整代码放到console台跑跑看。

let data = { price: 5, count: 2 }, callb = null class Events { constructor() { this._events = [] } on() { if (callb && !this._events.includes(callb)) { this._events.push(callb) } } triger() { this._events.forEach((callb) => { callb && callb() }) } } Object.keys(data).forEach((key) => { let initVlue = data[key] const e1 = new Events() Object.defineProperty(data, key, { get() { //内部判断是否需要注册 e1.on() // 执行过置否 callb = null // get不变更值 return initVlue }, set(newVal) { initVlue = newVal // set操作触发事件,同步数据变动 e1.triger() } }) }) function watcher(func) { // 参数赋予callback,执行时触发get方法,进行监听事件注册 callb = func // 初次执行时,获取对应值自然经过get方法注册事件 callb() // 置否避免重复注册 callb = null } // 此处指定事件触发回调,注册监听事件 watcher(() => { data.total = data.price * data.count })

结束语

参考文章

vue数据响应的实现Creating and triggering events看到知识盲点,就需要立即行动,不然下次还是盲点。正好是事件相关,就一并总结了下发布订阅相关进而到了数据响应的实现。个人的一点心得记录,分享出来希望共同学习和进步。更多请移步我的博客demo地址源码地址

, 1, 0, 9);