读Zepto源码之Callbacks模块

Callbacks 模块并不是必备的模块,其作用是管理回调函数,为 Defferred 模块提供支持,Defferred 模块又为 Ajax 模块的 promise 风格提供支持,接下来很快就会分析到 Ajax模块,在此之前,先看 Callbacks 模块和 Defferred 模块的实现。

源码版本

本文阅读的源码为 zepto1.2.0

整体结构

将 Callbacks 模块的代码精简后,得到的结构如下:

;(function($){
  $.Callbacks = function(options) {
    ...
    Callbacks = {
      ...
    }
    return Callbacks
  }
})(Zepto)

其实就是向 zepto 对象上,添加了一个 Callbacks 函数,这个是一个工厂函数,调用这个函数返回的是一个对象,对象内部包含了一系列的方法。

options 参数为一个对象,在源码的内部,作者已经注释了各个键值的含义。

// Option flags:
  //   - once: Callbacks fired at most one time.
  //   - memory: Remember the most recent context and arguments
  //   - stopOnFalse: Cease iterating over callback list
  //   - unique: Permit adding at most one instance of the same callback
once: 回调至多只能触发一次
memory: 记下最近一次触发的上下文及参数列表,再添加新回调的时候都立刻用这个上下文及参数立即执行
stopOnFalse: 如果队列中有回调返回 `false`,立即中止后续回调的执行
unique: 同一个回调只能添加一次

全局变量

options = $.extend({}, options)

var memory, // Last fire value (for non-forgettable lists)
    fired,  // Flag to know if list was already fired
    firing, // Flag to know if list is currently firing
    firingStart, // First callback to fire (used internally by add and fireWith)
    firingLength, // End of the loop when firing
    firingIndex, // Index of currently firing callback (modified by remove if needed)
    list = [], // Actual callback list
    stack = !options.once && [], // Stack of fire calls for repeatable lists
  • options : 构造函数的配置,默认为空对象
  • list : 回调函数列表
  • stack : 列表可以重复触发时,用来缓存触发过程中未执行的任务参数,如果列表只能触发一次,stack 永远为 false
  • memory : 记忆模式下,会记住上一次触发的上下文及参数
  • fired : 回调函数列表已经触发过
  • firing : 回调函数列表正在触发
  • firingStart : 回调任务的开始位置
  • firingIndex : 当前回调任务的索引
  • firingLength:回调任务的长度

基础用法

我用 jQueryZepto 的时间比较短,之前也没有直接用过 Callbacks 模块,单纯看代码不易理解它是怎样工作的,在分析之前,先看一下简单的 API 调用,可能会有助于理解。

var callbacks = $.Callbacks({memory: true})
var a = function(a) {
  console.log('a ' + a)
}
var b = function(b) {
  console.log('b ' + b)
}
var c = function(c) {
  console.log('c ' + c)
}
callbacks.add(a).add(b).add(c)  // 向队列 list 中添加了三个回调
callbacks.remove(c) // 删除 c
callbacks.fire('fire') 
// 到这步输出了 `a fire` `b fire` 没有输出 `c fire`
callbacks.lock()
callbacks.fire('fire after lock')  // 到这步没有任何输出
// 继续向队列添加回调,注意 `Callbacks` 的参数为 `memory: true`
callbacks.add(function(d) {  
  console.log('after lock')
})
// 输出 `after lock`
callbacks.disable()
callbacks.add(function(e) {
  console.log('after disable')
}) 
// 没有任何输出

上面的例子只是简单的调用,也有了注释,下面开始分析 API

内部方法

fire

fire = function(data) {
  memory = options.memory && data
  fired = true
  firingIndex = firingStart || 0
  firingStart = 0
  firingLength = list.length
  firing = true
  for ( ; list && firingIndex < firingLength ; ++firingIndex ) {
    if (list[firingIndex].apply(data[0], data[1]) === false && options.stopOnFalse) {
      memory = false
      break
    }
  }
  firing = false
  if (list) {
    if (stack) stack.length && fire(stack.shift())
    else if (memory) list.length = 0
    else Callbacks.disable()
      }
}

Callbacks 模块只有一个内部方法 fire ,用来触发 list 中的回调执行,这个方法是 Callbacks 模块的核心。

变量初始化

memory = options.memory && data
fired = true
firingIndex = firingStart || 0
firingStart = 0
firingLength = list.length
firing = true

fire 只接收一个参数 data ,这个内部方法 fire 跟我们调用 API 所接收的参数不太一样,这个 data 是一个数组,数组里面只有两项,第一项是上下文对象,第二项是回调函数的参数数组。

如果 options.memorytrue ,则将 data,也即上下文对象和参数保存下来。

list 是否已经触发过的状态 fired 设置为 true

将当前回调任务的索引值 firingIndex 指向回调任务的开始位置 firingStart 或者回调列表的开始位置。

将回调列表的开始位置 firingStart 设置为回调列表的开始位置。

将回调任务的长度 firingLength 设置为回调列表的长度。

将回调的开始状态 firing 设置为 true

执行回调

for ( ; list && firingIndex < firingLength ; ++firingIndex ) {
  if (list[firingIndex].apply(data[0], data[1]) === false && options.stopOnFalse) {
    memory = false
    break
  }
}
firing = false

执行回调的整体逻辑是遍历回调列表,逐个执行回调。

循环的条件是,列表存在,并且当前回调任务的索引值 firingIndex 要比回调任务的长度要小,这个很容易理解,当前的索引值都超出了任务的长度,就找不到任务执行了。

list[firingIndex].apply(data[0], data[1]) 就是从回调列表中找到对应的任务,绑定上下文对象,和传入对应的参数,执行任务。

如果回调执行后显式返回 false, 并且 options.stopOnFalse 设置为 true ,则中止后续任务的执行,并且清空 memory 的缓存。

回调任务执行完毕后,将 firing 设置为 false,表示当前没有正在执行的任务。

检测未执行的回调及清理工作

if (list) {
  if (stack) stack.length && fire(stack.shift())
  else if (memory) list.length = 0
  else Callbacks.disable()
}

列表任务执行完毕后,先检查 stack 中是否有没有执行的任务,如果有,则将任务参数取出,调用 fire 函数执行。后面会看到,stack 储存的任务是 push 进去的,用 shift 取出,表明任务执行的顺序是先进先出。

memory 存在,则清空回调列表,用 list.length = 0 是清空列表的一个方法。在全局参数中,可以看到, stackfalse ,只有一种情况,就是 options.oncetrue 的时候,表示任务只能执行一次,所以要将列表清空。而 memorytrue ,表示后面添加的任务还可以执行,所以还必须保持 list 容器的存在,以便后续任务的添加和执行。

其他情况直接调用 Callbacks.disable() 方法,禁用所有回调任务的添加和执行。

.add()

add: function() {
  if (list) {
    var start = list.length,
        add = function(args) {
          $.each(args, function(_, arg){
            if (typeof arg === "function") {
              if (!options.unique || !Callbacks.has(arg)) list.push(arg)
                }
            else if (arg && arg.length && typeof arg !== 'string') add(arg)
              })
        }
    add(arguments)
    if (firing) firingLength = list.length
    else if (memory) {
      firingStart = start
      fire(memory)
    }
  }
  return this
},

start 为原来回调列表的长度。保存起来,是为了后面修正回调任务的开始位置时用。

内部方法add

add = function(args) {
  $.each(args, function(_, arg){
    if (typeof arg === "function") {
      if (!options.unique || !Callbacks.has(arg)) list.push(arg)
        }
    else if (arg && arg.length && typeof arg !== 'string') add(arg)
      })
}

add 方法的作用是将回调函数 push 进回调列表中。参数 arguments 为数组或者伪数组。

$.each 方法来遍历 args ,得到数组项 arg,如果 argfunction 类型,则进行下一个判断。

在下一个判断中,如果 options.unique 不为 true ,即允许重复的回调函数,或者原来的列表中不存在该回调函数,则将回调函数存入回调列表中。

如果 arg 为数组或伪数组(通过 arg.length 是否存在判断,并且排除掉 string 的情况),再次调用 add 函数分解。

修正回调任务控制变量

add(arguments)
if (firing) firingLength = list.length
else if (memory) {
  firingStart = start
  fire(memory)
}

调用 add 方法,向列表中添加回调函数。

如果回调任务正在执行中,则修正回调任务的长度 firingLength 为当前任务列表的长度,以便后续添加的回调函数可以执行。

否则,如果为 memory 模式,则将执行回调任务的开始位置设置为 start ,即原来列表的最后一位的下一位,也就是新添加进列表的第一位,然后调用 fire ,以缓存的上下文及参数 memory 作为 fire 的参数,立即执行新添加的回调函数。

.remove()

remove: function() {
  if (list) {
    $.each(arguments, function(_, arg){
      var index
      while ((index = $.inArray(arg, list, index)) > -1) {
        list.splice(index, 1)
        // Handle firing indexes
        if (firing) {
          if (index <= firingLength) --firingLength
          if (index <= firingIndex) --firingIndex
            }
      }
    })
  }
  return this
},

删除列表中指定的回调。

删除回调函数

each 遍历参数列表,在 each 遍历里再有一层 while 循环,循环的终止条件如下:

(index = $.inArray(arg, list, index)) > -1

$.inArray() 最终返回的是数组项在数组中的索引值,如果不在数组中,则返回 -1,所以这个判断是确定回调函数存在于列表中。关于 $.inArray 的分析,见《读zepto源码之工具函数》。

然后调用 splice 删除 list 中对应索引值的数组项,用 while 循环是确保列表中有重复的回调函数都会被删除掉。

修正回调任务控制变量

if (firing) {
  if (index <= firingLength) --firingLength
  if (index <= firingIndex) --firingIndex
}

如果回调任务正在执行中,因为回调列表的长度已经有了变化,需要修正回调任务的控制参数。

如果 index <= firingLength ,即回调函数在当前的回调任务中,将回调任务数减少 1

如果 index <= firingIndex ,即在正在执行的回调函数前,将正在执行函数的索引值减少 1

这样做是防止回调函数执行到最后时,没有找到对应的任务执行。

.fireWith

fireWith: function(context, args) {
  if (list && (!fired || stack)) {
    args = args || []
    args = [context, args.slice ? args.slice() : args]
    if (firing) stack.push(args)
    else fire(args)
      }
  return this
},

以指定回调函数的上下文的方式来触发回调函数。

fireWith 接收两个参数,第一个参数 context 为上下文对象,第二个 args 为参数列表。

fireWith 后续执行的条件是列表存在并且回调列表没有执行过或者 stack 存在(可为空数组),这个要注意,后面讲 disable 方法和 lock 方法区别的时候,这是一个很重要的判断条件。

args = args || []
args = [context, args.slice ? args.slice() : args]

先将 args 不存在时,初始化为数组。

再重新组合成新的变量 args ,这个变量的第一项为上下文对象 context ,第二项为参数列表,调用 args.slice 是对数组进行拷贝,因为 memory 会储存上一次执行的上下文对象及参数,应该是怕外部对引用的更改的影响。

if (firing) stack.push(args)
else fire(args)

如果回调正处在触发的状态,则将上下文对象和参数先储存在 stack 中,从内部函数 fire 的分析中可以得知,回调函数执行完毕后,会从 stack 中将 args 取出,再触发 fire

否则,触发 fire,执行回调函数列表中的回调函数。

addremove 都要判断 firing 的状态,来修正回调任务控制变量,fire 方法也要判断 firing ,来判断是否需要将 args 存入 stack 中,但是 javascript 是单线程的,照理应该不会出现在触发的同时 add 或者 remove 或者再调用 fire 的情况。

.fire()

fire: function() {
  return Callbacks.fireWith(this, arguments)
},

fire 方法,用得最多,但是却非常简单,调用的是 fireWidth 方法,上下文对象是 this

.has()

has: function(fn) {
  return !!(list && (fn ? $.inArray(fn, list) > -1 : list.length))
},

has 有两个作用,如果有传参时,用来查测所传入的 fn 是否存在于回调列表中,如果没有传参时,用来检测回调列表中是否已经有了回调函数。

fn ? $.inArray(fn, list) > -1 : list.length

这个三元表达式前面的是判断指定的 fn 是否存在于回调函数列表中,后面的,如果 list.length 大于 0 ,则回调列表已经存入了回调函数。

.empty()

empty: function() {
  firingLength = list.length = 0
  return this
},

empty 的作用是清空回调函数列表和正在执行的任务,但是 list 还存在,还可以向 list 中继续添加回调函数。

.disable()

disable: function() {
  list = stack = memory = undefined
  return this
},

disable 是禁用回调函数,实质是将回调函数列表置为 undefined ,同时也将 stackmemory 置为 undefined ,调用 disable 后,addremovefirefireWith 等方法不再生效,这些方法的首要条件是 list 存在。

.disabled()

disabled: function() {
  return !list
},

回调是否已经被禁止,其实就是检测 list 是否存在。

.lock()

lock: function() {
  stack = undefined
  if (!memory) Callbacks.disable()
  return this
},

锁定回调列表,其实是禁止 firefireWith 的执行。

其实是将 stack 设置为 undefinedmemory 不存在时,调用的是 disable 方法,将整个列表清空。效果等同于禁用回调函数。fireadd 方法都不能再执行。

.lock() 和 .disable() 的区别

为什么 memory 存在时,stackundefined 就可以将列表的 firefireWith 禁用掉呢?在上文的 fireWith 中,我特别提到了 !fired || stack 这个判断条件。在 stackundefined 时,fireWith 的执行条件看 fired 这个条件。如果回调列表已经执行过, firedtruefireWith 不会再执行。如果回调列表没有执行过,memoryundefined ,会调用 disable 方法禁用列表,fireWith 也不能执行。

所以,disablelock 的区别主要是在 memory 模式下,回调函数触发过后,lock 还可以调用 add 方法,向回调列表中添加回调函数,添加完毕后会立刻用 memory 的上下文和参数触发回调函数。

.locked()

locked: function() {
  return !stack
},

回调列表是否被锁定。

其实就是检测 stack 是否存在。

.fired()

fired: function() {
  return !!fired
}

回调列表是否已经被触发过。

回调列表触发一次后 fired 就会变为 true,用 !! 的目的是将 undefined 转换为 false 返回。

系列文章

  1. 读Zepto源码之代码结构
  2. 读 Zepto 源码之内部方法
  3. 读Zepto源码之工具函数
  4. 读Zepto源码之神奇的$
  5. 读Zepto源码之集合操作
  6. 读Zepto源码之集合元素查找
  7. 读Zepto源码之操作DOM
  8. 读Zepto源码之样式操作
  9. 读Zepto源码之属性操作
  10. 读Zepto源码之Event模块
  11. 读Zepto源码之IE模块

参考

License

最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:

作者:对角另一面

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容