Arale源码解析(2)——Events

多态薄雾
• 阅读 3305

带注释源码

// 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事件队列即可。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
1年前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Easter79 Easter79
4年前
sql注入
反引号是个比较特别的字符,下面记录下怎么利用0x00SQL注入反引号可利用在分隔符及注释作用,不过使用范围只于表名、数据库名、字段名、起别名这些场景,下面具体说下1)表名payload:select\from\users\whereuser\_id1limit0,1;!(https://o
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
4年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这