前言javascript
套用上篇文章向zepto.js学习如何手动触发DOM事件 的开头😀😀😀css
前端在最近几年实在火爆异常,vue、react、angular各路框架层出不穷,我们要是不知道个双向数据绑定,不晓得啥是虚拟DOM,也许就被鄙视了。火热的背后每每也是无尽的浮躁,学习这些先进流行的类库或者框架可让咱们走的更快,可是静下心来回归基础,把基石打牢固,却可让咱们走的更稳,更远。html
最近一直在看zepto的源码,但愿经过学习它掌握一些框架设计的技巧,也将好久再也不拾起的js基础从新温习巩固一遍。若是你对这个系列感兴趣,欢迎点击watch
,随时关注动态。这篇文章主要想说一下zepto中事件模块(event.js)的添加事件on
以及移除事件off
实现原理,中间会详细地讲解涉及到的细节方面。前端
若是你想看event.js全文翻译版本,请点击这里查看vue
原文地址java
仓库地址react
在没有vue和react,甚至angular都没怎么接触的刀耕火种的时代,jQuery或者zepto是咱们手中的利器,是刀刃,他让咱们游刃有余地开发出兼容性好的漂亮的网页,咱们膜拜并感叹做者带来的便利,沉浸其中,没法自拔。git
可是用了这么久的zepto你知道这样写代码github
$('.list').on('click', 'li', function (e) {
console.log($(this).html())
})复制代码
是怎么实现事件委托的吗?为啥此时的this
就是你点中的li
呢?面试
日常咱们可能还会这样写。
$('.list li').bind('click', function () {})
$('.list').delegate('li', 'click', function () {})
$('.list li').live('click', function () {})
$('.list li').click(function () {})复制代码
写法有点多,也许你还有其余的写法,那么
on
bind
delegate
live
click()
这些添加事件的形式,有什么区别,内部之间又有什么联系呢?
相信你在面试过程当中也遇到过相似的问题(看完这边文章,你能够知道答案的噢😯
)?
接下来咱们从源码的角度一步步去探究其内部实现的原理。
on
开始为何选择从
on
添加事件的方式开始提及,缘由在于其余写法几乎都是on
衍生出来的,明白了on
的实现原理,其余的也就差很少那么回事了。
祭出一张画了很久的图
上面大概是zepto中on
形式注册事件的大体流程,好啦开始看源码啦,首先是on函数,它主要作的事情是注册事件前的参数处理,真正添加事件是内部函数add。
$.fn.on = function (event, selector, data, callback, one) {
// 第一段
var autoRemove, delegator, $this = this
if (event && !isString(event)) {
$.each(event, function (type, fn) {
$this.on(type, selector, data, fn, one)
})
return $this
}
// 第二段
if (!isString(selector) && !isFunction(callback) && callback !== false)
callback = data, data = selector, selector = undefined
if (callback === undefined || data === false)
callback = data, data = undefined
if (callback === false) callback = returnFalse
// 以上为针对不一样的调用形式,作好参数处理
// 第三段
return $this.each(function (_, element) {
// 处理事件只有一次生效的状况
if (one) autoRemove = function (e) {
remove(element, e.type, callback)
return callback.apply(this, arguments)
}
// 添加事件委托处理函数
if (selector) delegator = function (e) {
var evt, match = $(e.target).closest(selector, element).get(0)
if (match && match !== element) {
evt = $.extend(createProxy(e), { currentTarget: match, liveFired: element })
return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
}
}
// 使用add内部函数真正去给选中的元素注册事件
add(element, event, callback, data, selector, delegator || autoRemove)
})
}复制代码
直接看到这么一大坨的代码不易于理解,咱们分段进行阅读。
第一段
var autoRemove, delegator, $this = this
if (event && !isString(event)) {
$.each(event, function (type, fn) {
$this.on(type, selector, data, fn, one)
})
return $this
}复制代码
这段代码主要是为了处理下面这种调用形式。
$('.list li').on({
click: function () {
console.log($(this).html())
},
mouseover: function () {
$(this).css('backgroundColor', 'red')
},
mouseout: function () {
$(this).css('backgroundColor', 'green')
}
})复制代码
这种写法咱们平时写的比较少一点,可是确实是支持的。而zepto的处理方式则是循环调用on
方法,以key
为事件名,val
为事件处理函数。
在开始第二段代码阅读前,咱们先回顾一下,平时常用on
来注册事件的写法通常有哪些
// 这种咱们使用的也许最多了
on(type, function(e){ ... })
// 能够预先添加数据data,而后在回调函数中使用e.data来使用添加的数据
on(type, data, function(e){ ... })
// 事件代理形式
on(type, [selector], function(e){ ... })
// 固然事件代理的形式也能够预先添加data
on(type, [selector], data, function(e){ ... })
// 固然也能够只让事件只有一次起效
on(type, [selector], data, function (e) { ... }, true)复制代码
还会有其余的写法,可是常见的可能就是这些,第二段代码就是处理这些参数以让后续的事件正确添加。
第二段
// selector不是字符串形式,callback也不是函数
if (!isString(selector) && !isFunction(callback) && callback !== false)
callback = data, data = selector, selector = undefined
// 处理data没有传或者传了函数
if (callback === undefined || data === false)
callback = data, data = undefined
// callback能够传false值,将其转换为returnFalse函数
if (callback === false) callback = returnFalse复制代码
三个if语句很好的处理了多种使用状况的参数处理。也许直接看不能知晓究竟是如何作到的,能够试试每种使用状况都代入其中,找寻其是如何兼容的。
接下来咱们第三段
这段函数作了很是重要的两件事
咱们一件件看它如何实现。
if (one) autoRemove = function (e) {
remove(element, e.type, callback)
return callback.apply(this, arguments)
}复制代码
内部用了一个remove
函数,这里先不作解析,只要知道他就是移除事件的函数就能够,当移除事件的时候,再执行了传进来的回调函数。进而实现只调用一次的效果。
那么事件代理又是怎么实现咧?
回想一下日常本身是怎么写事件代理的,通常是利用事件冒泡(固然也可使用事件捕获)的性质,将子元素的事件委托到祖先元素身上,不只能够实现事件的动态性,还能够减小事件总数,提升性能。
举个例子
咱们把本来要添加到li上的事件委托到父元素ul上。
<ul class="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>复制代码
let $list = document.querySelector('.list')
$list.addEventListener('click', function (e) {
e = e || window.event
let target = e.target || e.srcElement
if (target.tagName.toLowerCase() === 'li') {
target.style.background = 'red'
}
}, false)复制代码
回到第三段
if (selector) delegator = function (e) {
// 这里用了closest函数,查找到最早符合selector条件的元素
var evt, match = $(e.target).closest(selector, element).get(0)
// 查找到的最近的符合selector条件的节点不能是element元素
if (match && match !== element) {
// 而后将match节点和element节点,扩展到事件对象上去
evt = $.extend(createProxy(e), { currentTarget: match, liveFired: element })
// 最后即是执行回调函数
return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
}
}复制代码
zepto中实现事件代理的基本原理是:以当前目标元素e.target
为起点向上查找到最早符合selector
选择器规则的元素,而后扩展了事件对象,添加了一些属性,最后以找到的match元素做为回调函数的内部this
做用域,并将扩展的事件对象做为回调函数的第一个参数传进去执行。
这里须要知道.closest(...)
api的具体使用,若是你不太熟悉,请点击这里查看
说道这里,事件尚未添加啊!到底在哪里添加的呢,on函数的最后一句,即是要进入事件添加了。
add(element, event, callback, data, selector, delegator || autoRemove)复制代码
zepto的内部真正给元素添加事件的地方在add函数。
function add(element, events, fn, data, selector, delegator, capture) {
var id = zid(element),
set = (handlers[id] || (handlers[id] = []))
events.split(/\s/).forEach(function (event) {
if (event == 'ready') return $(document).ready(fn)
var handler = parse(event)
handler.fn = fn
handler.sel = selector
// emulate mouseenter, mouseleave
if (handler.e in hover) fn = function (e) {
var related = e.relatedTarget
if (!related || (related !== this && !$.contains(this, related)))
return handler.fn.apply(this, arguments)
}
handler.del = delegator
var callback = delegator || fn
handler.proxy = function (e) {
e = compatible(e)
if (e.isImmediatePropagationStopped()) return
e.data = data
var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
if (result === false) e.preventDefault(), e.stopPropagation()
return result
}
handler.i = set.length
set.push(handler)
if ('addEventListener' in element)
element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
})
}复制代码
个人神,又是这么长长长长的一大坨,人艰不拆,看着心累啊啊啊啊!!!
不过不用急,只要一步步去看,最终确定能够看懂的。
开头有一句话
var id = zid(element)复制代码
function zid(element) {
return element._zid || (element._zid = _zid++)
}复制代码
zepto中会给添加事件的元素身上加一个惟一的标志,_zid从1开始不断往上递增。后面的事件移除函数都是基于这个id来和元素创建关联的。
// 代码初始地方定义
var handlers = {},
set = (handlers[id] || (handlers[id] = []))复制代码
handlers
即是事件缓冲池,以数字0, 1, 2, 3...保存着一个个元素的事件处理程序。来看看handlers长啥样。
html
<ul class="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>复制代码
javascript
$('.list').on('click', 'li', '', function (e) {
console.log(e)
}, true)复制代码
以上截图即是这段代码执行后获得的handlers,其自己是个对象,每一个key(1, 2, 3 ...)(这个key也是和元素身上的_zid属性一一对应的)都保存着一个数组,而数组中的每一项目都保存着一个与事件类型相关的对象。咱们来看看,每一个key的数组都长啥样
[
{
e: 'click', // 事件名称
fn: function () {}, // 用户传入的回调函数
i: 0, // 该对象在该数组中的索引
ns: 'qianlongo', // 命名空间
proxy: function () {}, // 真正给dom绑定事件时执行的事件处理程序, 为del或者fn
sel: '.qianlongo', // 进行事件代理时传入的选择器
del: function () {} // 事件代理函数
},
{
e: 'mouseover', // 事件名称
fn: function () {}, // 用户传入的回调函数
i: 1, // 该对象在该数组中的索引
ns: 'qianlongo', // 命名空间
proxy: function () {}, // 真正给dom绑定事件时执行的事件处理程序, 为del或者fn
sel: '.qianlongo', // 进行事件代理时传入的选择器
del: function () {} // 事件代理函数
}
]复制代码
这样的设置给后面事件的移除带了很大的便利。画个简单的图,看看元素添加的事件和handlers中的映射关系。
明白了他们之间的映射关系,咱们再回到源码处,继续看。
events.split(/\s/).forEach(function (event) {
// xxx
})复制代码
暂时去除了一些内部代码逻辑,咱们看到其对event
作了切分,并循环添加事件,这也是咱们像下面这样添加事件的缘由
$('li').on('click mouseover mouseout', function () {})复制代码
那么接下来咱们要关注的就是循环的内部细节了。添加了部分注释
// 若是是ready事件,就直接调用ready方法(这里的return貌似没法结束forEach循环吧)
if (event == 'ready') return $(document).ready(fn)
// 获得事件和命名空间分离的对象 'click.qianlongo' => {e: 'click', ns: 'qianlongo'}
var handler = parse(event)
// 将用户输入的回调函数挂载到handler上
handler.fn = fn
// 将用户传入的选择器挂载到handler上(事件代理有用)
handler.sel = selector
// 用mouseover和mouseout分别模拟mouseenter和mouseleave事件
// https://qianlongo.github.io/zepto-analysis/example/event/mouseEnter-mouseOver.html(mouseenter与mouseover为什么这般纠缠不清?)
// emulate mouseenter, mouseleave
if (handler.e in hover) fn = function (e) {
var related = e.relatedTarget
if (!related || (related !== this && !$.contains(this, related)))
return handler.fn.apply(this, arguments)
}
handler.del = delegator
// 注意须要事件代理函数(通过一层处理事后的)和用户输入的回调函数优先使用事件代理函数
var callback = delegator || fn
// proxy是真正绑定的事件处理程序
// 而且改写了事件对象event
// 添加了一些方法和属性,最后调用用户传入的回调函数,若是该函数返回false,则认为须要阻止默认行为和阻止冒泡
handler.proxy = function (e) {
e = compatible(e)
if (e.isImmediatePropagationStopped()) return
e.data = data
var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
// 若是回调函数返回false,那么将阻止冒泡和阻止浏览器默认行为
if (result === false) e.preventDefault(), e.stopPropagation()
return result
}
// 将该次添加的handler在set中的索引赋值给i
handler.i = set.length
// 把handler保存起来,注意由于一个元素的同一个事件是能够添加多个事件处理程序的
set.push(handler)
// 最后固然是绑定事件
if ('addEventListener' in element)
element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))复制代码
至此,添加事件到这里告一段落了。让咱们再回到文章初始的问题,
on
bind
delegate
live
click()
这些添加事件的形式,有什么区别,内部之间又有什么联系呢?其实看他们的源码大概就知道区别
// 绑定事件
$.fn.bind = function (event, data, callback) {
return this.on(event, data, callback)
}
// 小范围冒泡绑定事件
$.fn.delegate = function (selector, event, callback) {
return this.on(event, selector, callback)
}
// 将事件冒泡代理到body上
$.fn.live = function (event, callback) {
$(document.body).delegate(this.selector, event, callback)
return this
}
// 绑定以及触发事件的快件方式
// 好比 $('li').click(() => {})
; ('focusin focusout focus blur load resize scroll unload click dblclick ' +
'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave ' +
'change select keydown keypress keyup error').split(' ').forEach(function (event) {
$.fn[event] = function (callback) {
return (0 in arguments) ?
// click() 形式的调用内部仍是用了bind
this.bind(event, callback) :
this.trigger(event)
}
})复制代码
bind和click()函数都是直接将事件绑定到元素身上,live则代理到body元素身上,delegate是小范围是事件代理,性能在因为live,on就最厉害了,以上函数均可以用on实现调用。
事件移除的实现有赖于事件绑定的实现,绑定的时候,把真正注册的事件信息都和dom关联起来放在了handlers中,那么移除具体是如何实现的呢?咱们一步步来看。
一样先放一张事件移除的大体流程图
off函数
$.fn.off = function (event, selector, callback) {
var $this = this
// {click: clickFn, mouseover: mouseoverFn}
// 传入的是对象,循环遍历调用自己解除事件
if (event && !isString(event)) {
$.each(event, function (type, fn) {
$this.off(type, selector, fn)
})
return $this
}
// ('click', fn)
if (!isString(selector) && !isFunction(callback) && callback !== false)
callback = selector, selector = undefined
if (callback === false) callback = returnFalse
// 循环遍历删除绑定在元素身上的事件,如何解除,能够看remove
return $this.each(function () {
remove(this, event, callback, selector)
})
}复制代码
off函数基本上和on函数是一个套路,先作一些基本的参数解析,而后把移除事件的具体工做交给remove函数实现,因此咱们主要看remove函数。
remove函数
// 删除事件,off等方法底层用的该方法
function remove(element, events, fn, selector, capture) {
// 获得添加事件的时候给元素添加的标志id
var id = zid(element)
// 循环遍历要移除的事件(因此咱们用的时候,能够一次性移除多个事件)
; (events || '').split(/\s/).forEach(function (event) {
// findHandlers返回的是符合条件的事件响应集合
findHandlers(element, event, fn, selector).forEach(function (handler) {
// [{}, {}, {}]每一个元素添加的事件形如该结构
// 删除存在handlers上的响应函数
delete handlers[id][handler.i]
// 真正删除绑定在element上的事件及其事件处理函数
if ('removeEventListener' in element)
element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
})
})
}复制代码
继续往下走,一个重要的函数findHandlers
// 根据给定的element、event等参数从handlers中查找handler,
// 主要用于事件移除(remove)和主动触发事件(triggerHandler)
function findHandlers(element, event, fn, selector) {
// 解析event,从而获得事件名称和命名空间
event = parse(event)
if (event.ns) var matcher = matcherFor(event.ns)
// 读取添加在element身上的handler(数组),并根据event等参数帅选
return (handlers[zid(element)] || []).filter(function (handler) {
return handler
&& (!event.e || handler.e == event.e) // 事件名须要相同
&& (!event.ns || matcher.test(handler.ns)) // 命名空间须要相同
&& (!fn || zid(handler.fn) === zid(fn)) // 回调函数须要相同(话说为何经过zid()这个函数来判断呢?)
&& (!selector || handler.sel == selector) // 事件代理时选择器须要相同
})
}复制代码
由于注册事件的时候回调函数不是用户传入的fn,而是自定义以后的proxy函数,因此须要将用户此时传入的fn和handler中保存的fn相比较是否相等。
罗里吧嗦说了好多,不知道有没有把zepto中的事件处理部分说明白说详细,欢迎你们提意见。
若是对你有一点点帮助,点击这里,加一个小星星好很差呀
若是对你有一点点帮助,点击这里,加一个小星星好很差呀
若是对你有一点点帮助,点击这里,加一个小星星好很差呀