前言
除了大家经常提到的自定义事件之外, 浏览器本身也支持我们自定义事件, 我们常说的自定义事件一般也都是用于项目中的一些通知机制, 例如双向绑定. 一起看一下如何实现自定义事件, 以及基于观察者模式的双向绑定的基本原理.
浏览器自定义事件
定义
除了我们常见的 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 地址 http://xxdy.tech/event/index.html :
大概思路捋一下:
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 中的双向绑定, react 中的 setState 等, 以 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 里面的双向绑定.
双向绑定
- 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 设置是不被允许的
- // 基本数据
- let data = {
- price: 5,
- count: 2
- },
- callb = null
- class Events {
- constructor() {
- this._events = []
- }
- on() {
- // 此处不需要指定 tyep 了
- 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
- })
- 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
- })
来源: https://www.cnblogs.com/pqjwyn/p/9517021.html