前言

除了大家经常提到的自定义事件之外,浏览器本身也支持我们自定义事件,我们常说的自定义事件一般用于项目中的一些通知机制。最近正好看到了这部分,就一起看了下自定义事件不同的实现,以及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当输入时,触发awesome
textarea.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三个方法。分别看一下

  1. 对照原生事件很容易理解,绑定一个事件,应该有对应方法名和回调,当然还有一个事件队列
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)
}
}
}
  1. 触发事件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);
}
}
}
  1. 解除绑定:
// 取消绑定,还是循环查找
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地址

源码地址

由自定义事件到vue数据响应的更多相关文章

  1. Vue 数据响应式原理

    Vue 数据响应式原理 Vue.js 的核心包括一套“响应式系统”.“响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码.例如,视图渲染中使用了数据,数据改变后,视图也会自动更新. 举个简单 ...

  2. 一探 Vue 数据响应式原理

    一探 Vue 数据响应式原理 本文写于 2020 年 8 月 5 日 相信在很多新人第一次使用 Vue 这种框架的时候,就会被其修改数据便自动更新视图的操作所震撼. Vue 的文档中也这么写道: Vu ...

  3. vue数据响应式的一些注意点

    有关对象属性值不触发视图更新的情况: Vue 不能检测到对象属性的添加或删除,由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 ...

  4. vue数据响应的坑

    1.首先遇到的第一个坑是数组 vue初始化时,data是一个数组并且为空的时候,里面有一些对象元素,直接改变这些对象的的属性不会触发视图更新 解决办法,copy一个新的数组(vue.assign是浅c ...

  5. 仿VUE创建响应式数据

    VUE对于前端开发人员都非常熟悉了,其工作原理估计也都能说的清个大概,具体代码的实现估计看的人不会太多,这里对vue响应式数据做个简单的实现. 先简单介绍一下VUE数据响应原理,VUE响应数据分为对象 ...

  6. vue2自定义事件之$emit

    父组件: API上的解释不多: https://cn.vuejs.org/v2/api/#vm-emit vm.$emit( event, […args] ) 参数: {string} event [ ...

  7. VueJS组件通过props自定义事件

    父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,就需要使用自定义事件! 我们可以使用 v-on 绑定自定义事件, 每个 Vue 实例都实现了事件接口(Events inter ...

  8. vue2.0自定义事件

    我们知道父组件是使用props传递数据给子组件,如果子组件要把数据传递回去,怎么办? 那就是要自定义事件!使用v-on绑定自定义事件 每个Vue实例都实现了事件接口(Events interface) ...

  9. Vue3手册译稿 - 深入组件 - 自定义事件

    本章节需要掌握组件基础 emit我译成发射,觉得发射这个词比较形象的形容将子组件事件发射出来的一个动作. 事件名 像组件和props,事件名也会进行自动转换,如果你在子组件里发射一个驼峰命名的事件,你 ...

随机推荐

  1. B - 集合选数 (状压DP)

    题目链接:https://cn.vjudge.net/contest/281960#problem/B 题目大意:中文题目 具体思路: 我们通过构造矩阵, x , 3x,9x,27x 2x,6x,18 ...

  2. 关于《汇编语言(王爽)》程序6.3使用16个dw 0的问题

    在学习王爽老师<汇编语言>的第6.2节时,在程序6.3代码中,给出了如下的代码: assume cs:code code segment dw 0123h, 0456h, 0789h, 0 ...

  3. 2017-2018-2 20155303『网络对抗技术』Exp1:PC平台逆向破解

    2017-2018-2 『网络对抗技术』Exp1:PC平台逆向破解 --------CONTENTS-------- 1. 逆向及Bof基础实践说明 2. 直接修改程序机器指令,改变程序执行流程 3. ...

  4. fastdfs+nginx集群高可用搭建的一些坑!!记录一下

    首先我这里是三台节点,都搭tracker和storage,然后使用nginx做负载,只建一个group1,三个tracker! 搭建步骤比较麻烦,里面有很多坑需要注意,步骤就不啰嗦了,这里主要记录几个 ...

  5. python模块介绍- binascii:二进制和ASCII互转以及其他进制转换

    20.1 binascii:二进制和ASCII互转作用:二进制和ASCII互相转换. Python版本:1.5及以后版本 binascii模块包含很多在二进制和ASCII编码的二进制表示转换的方法.通 ...

  6. LinkedList源码分析笔记(jdk1.8)

    1.特点 LinkedList的底层实现是由一个双向链表实现的,可以从两端作为头节点遍历链表. 允许元素为null 线程不安全 增删相对ArrayList快,改查相对ArrayList慢(curd都会 ...

  7. as 插件GsonFormat用法(json字符串快速生成javabean)

    GsonFormat 主要用于使用Gson库将JSONObject格式的String 解析成实体,该插件可以加快开发进度,使用非常方便,效率高. 插件地址:https://plugins.jetbra ...

  8. jQuery实现鼠标点击div外的地方div隐藏消失的效果

    css部分: <style type="text/css"> .pop { width:200px; height:130px; background:#080;} & ...

  9. ES6的相关信息

    ECMAScript 是什么? ECMAScript 是 Javascript 语言的标准.ECMA European Computer Manufactures Association(欧洲计算机制 ...

  10. ie6 表格td中无内容时不显示边框的解决办法

    1.在单元格中加入一个空格.这样: <td> </td> 2.直接在table里这样写:<table border="0" cellspacing=& ...