除了你们常常提到的自定义事件以外,浏览器自己也支持咱们自定义事件,咱们常说的自定义事件通常用于项目中的一些通知机制。最近正好看到了这部分,就一块儿看了下自定义事件不一样的实现,以及vue数据响应的基本原理。javascript
除了咱们常见的click,touch等事件以外,浏览器支持咱们定义和分发自定义事件。 建立也十分简单:html
//建立名为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);
复制代码
自定义事件的触发和原生事件相似,能够经过冒泡事件触发。vue
<form>
<textarea></textarea>
</form>
复制代码
触发以下,这里就偷个懒,直接拿mdn的源码来示例了,毕竟清晰易懂。java
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当输入时,触发awesome
textarea.addEventListener('input', e => e.target.dispatchEvent(eventAwesome));
复制代码
上面例子很清晰的展现了自定义事件定义、监听、触发的整个过程,和原生事件的流程相比看起来多了个触发的步骤,缘由在原生事件的触发已经被封装无需手动处理而已。react
各类js库中用到的也比较多,例如zepto中的tap,原理就是监听touch事件,而后去触发自定的tap事件(固然这种成熟的框架作的是比较严谨的)。能够看下部分代码:git
//这里作了个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结构比较简单github
<div >一本书的价格:<input type='text' id='el' value=10 /></div>
<div >5本书的价格:<span id='el2'>50</span>元</div>
复制代码
当改变input值得时候,效果以下demo地址 : 数组
大概思路捋一下:浏览器
一、自定义事件,priceChange,用来监听改变price的改变
二、 加个监听事件,priceChange触发时改变total的值。
三、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(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的数据响应,网上相关文章简直太多了,这里就不深刻去讨论了。简单搬运一下基本概念。详细的话你们能够自行搜索。
直接看图比较直观: 就是经过观察者模式来实现,不过其经过数据劫持方式实现的更加巧妙。
数据劫持是经过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地址
源码地址