天天看点

读Zepto源码之Callbacks模块

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

本文阅读的源码为 zepto1.2.0

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

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

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

<code>options</code> : 构造函数的配置,默认为空对象

<code>list</code> : 回调函数列表

<code>stack</code> : 列表可以重复触发时,用来缓存触发过程中未执行的任务参数,如果列表只能触发一次,<code>stack</code> 永远为 <code>false</code>

<code>memory</code> : 记忆模式下,会记住上一次触发的上下文及参数

<code>fired</code> : 回调函数列表已经触发过

<code>firing</code> : 回调函数列表正在触发

<code>firingStart</code> : 回调任务的开始位置

<code>firingIndex</code> : 当前回调任务的索引

<code>firingLength</code>:回调任务的长度

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

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

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

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

如果 <code>options.memory</code> 为 <code>true</code> ,则将 <code>data</code>,也即上下文对象和参数保存下来。

将 <code>list</code> 是否已经触发过的状态 <code>fired</code> 设置为 <code>true</code>。

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

将回调列表的开始位置 <code>firingStart</code> 设置为回调列表的开始位置。

将回调任务的长度 <code>firingLength</code> 设置为回调列表的长度。

将回调的开始状态 <code>firing</code> 设置为 <code>true</code>

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

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

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

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

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

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

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

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

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

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

用 <code>$.each</code> 方法来遍历 <code>args</code> ,得到数组项 <code>arg</code>,如果 <code>arg</code> 为 <code>function</code> 类型,则进行下一个判断。

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

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

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

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

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

删除列表中指定的回调。

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

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

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

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

如果 <code>index &lt;= firingLength</code> ,即回调函数在当前的回调任务中,将回调任务数减少 <code>1</code> 。

如果 <code>index &lt;= firingIndex</code> ,即在正在执行的回调函数前,将正在执行函数的索引值减少 <code>1</code> 。

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

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

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

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

先将 <code>args</code> 不存在时,初始化为数组。

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

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

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

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

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

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

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

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

<code>disable</code> 是禁用回调函数,实质是将回调函数列表置为 <code>undefined</code> ,同时也将 <code>stack</code> 和 <code>memory</code> 置为 <code>undefined</code> ,调用 <code>disable</code> 后,<code>add</code> 、<code>remove</code> 、<code>fire</code> 、<code>fireWith</code> 等方法不再生效,这些方法的首要条件是 <code>list</code> 存在。

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

锁定回调列表,其实是禁止 <code>fire</code> 和 <code>fireWith</code> 的执行。

其实是将 <code>stack</code> 设置为 <code>undefined</code> , <code>memory</code> 不存在时,调用的是 <code>disable</code> 方法,将整个列表清空。效果等同于禁用回调函数。<code>fire</code> 和 <code>add</code> 方法都不能再执行。

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

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

回调列表是否被锁定。

其实就是检测 <code>stack</code> 是否存在。

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

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

本文转自 sshpp 51CTO博客,原文链接:http://blog.51cto.com/12902932/1950348,如需转载请自行联系原作者

继续阅读