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

读 Zepto 源码系列文章已经放到了github上,欢迎star: reading-zepto

源码版本

本文阅读的源码为 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 firedfiring, // Flag to know if list is currently firingfiringStart, // First callback to fire (used internally by add and fireWith)firingLength, // End of the loop when firingfiringIndex, // Index of currently firing callback (modified by remove if needed)list = [], // Actual callback liststack = !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 && datafired = truefiringIndex = firingStart || 0firingStart = 0firingLength = list.lengthfiring = truefor ( ; list && firingIndex < firingLength ; ++firingIndex ) {if (list[firingIndex].apply(data[0], data[1]) === false && options.stopOnFalse) {memory = falsebreak}}firing = falseif (list) {if (stack) stack.length && fire(stack.shift())else if (memory) list.length = 0else 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 = falsebreak}
}
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 = 0else 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.lengthelse if (memory) {firingStart = startfire(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 = startfire(memory)
}

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

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

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

.remove()

remove: function() {if (list) {$.each(arguments, function(_, arg){var indexwhile ((index = $.inArray(arg, list, index)) > -1) {list.splice(index, 1)// Handle firing indexesif (firing) {if (index <= firingLength) --firingLengthif (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) --firingLengthif (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 = 0return this
},

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

.disable()

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

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

.disabled()

disabled: function() {return !list
},

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

.lock()

lock: function() {stack = undefinedif (!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模块

参考

  • Zepto源码分析-callbacks模块
  • 读jQuery之十九(多用途回调函数列表对象)

License

作者:对角另一面

转载于:https://www.cnblogs.com/hefty/p/7223750.html

读Zepto源码之Callbacks模块相关推荐

  1. 读Zepto源码之Deferred模块

    Deferred 模块也不是必备的模块,但是 ajax 模块中,要用到 promise 风格,必需引入 Deferred 模块.Deferred 也用到了上一篇文章<读Zepto源码之Callb ...

  2. 读Zepto源码之Ajax模块 1

    Ajax 模块也是经常会用到的模块,Ajax 模块中包含了 jsonp 的现实,和 XMLHttpRequest 的封装. 读 Zepto 源码系列文章已经放到了github上,欢迎star: rea ...

  3. zepto ajax php实例,读Zepto源码之Ajax模块

    Ajax 模块也是经常会用到的模块,Ajax 模块中包含了 jsonp 的现实,和 XMLHttpRequest 的封装. 读 Zepto 源码系列文章已经放到了github上,欢迎star: rea ...

  4. 读Zepto源码之操作DOM

    2019独角兽企业重金招聘Python工程师标准>>> 这篇依然是跟 dom 相关的方法,侧重点是操作 dom 的方法. 读Zepto源码系列文章已经放到了github上,欢迎sta ...

  5. 读 zepto 源码之工具函数

    对角另一面 读 zepto 源码之工具函数 Zepto 提供了丰富的工具函数,下面来一一解读. 源码版本 本文阅读的源码为 zepto1.2.0 $.extend $.extend 方法可以用来扩展目 ...

  6. 读zepto源码之工具函数

    2019独角兽企业重金招聘Python工程师标准>>> Zepto 提供了丰富的工具函数,下面来一一解读. 源码版本 本文阅读的源码为 zepto1.2.0 $.extend $.e ...

  7. 读Zepto源码之代码结构

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

  8. Zepto源码分析-event模块

    源码注释 // Zepto.js // (c) 2010-2015 Thomas Fuchs // Zepto.js may be freely distributed under the MIT l ...

  9. 【Android 10 源码】healthd 模块 HAL 2.0 分析

    Android 9 引入了从 health@1.0 HAL 升级的主要版本 android.hardware.health HAL 2.0.这一新 HAL 具有以下优势: 框架代码和供应商代码之间的区 ...

  10. 试读angular源码第三章:初始化zone

    直接看人话总结 前言 承接上一章 项目地址 文章地址 angular 版本:8.0.0-rc.4 欢迎看看我的类angular框架 文章列表 试读angular源码第一章:开场与platformBro ...

最新文章

  1. python3最新稳定版本-python3稳定版
  2. python循环输入字典_python - 使用'for'循环迭代字典
  3. ARC中block块作为属性的使用笔记
  4. TI公司dsp的cmd配置文件的说明
  5. 利用WinRAR命令行压缩文件或文件夹2007-11-14 15:07压缩文件夹
  6. 用CRF做命名实体识别——NER系列(三)
  7. linux操作这样用视频,Linux下使用mencoder对视频进行操作
  8. 普通卷积armv7-neon指令集实现—XNNPACK
  9. php是做前端还是后端,在后端准备数据还是在前端操作? - php
  10. 利用位运算的模四算法
  11. python私有属性怎么定义_Python中定义私有属性的方法是()。
  12. 2021赣一中高考成绩查询,赣州中学2021年高一招生问答
  13. pdd本质上来说,没有什么壁垒
  14. Rust 第一章 简介
  15. 编译报错程序集版本高于所引用的程序集的版本
  16. iOS SDK:预览和打开文档
  17. 高创CDHD伺服驱动器调试软件ServoStudioSetup V2.18版本与技术资料
  18. 毕设题目:Matlab交通标志识别
  19. arduino动态刷新显示_玩家国度XG27UQ绝影游戏显示器评测:DSC加持的满血4K显示器...
  20. expected at least 1 bean which qualifies as autowire candidate for this ***错误的分析

热门文章

  1. python生成器表达式yield,面向过程编程,部分内置函数
  2. 1002:A+B 输入输出练习III 分数: 2
  3. NetBeans 时事通讯(刊号 # 143 - Apr 19, 2011)
  4. 突发!Spring疑似沦陷了。。。
  5. 面试过程中,竟然遇到PUA,我不得不服这种“潜规则”~
  6. 远程删除用户照片?刚刚,拼多多承认了!
  7. 阿里开源代码质量检测工具!
  8. Spring Boot 启动类真的是XXApplication?
  9. 我私藏的一个超级无敌好用的 Java 工具类库
  10. 女程序员上班第一件事:调整IDE颜色以适配今天的衣着妆容