XFE 技术 生活 笔记 文集

zepto源码分析·event模块

🔖 javascript 👀 113 🕒 2018-07-12 👨‍🎓 戡玉

准备知识

事件的本质就是发布/订阅模式,dom事件也不例外;先简单说明下发布/订阅模式,dom事件api和兼容性

发布/订阅模式

所谓发布/订阅模式,用一个形象的比喻就是买房的人订阅楼房消息,售楼处发布消息,体现为代码的话就是如下形式

var Observable = {
  callbacks: [],
  add: function(fn) {
    this.callbacks.push(fn);
  },
  fire: function(data) {
    this.callbacks.forEach(function(fn) {
      fn(data);
    })
  }
}

//甲-订阅楼盘消息
Observable.add(function(data) {
  show('执行动作一 ' + data)
})
//乙-订阅楼盘消息
Observable.add(function(data) {
  show('执行动作二' + data)
})

// 售楼处-发布消息
Observable.fire(data);

DOM/Event

eventType   事件类型值
canBubble   事件是否起泡
cancelable  是否可以用preventDefault()方法取消事件

var event = document.createEvent(eventType);               // 获取事件对象
event.initEvent(canBubble, canBubble, cancelable);         // 初始化事件
dom.dispatchEvent(event);                                  // 发送事件

dom.addEventListener(eventType, 处理方法, canBubble);       // 绑定事件
dom.removeEventListener(eventType, 处理方法, canBubble);    // 移除事件

兼容性

  1. focus/blur和mouseenter/mouseleave事件不支持冒泡,因而不能进行事件委托,需要对其进行事件模拟,现代游览器的focusin和focusout和mouseover/mouseout刚好能做到

  2. 在鼠标事件中,有一个relatedTarget属性,返回与事件的目标节点相关的节点;

  3. mouseover的relatedTarget指向移到目标节点上时离开的节点,mouseout的relatedTarget指向离开所在节点后进入的节点

  4. 要模拟mouseenter/mouseleave,只需确定触发mouseover/mouseout上的relatedTarget不存在,或者relatedTarget不为当前节点且不为当前节点的子节点(避免子节点事件冒泡的影响)

前要知识了解得差不多了,下面我们来看看zepto的event实现

事件创建/代理

$.Event

使用

// 创建一个点击事件,并允许事件冒泡
$.Event('click', { bubbles: true })

源码

// zepto代码很喜欢简写成一行, 为了可读性, 我修改如下
$.Event = function(type, props) {
    // 如果type不为字符串类型,做一些处理
    if (!isString(type)) {
         props = type;
         type = props.type;
    }
    // 创建原生事件, 如果specialEvents[type]不到预设类型,就用Events
    var event = document.createEvent(specialEvents[type] || 'Events');
    // 默认事件冒泡
    var bubbles = true;
    // 如果存在props,来一波Event对象设置
    if (props) {
        for (var name in props) {
            if (name == 'bubbles') {
                bubbles = !!props[name];
            } else {
                event[name] = props[name];
            }
        }
    }
    // 初始化事件配置
    event.initEvent(type, bubbles, true);
    // 对事件做一些改造和兼容性处理
    return compatible(event);
}

可以看到,$.Event方法的实现利用了dom原生事件api,做了一些参数处理和事件对象的包装

$.proxy

使用

var obj = {name: 'Zepto'},
var handler = function(){ 
    console.log("hello from + ", this.name) 
}
$(document).on('click', $.proxy(handler, obj));

$.proxy方法的作用就是为了改变回调函数的this指向,下面看它的实现

$.proxy = function(fn, context) {
    // 如果arguments参数超过2个,就说明存在代理参数
    var args = (2 in arguments) && slice.call(arguments, 2)
    if (isFunction(fn)) {
        // 如果fn是方法,返回代理函数proxyFn
        var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) }
        proxyFn._zid = zid(fn)
        return proxyFn
    } else if (isString(context)) {
        // 如果context为字符串,则将fn当做对象处理
        if (args) {
            // 有参数时,则组装args,重新执行$.proxy方法
            args.unshift(fn[context], fn)
            return $.proxy.apply(null, args)
        } else {
            return $.proxy(fn[context], fn)
        }
    } else {
        throw new TypeError("expected function")
    }
}

其中var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) }可以整理成如下形式:

var proxyFn = function () {
    var proxyArgs;
    if (args) {
        // 如果调用的时候,传递了参数
        // 就跟执行$.proxy时可能传递了的参数做整合
        proxyArgs = args.concat(slice.call(arguments));
    } else {
        // 直接取调用时传递的参数
        proxyArgs = arguments;
    }
    return proxyArgs;
}

$.proxy方法其实就是做了原生bind方法做的事情,只是处理了做了类型判断处理

事件绑定/解除

on

使用

var elem = $('#content');
elem.on('click', function(e){ ... });
elem.on({ type: handler, type2: handler2, ... }, 'li');

源码

// 将on绑定方法定义在zepto原型上
$.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

    // 如果callback为false,直接赋值会返回retunr false的函数
    if (callback === false) callback = returnFalse

    // 为每个dom元素执行事件绑定处理
    return $this.each(function(_, element){
        // 如果存在one参数,设置只执行一次操作的函数
        if (one) autoRemove = function(e){
            remove(element, e.type, callback)
            return callback.apply(this, arguments)
        }

        // 如果存在selector参数,创建委托函数
        if (selector) delegator = function(e){

            // 通过closest方法找到代理元素
            // 这也就是绑定未来才出现的元素的秘诀,不是考刷定时器检测未来元素
            // 而是当事件触发,代理delegator方法被事件回调执行时进行动态获取
            var evt, match = $(e.target).closest(selector, element).get(0)
            if (match && match !== element) {

                // 通过内部createProxy方法创建代理事件对象, 排除非标准属性
                // 然后将代理元素和触发事件的元素保存到事件对象中
                evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element})

                // 传入的事件回调函数被执行
                return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
            }
        }

        // 进行zepto版本的订阅操作,也就是对事件和传入的回调函数进行绑定
        add(element, event, callback, data, selector, delegator || autoRemove)
    })
}

add实现如下

function add(element, events, fn, data, selector, delegator, capture){
    // zid方法用于在elementt添加标记序号,便于内部对事件绑定内容进行查找
    // handlers为内部定义的对象字面量, 用于根据id存储绑定内容,充当句柄对象容器(内部缓存对象)
    var id = zid(element), set = (handlers[id] || (handlers[id] = []))

    // 以空格进行拆分,遍历各种事件进行绑定
    events.split(/\s/).forEach(function(event){
        // 如果为ready, 走ready流程
        if (event == 'ready') return $(document).ready(fn)

        // 创建回调句柄, parse方法生成基础句柄对象
        var handler   = parse(event)
        handler.fn    = fn
        handler.sel   = selector

        // 对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)
            // 如果调用了event.stopImmediatePropagation()
            if (e.isImmediatePropagationStopped()) return
            e.data = data
            var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
            // 如果事件回调执行了return false;
            if (result === false) e.preventDefault(), e.stopPropagation()
            return result
        }
        handler.i = set.length

        // 将句柄对象添加到内部句柄容器handlers, 也就是订阅操作
        set.push(handler)

        // 具体绑定操作
        if ('addEventListener' in element)
            element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
    })
}

对focus/blur和mouseenter/mouseleave的模拟和兼容处理

// 顶部变量定义
focusinSupported = 'onfocusin' in window,
focus = { focus: 'focusin', blur: 'focusout' },
hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }

// 事件传递的形式(捕获/冒泡)
// 如果事件为focus/blur事件并且浏览器不支持focusin/focusout时, 设置为true
// 为true时, 在捕获阶段处理事件,间接达到冒泡的目的
// captureSetting参数在内部并未传递, 通过!!自动转换为false, 做默认值
function eventCapture(handler, captureSetting) {
    return handler.del &&
    (!focusinSupported && (handler.e in focus)) ||
    !!captureSetting
}

// 将focus/blur转换成focusin/focusout,将mouseenter/mouseleave转换成mouseover/mouseout
function realEvent(type) {
  return hover[type] || (focusinSupported && focus[type]) || type
}

off

使用

var elem = $('#content'),
var callback = function() {};
elem.off('click', callback);
elem.off({ type: handler, type2: handler2, ... }, 'li');

源码

$.fn.off = function(event, selector, callback){
    var $this = this
    // 处理{'事件类型':'函数'}的传参形式
    if (event && !isString(event)) {
        $.each(event, function(type, fn){
            $this.off(type, selector, fn)
        })
        return $this
    }

    // 处理非{'事件类型':'函数'}的传参形式
    if (!isString(selector) && !isFunction(callback) && callback !== false)
    callback = selector, selector = undefined

    // 如果callback为false,直接赋值会返回retunr false的函数
    if (callback === false) callback = returnFalse

    // 为每个dom元素执行事件解除处理
    return $this.each(function(){
        remove(this, event, callback, selector)
    })
}

remove实现如下

function remove(element, events, fn, selector, capture){
    // 通过zid方法找回元素的标记序号
    var id = zid(element)

    // 以空格进行拆分,遍历各种事件进行解除
    ;(events || '').split(/\s/).forEach(function(event){

        // 通过findHandlers方法(操作handlers对象)找回对应的函数句柄,遍历进行绑定解除
        findHandlers(element, event, fn, selector).forEach(function(handler){
            delete handlers[id][handler.i]
        if ('removeEventListener' in element)
            element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
        })
    })
}

bind/unbind

$.fn.bind = function(event, data, callback){
    return this.on(event, data, callback)
}
$.fn.unbind = function(event, callback){
    return this.off(event, callback)
}

live/die

所谓给未来的元素绑定,换汤不换药,都是先通过绑定一个元素,然后执行时再具体查找子元素,再执行回调

$.fn.live = function(event, callback){
    $(document.body).delegate(this.selector, event, callback)
    return this
}
$.fn.die = function(event, callback){
    $(document.body).undelegate(this.selector, event, callback)
    return this
}

delegate/undelegate

$.fn.delegate = function(selector, event, callback){
    return this.on(event, selector, callback)
}
$.fn.undelegate = function(selector, event, callback){
    return this.off(event, selector, callback)
}

one

$.fn.one = function(event, selector, data, callback){
    return this.on(event, selector, data, callback, 1)
}

事件触发

trigger

使用

$(document.body).trigger('click', 'hello');

源码

$.fn.trigger = function(event, args){
    // 字符串和纯粹Object对象判断, 如果是就创建Event对象, 否则返回处理后的event对象
    event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event)

    // 将参数保存到事件对象上
    event._args = args

    // 遍历所有dom元素进行触发操作
    return this.each(function(){

        // 如果是focus/blur事件, 则直接执行, focus/blur游览器原生支持
        if (event.type in focus && typeof this[event.type] == "function") this[event.type]()

        // 存在dispatchEvent则直接执行事件触发
        else if ('dispatchEvent' in this) this.dispatchEvent(event)

        // 不存在dispatchEvent时, 则直接执行triggerHandler
        else $(this).triggerHandler(event, args)
    })
}

triggerHandler

使用

$(this).triggerHandler('click', '我是参数');

源码

$.fn.triggerHandler = function(event, args){
    var e, result

    // 遍历所有dom元素进行触发操作
    this.each(function(i, element){

        // 通过内部createProxy方法创建代理事件对象, 排除非标准属性
        // 字符串判断, 如果是就创建Event对象, 否则返回处理后的event对象
        e = createProxy(isString(event) ? $.Event(event) : event)

        // 将参数保存到事件对象上
        e._args = args

        // 将element保存到事件对象上
        e.target = element

        // 通过findHandlers方法(操作handlers对象)找回对应的函数句柄,遍历进行执行
        $.each(findHandlers(element, event.type || event), function(i, handler){
            result = handler.proxy(e)
            if (e.isImmediatePropagationStopped()) return false
        })
    })
    return result
}

事件判断

isDefaultPrevented

如果preventDefault()被该事件的实例调用,那么返回true

isImmediatePropagationStopped

如果stopImmediatePropagation()被该事件的实例调用,那么返回true

isPropagationStopped

如果stopPropagation()被该事件的实例调用,那么返回true

方法实现依赖于内部, 源码如下

// 顶部变量定义
var returnTrue = function(){return true},
returnFalse = function(){return false},
eventMethods = {
    preventDefault: 'isDefaultPrevented',
    stopImmediatePropagation: 'isImmediatePropagationStopped',
    stopPropagation: 'isPropagationStopped'
}

// 对事件做一些改造和兼容性处理
function compatible(event, source) {
    if (source || !event.isDefaultPrevented) {
    source || (source = event)

    // 遍历eventMethods进行原生函数劫持和事件判断函数的支持
    $.each(eventMethods, function(name, predicate) {
        var sourceMethod = source[name]

        // 通过键进行函数重置, 内部进行事件判断函数的支持
        event[name] = function(){
            this[predicate] = returnTrue
            return sourceMethod && sourceMethod.apply(source, arguments)
        }
        event[predicate] = returnFalse
    })

    // 返回一个事件发生的时间戳,没有则取执行时的当前时间
    event.timeStamp || (event.timeStamp = Date.now())

    // 是否调用了event.preventDefault()方法
    // 是否存在新的defaultPrevented判断属性,存在就用它
    // 不存在就用非标准属性returnValue,存在就用它
    // 不存在就用老属性方法getPreventDefault
    if (source.defaultPrevented !== undefined ? source.defaultPrevented :
        'returnValue' in source ? source.returnValue === false :
        source.getPreventDefault && source.getPreventDefault())
        event.isDefaultPrevented = returnTrue
    }
    return event
}

上述就Zepto对事件模块的实现,大约300行的样子