文档章节

读Zepto源码之Callbacks模块

对角另一面
 对角另一面
发布于 2017/07/23 09:56
字数 3488
阅读 11
收藏 0

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

License: CC BY-NC-ND 4.0

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

作者:对角另一面

© 著作权归作者所有

对角另一面
粉丝 0
博文 25
码字总数 57637
作品 0
广州
私信 提问
读Zepto源码之Form模块

模块处理的是表单提交。表单提交包含两部分,一部分是格式化表单数据,另一部分是触发 事件,提交表单。 读 Zepto 源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅...

对角另一面
2017/10/23
5
0
读Zepto源码之Data模块

的 模块用来获取 节点中的 属性的数据,和储存跟 相关的数据。 读 Zepto 源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅读的源码为 zepto1.2.0 GitBook 《readi...

对角另一面
2017/10/25
20
0
读Zepto源码之Callbacks模块

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

sshpp
2017/07/24
0
0
zepto源码Callback模块

Callback模块是用来管理回调函数,也作为deferred延迟对象得基础部分,现在一起来看看他的源码。 可选参数: 这是可选参数,下面可以进行试验: 下面是他的输出结果: 可以看到得是,当我们第...

B_Cornelius
2018/08/06
0
0
读Zepto源码之代码结构

虽然最近工作中没有怎么用 zepto ,但是据说 zepto 的源码比较简单,而且网上的资料也比较多,所以我就挑了 zepto 下手,希望能为以后阅读其他框架的源码打下基础吧。 源码版本 本文阅读的源...

sshpp
2017/07/24
0
0

没有更多内容

加载失败,请刷新页面

加载更多

个人服务容器化和监控集成

1.前景 自己比较喜欢玩机器,目前手上有4台常用的机器 asw 1核1G 阿里云 1核2G 腾讯云 1核1G 百度云 2核4G

MrPei
7分钟前
1
0
Rancher源码编译

源码包准备 mkdir -p $GOPATH/src/github.com/ranchercd $GOPATH/src/github.com/ranchergit clone https://github.com/rancher/rancher.gitcd ranchergit checkout v2.2.3-rc9 注1......

深蓝苹果
12分钟前
3
0
7个理由,给你推荐这款“秒杀Excel”的分析神器!

谈到数据分析,自然离不开赖以使用的数据分析工具。 商业智能时代,可用于数据分析的工具有很多,Python、R......还有各式各样的专业工具。其中,Excel也是推荐的比较多的一种,尤其是刚入门...

朕想上头条
25分钟前
2
0
Spring5 源码分析-容器刷新-解析配置类-主流程

上一篇:Spring5 源码分析-容器刷新-invokeBeanFactoryPostProcessors()方法 此篇是上一篇方法中非常非常重要的功能,也是Spring核心功能,完成所有的BeanDefinition注册。 详细的主流程,如...

特拉仔
26分钟前
2
0
Python 3.8.0 正式发布 更新内容

Python是一种跨平台的计算机程序设计语言。是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越来越多被用于独立的、大型项目的...

阮鹏
26分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部