资讯专栏INFORMATION COLUMN

Arale源码解析(2)——Events

adie / 3281人阅读

摘要:带注释源码用于分割事件名的正则,识别空格介绍使用方法,这个模块可以混入任何对象之中,实现对自定义事件的资瓷将空格分割的事件绑定给对象,事件名为的话,事件回调函数在任何事件被触发时都会调用。

带注释源码
// Regular expression used to split event strings
// 用于分割事件名的正则,识别空格
var eventSplitter = /s+/


// A module that can be mixed in to *any object* in order to provide it
// with custom events. You may bind with `on` or remove with `off` callback
// functions to an event; `trigger`-ing an event fires all callbacks in
// succession.
//
//     var object = new Events();
//     object.on("expand", function(){ alert("expanded"); });
//     object.trigger("expand");
//
// 介绍使用方法,这个模块可以混入任何对象之中,实现对自定义事件的资瓷~
function Events() {
}


// Bind one or more space separated events, `events`, to a `callback`
// function. Passing `"all"` will bind the callback to all events fired.
// 将空格分割的事件绑定给对象,事件名为all的话,事件回调函数在任何事件被触发时都会调用。
Events.prototype.on = function(events, callback, context) {
  var cache, event, list
  // 回调函数不存在,直接返回
  if (!callback) return this

  // 将对象的`__events`属性缓存,`__events`属性不存在则初始化为空对象
  cache = this.__events || (this.__events = {})
  // 将参数中的事件字符串进行分割,得到事件名数组
  events = events.split(eventSplitter)

  // 循环遍历`events`中的事件
  while (event = events.shift()) {
    // 查询cache中是否缓存了事件,如果有,取得这个事件的回调函数队列的引用,如果没有,初始化为空数组
    list = cache[event] || (cache[event] = [])
    // 将回调和上下文存入回调函数队列
    list.push(callback, context)
  }

  return this
}

// 绑定只执行一次就销毁的事件回调
Events.prototype.once = function(events, callback, context) {
  var that = this
  // 对传入的`callback`进行一次封装,`cb`内调用`off`方法,调用一次就解绑
  var cb = function() {
    that.off(events, cb)
    callback.apply(context || that, arguments)
  }
  // 将封装后的`cb`进行绑定
  return this.on(events, cb, context)
}

// Remove one or many callbacks. If `context` is null, removes all callbacks
// with that function. If `callback` is null, removes all callbacks for the
// event. If `events` is null, removes all bound callbacks for all events.
// 移除一个或多个回调,如果`context`为空,移除所有同名的回调。
// 如果`callback`为空,移除该事件上所有回调。
// 如果`events`为空,移除所有时间上绑定的所有回调函数。
Events.prototype.off = function(events, callback, context) {
  var cache, event, list, i

  // No events, or removing *all* events.
  // 如果没有任何已绑定事件,直接返回
  if (!(cache = this.__events)) return this
  // 如果三个参数都没传,则删除对象上的`__events`属性,并返回对象
  if (!(events || callback || context)) {
    delete this.__events
    return this
  }

  // 对传入的`events`进行分割处理,如果没有传入`events`,取得缓存中的所有事件
  events = events ? events.split(eventSplitter) : keys(cache)

  // Loop through the callback list, splicing where appropriate.
  // 循环遍历events
  while (event = events.shift()) {
    // 保存事件回调队列
    list = cache[event]
    // 如队列为空,跳过
    if (!list) continue

    // 如果`callback`和`context`都没传,则删除该事件队列
    if (!(callback || context)) {
      delete cache[event]
      continue
    }

    // 遍历回调队列,注意每个回调和其调用上下文是间隔排列的,步长为2
    // 和传入的`callback`以及`context`比较,都符合的则将回调和调用上下文从数组中移除
    for (i = list.length - 2; i >= 0; i -= 2) {
      if (!(callback && list[i] !== callback ||
          context && list[i + 1] !== context)) {
        list.splice(i, 2)
      }
    }
  }

  return this
}


// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you"re listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
Events.prototype.trigger = function(events) {
  var cache, event, all, list, i, len, rest = [], args, returned = true;
  // 如果没有绑定过任何事件,直接返回
  if (!(cache = this.__events)) return this

  // 分割
  events = events.split(eventSplitter)

  // Fill up `rest` with the callback arguments.  Since we"re only copying
  // the tail of `arguments`, a loop is much faster than Array#slice.
  // 将除第一个参数`events`外的所有参数保存保存为数组存入`rest`
  for (i = 1, len = arguments.length; i < len; i++) {
    rest[i - 1] = arguments[i]
  }

  // For each event, walk through the list of callbacks twice, first to
  // trigger the event, then to trigger any `"all"` callbacks.
  // 对于每个事件,遍历两次回调队列,第一次是触发那个事件,第二次是触发任何`all`事件的回调
  while (event = events.shift()) {
    // Copy callback lists to prevent modification.
    // 如果缓存中存在all事件,将其回调队列分割存入all
    if (all = cache.all) all = all.slice()
    // 如果缓存中有当前遍历到的事件,将其回调队列分割存入list
    if (list = cache[event]) list = list.slice()

    // Execute event callbacks except one named "all"
    // 当遍历到的事件名不是all时,触发事件的所有回调,以this作为调用上下文
    if (event !== "all") {
      returned = triggerEvents(list, rest, this) && returned
    }

    // Execute "all" callbacks.
    // 触发对应all事件的所有回调
    returned = triggerEvents(all, [event].concat(rest), this) && returned
  }

  // 返回值
  return returned
}

// trigger == emit
Events.prototype.emit = Events.prototype.trigger


// Helpers
// -------
// 保存对`Object.keys`方法的引用
var keys = Object.keys

// 不存在`Object.keys`方法时就自己实现
if (!keys) {
  // 接受一个对象,返回该对象所有自有属性
  keys = function(o) {
    var result = []

    for (var name in o) {
      if (o.hasOwnProperty(name)) {
        result.push(name)
      }
    }
    return result
  }
}

// Mix `Events` to object instance or Class function.
// 将`Events`混入任何一个Class类的实例
Events.mixTo = function(receiver) {
  // 保存`Events`的原型
  var proto = Events.prototype

  // 判断接收对象类型,是否为构造函数
  if (isFunction(receiver)) {
    // 遍历`Events`原型内方法
    for (var key in proto) {
      // 将自有方法进行复制
      if (proto.hasOwnProperty(key)) {
        receiver.prototype[key] = proto[key]
      }
    }
    // 经过调试这步和上步的作用是一样的,只是调用了ES5的API,不知是否和兼容性有关
    Object.keys(proto).forEach(function(key) {
      receiver.prototype[key] = proto[key]
    })
  }
  else { // 针对接收者不是构造函数而是实例的情况
    // 生成Event类的实例
    var event = new Events
    // 遍历,判断,复制
    for (var key in proto) {
      if (proto.hasOwnProperty(key)) {
        copyProto(key)
      }
    }
  }

  // 复制属性
  function copyProto(key) {
    receiver[key] = function() {
      // 由于receiver已经是一个对象而不是构造函数,所以将所有方法的执行上下文转换为一个Event类的实例
      proto[key].apply(event, Array.prototype.slice.call(arguments))
      return this
    }
  }
}

// Execute callbacks
/**
 * 执行回调的方法
 * @param  {Array} list    回调函数队列
 * @param  {Array} args    参数数组
 * @param  {Object} context 调用上下文
 * @return {Boolean} pass
 */
function triggerEvents(list, args, context) {
  var pass = true

  if (list) {
    var i = 0, l = list.length, a1 = args[0], a2 = args[1], a3 = args[2]
    // call is faster than apply, optimize less than 3 argu
    // http://blog.csdn.net/zhengyinhui100/article/details/7837127
    // 由于`call`方法要比`apply`快,因此针对参数数量少于等于3个的情况进行优化,调用`call`,参数数量大于3个时调用`apply`
    switch (args.length) {
      case 0: for (; i < l; i += 2) {pass = list[i].call(list[i + 1] || context) !== false && pass} break;
      case 1: for (; i < l; i += 2) {pass = list[i].call(list[i + 1] || context, a1) !== false && pass} break;
      case 2: for (; i < l; i += 2) {pass = list[i].call(list[i + 1] || context, a1, a2) !== false && pass} break;
      case 3: for (; i < l; i += 2) {pass = list[i].call(list[i + 1] || context, a1, a2, a3) !== false && pass} break;
      default: for (; i < l; i += 2) {pass = list[i].apply(list[i + 1] || context, args) !== false && pass} break;
    }
  }
  // trigger will return false if one of the callbacks return false
  // 有一个回调函数的返回值为false则pass值为false
  return pass;
}

// 判断是否为Function类型的工具函数
function isFunction(func) {
  return Object.prototype.toString.call(func) === "[object Function]"
}

module.exports = Events
分析

这个Events类的运行方式还是比较简单的,这里就把实现机制归纳一下,这个应该是学习的重点。具体代码层面的实现看源码和注释就行了。

事件的所有相关信息全部保存在对象的__events属性上,该属性值是一个对象,以k-v的形式保存事件名和回调队列的对应关系,结构就像这样:

{
  "click": [callback1, context1, callback2, context2, ...],
  "remove": [callback1, context1, callback2, context2, ...],
  ...
}

一旦触发了某个事件,比如click,那么它对应的回调队列中的所有回调函数就会依次被执行。值得一提的时每个回调函数都有各自的执行上下文对象,这个比较特别,回调和上下文在数组中是间隔排列的,因此触发事件和解除绑定时都会特别处理这种特殊的数据结构。我认为之所以选用数组这种结构主要还是为了保证所有回调的触发顺序可控,如果用对象的话,遍历时的顺序是不一定的。针对这个问题,在玉伯和一位开发者的讨论中也能得到答案。

另外值得一提的是all这个事件,在一个对象上触发任何事件,同时也一定会触发all事件,实现原理很简单,就是在trigger这个方法中,判断一下事件缓存中有没有all这个事件队列,如果有,那么不管触发哪个事件,最后都再触发一下all事件队列即可。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/85385.html

相关文章

  • Arale源码解析(3)——Base模块和Aspect模块

    摘要:本文同步自我的博客前言这个模块实际上才是模块系统中对外的模块,它包含了之前介绍的类和类,以及自己内部的模块和模块,因此模块是真正的基础类。这两个方法的作用就是针对类上的某个方法,给这个方法绑定先于其执行和后于其执行的回调函数。 本文同步自我的GitHub博客 前言 Base这个模块实际上才是Arale模块系统中对外的模块,它包含了之前介绍的Class类和Events类,以及自己内部...

    stdying 评论0 收藏0
  • Arale源码解析(1)——Class

    摘要:先来看源码中,首先是做的是参数的处理工作,针对某些参数未传的情况作了调整,最后达到的效果是的值为传入的父类构造函数,如果没有,设为。下一个语句其作用是处理父类构造函数没有修改的属性值并且有方法的时候,在上调用方法。 本文同步自我的GitHub 概述 Arale是支付宝开发的一套基础类库,提供了一整套前端模块架构,基于CMD规范,所有模块均是以sea.js的标准进行开发。其开发过程借...

    _ivan 评论0 收藏0
  • arale 源码之 attribute 篇

    摘要:系列文章读源码之篇提供基本的属性添加获取移除等功能。判断是否为等对象特性检查闭包实现块作用域,不污染全局变量。找这个属性,若没有则返回空对象执行函数,返回被修改的值。 系列文章:读 arale 源码之 class 篇 attributes 提供基本的属性添加、获取、移除等功能。它是与实例相关的状态信息,可读可写,发生变化时,会自动触发相关事件 先来了解一下 Attribute 模块要实...

    Magicer 评论0 收藏0
  • arale 源码之 class 篇

    摘要:拥有了和方法的三个变种属性这三个属性会做特殊处理继承的方法,只支持单继承建立原型链来实现继承强制改变构造函数提供语法糖,来调用父类属性混入属性,可以混入多个类的属性将参数变成数组无论参数是类,还是对象,都混入。 更新:读 arale 源码之 attribute 篇 arale 是阿里、开源社区明星人物--玉伯,开发的一套组件,代码相当优美,大赞玉伯的开源精神,我是您的粉丝。 这里分享下...

    firim 评论0 收藏0
  • jQuery源码解析之clone()

    摘要:五作用的关键方法,用来从目标节点克隆数据添加事件给克隆的元素注意采用数据分离的方法来保存上的事件和数据,利用标记每个元素,然后在内存上,将每个元素相关的数据放到内存中,然后在和内存的数据之间建立映射。 showImg(https://segmentfault.com/img/remote/1460000018991125); 前言:这篇讲完后,jQuery的文档处理就告一段落了,有空我...

    coolpail 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<