读Zepto源码之Callbacks模块

pxrr2646 2年前
   <p>Callbacks 模块并不是必备的模块,其作用是管理回调函数,为 Defferred 模块提供支持,Defferred 模块又为 Ajax 模块的 promise 风格提供支持,接下来很快就会分析到 Ajax模块,在此之前,先看 Callbacks 模块和 Defferred 模块的实现。</p>    <h2>源码版本</h2>    <p>本文阅读的源码为 <a href="/misc/goto?guid=4959750871803332655" rel="nofollow,noindex">zepto1.2.0</a></p>    <h2>整体结构</h2>    <p>将 Callbacks 模块的代码精简后,得到的结构如下:</p>    <pre>  <code class="language-javascript">;(function($){    $.Callbacks = function(options) {      ...      Callbacks = {        ...      }      return Callbacks    }  })(Zepto)</code></pre>    <p>其实就是向 zepto 对象上,添加了一个 Callbacks 函数,这个是一个工厂函数,调用这个函数返回的是一个对象,对象内部包含了一系列的方法。</p>    <p>options 参数为一个对象,在源码的内部,作者已经注释了各个键值的含义。</p>    <pre>  <code class="language-javascript">// 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: 同一个回调只能添加一次</code></pre>    <h2>全局变量</h2>    <pre>  <code class="language-javascript">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</code></pre>    <ul>     <li>options : 构造函数的配置,默认为空对象</li>     <li>list : 回调函数列表</li>     <li>stack : 列表可以重复触发时,用来缓存触发过程中未执行的任务参数,如果列表只能触发一次, stack 永远为 false</li>     <li>memory : 记忆模式下,会记住上一次触发的上下文及参数</li>     <li>fired : 回调函数列表已经触发过</li>     <li>firing : 回调函数列表正在触发</li>     <li>firingStart : 回调任务的开始位置</li>     <li>firingIndex : 当前回调任务的索引</li>     <li>firingLength :回调任务的长度</li>    </ul>    <h2>基础用法</h2>    <p>我用 jQuery 和 Zepto 的时间比较短,之前也没有直接用过 Callbacks 模块,单纯看代码不易理解它是怎样工作的,在分析之前,先看一下简单的 API 调用,可能会有助于理解。</p>    <pre>  <code class="language-javascript">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')  })   // 没有任何输出</code></pre>    <p>上面的例子只是简单的调用,也有了注释,下面开始分析 API</p>    <h2>内部方法</h2>    <h3>fire</h3>    <pre>  <code class="language-javascript">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()        }  }</code></pre>    <p>Callbacks 模块只有一个内部方法 fire ,用来触发 list 中的回调执行,这个方法是 Callbacks 模块的核心。</p>    <p>变量初始化</p>    <pre>  <code class="language-javascript">memory = options.memory && data  fired = true  firingIndex = firingStart || 0  firingStart = 0  firingLength = list.length  firing = true</code></pre>    <p>fire 只接收一个参数 data ,这个内部方法 fire 跟我们调用 API 所接收的参数不太一样,这个 data 是一个数组,数组里面只有两项,第一项是上下文对象,第二项是回调函数的参数数组。</p>    <p>如果 options.memory 为 true ,则将 data ,也即上下文对象和参数保存下来。</p>    <p>将 list 是否已经触发过的状态 fired 设置为 true 。</p>    <p>将当前回调任务的索引值 firingIndex 指向回调任务的开始位置 firingStart 或者回调列表的开始位置。</p>    <p>将回调列表的开始位置 firingStart 设置为回调列表的开始位置。</p>    <p>将回调任务的长度 firingLength 设置为回调列表的长度。</p>    <p>将回调的开始状态 firing 设置为 true</p>    <p>执行回调</p>    <pre>  <code class="language-javascript">for ( ; list && firingIndex < firingLength ; ++firingIndex ) {    if (list[firingIndex].apply(data[0], data[1]) === false && options.stopOnFalse) {      memory = false      break    }  }  firing = false</code></pre>    <p>执行回调的整体逻辑是遍历回调列表,逐个执行回调。</p>    <p>循环的条件是,列表存在,并且当前回调任务的索引值 firingIndex 要比回调任务的长度要小,这个很容易理解,当前的索引值都超出了任务的长度,就找不到任务执行了。</p>    <p>list[firingIndex].apply(data[0], data[1]) 就是从回调列表中找到对应的任务,绑定上下文对象,和传入对应的参数,执行任务。</p>    <p>如果回调执行后显式返回 false , 并且 options.stopOnFalse 设置为 true ,则中止后续任务的执行,并且清空 memory 的缓存。</p>    <p>回调任务执行完毕后,将 firing 设置为 false ,表示当前没有正在执行的任务。</p>    <p>检测未执行的回调及清理工作</p>    <pre>  <code class="language-javascript">if (list) {    if (stack) stack.length && fire(stack.shift())    else if (memory) list.length = 0    else Callbacks.disable()  }</code></pre>    <p>列表任务执行完毕后,先检查 stack 中是否有没有执行的任务,如果有,则将任务参数取出,调用 fire 函数执行。后面会看到, stack 储存的任务是 push 进去的,用 shift 取出,表明任务执行的顺序是先进先出。</p>    <p>memory 存在,则清空回调列表,用 list.length = 0 是清空列表的一个方法。在全局参数中,可以看到, stack 为 false ,只有一种情况,就是 options.once 为 true 的时候,表示任务只能执行一次,所以要将列表清空。而 memory 为 true ,表示后面添加的任务还可以执行,所以还必须保持 list 容器的存在,以便后续任务的添加和执行。</p>    <p>其他情况直接调用 Callbacks.disable() 方法,禁用所有回调任务的添加和执行。</p>    <h2>.add()</h2>    <pre>  <code class="language-javascript">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  },</code></pre>    <p>start 为原来回调列表的长度。保存起来,是为了后面修正回调任务的开始位置时用。</p>    <h3>内部方法add</h3>    <pre>  <code class="language-javascript">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)        })  }</code></pre>    <p>add 方法的作用是将回调函数 push 进回调列表中。参数 arguments 为数组或者伪数组。</p>    <p>用 $.each 方法来遍历 args ,得到数组项 arg ,如果 arg 为 function 类型,则进行下一个判断。</p>    <p>在下一个判断中,如果 options.unique 不为 true ,即允许重复的回调函数,或者原来的列表中不存在该回调函数,则将回调函数存入回调列表中。</p>    <p>如果 arg 为数组或伪数组(通过 arg.length 是否存在判断,并且排除掉 string 的情况),再次调用 add 函数分解。</p>    <h3>修正回调任务控制变量</h3>    <pre>  <code class="language-javascript">add(arguments)  if (firing) firingLength = list.length  else if (memory) {    firingStart = start    fire(memory)  }</code></pre>    <p>调用 add 方法,向列表中添加回调函数。</p>    <p>如果回调任务正在执行中,则修正回调任务的长度 firingLength 为当前任务列表的长度,以便后续添加的回调函数可以执行。</p>    <p>否则,如果为 memory 模式,则将执行回调任务的开始位置设置为 start ,即原来列表的最后一位的下一位,也就是新添加进列表的第一位,然后调用 fire ,以缓存的上下文及参数 memory 作为 fire 的参数,立即执行新添加的回调函数。</p>    <h2>.remove()</h2>    <pre>  <code class="language-javascript">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  },</code></pre>    <p>删除列表中指定的回调。</p>    <h3>删除回调函数</h3>    <p>用 each 遍历参数列表,在 each 遍历里再有一层 while 循环,循环的终止条件如下:</p>    <pre>  <code class="language-javascript">(index = $.inArray(arg, list, index)) > -1</code></pre>    <p>$.inArray() 最终返回的是数组项在数组中的索引值,如果不在数组中,则返回 -1 ,所以这个判断是确定回调函数存在于列表中。关于 $.inArray 的分析,见《 <a href="/misc/goto?guid=4959750871895944846" rel="nofollow,noindex">读zepto源码之工具函数</a> 》。</p>    <p>然后调用 splice 删除 list 中对应索引值的数组项,用 while 循环是确保列表中有重复的回调函数都会被删除掉。</p>    <h3>修正回调任务控制变量</h3>    <pre>  <code class="language-javascript">if (firing) {    if (index <= firingLength) --firingLength    if (index <= firingIndex) --firingIndex  }</code></pre>    <p>如果回调任务正在执行中,因为回调列表的长度已经有了变化,需要修正回调任务的控制参数。</p>    <p>如果 index <= firingLength ,即回调函数在当前的回调任务中,将回调任务数减少 1 。</p>    <p>如果 index <= firingIndex ,即在正在执行的回调函数前,将正在执行函数的索引值减少 1 。</p>    <p>这样做是防止回调函数执行到最后时,没有找到对应的任务执行。</p>    <h2>.fireWith</h2>    <pre>  <code class="language-javascript">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  },</code></pre>    <p>以指定回调函数的上下文的方式来触发回调函数。</p>    <p>fireWith 接收两个参数,第一个参数 context 为上下文对象,第二个 args 为参数列表。</p>    <p>fireWith 后续执行的条件是列表存在并且回调列表没有执行过或者 stack 存在(可为空数组),这个要注意,后面讲 disable 方法和 lock 方法区别的时候,这是一个很重要的判断条件。</p>    <pre>  <code class="language-javascript">args = args || []  args = [context, args.slice ? args.slice() : args]</code></pre>    <p>先将 args 不存在时,初始化为数组。</p>    <p>再重新组合成新的变量 args ,这个变量的第一项为上下文对象 context ,第二项为参数列表,调用 args.slice 是对数组进行拷贝,因为 memory 会储存上一次执行的上下文对象及参数,应该是怕外部对引用的更改的影响。</p>    <pre>  <code class="language-javascript">if (firing) stack.push(args)  else fire(args)</code></pre>    <p>如果回调正处在触发的状态,则将上下文对象和参数先储存在 stack 中,从内部函数 fire 的分析中可以得知,回调函数执行完毕后,会从 stack 中将 args 取出,再触发 fire 。</p>    <p>否则,触发 fire ,执行回调函数列表中的回调函数。</p>    <p>add 和 remove 都要判断 firing 的状态,来修正回调任务控制变量, fire 方法也要判断 firing ,来判断是否需要将 args 存入 stack 中,但是 javascript 是单线程的,照理应该不会出现在触发的同时 add 或者 remove 或者再调用 fire 的情况。</p>    <h2>.fire()</h2>    <pre>  <code class="language-javascript">fire: function() {    return Callbacks.fireWith(this, arguments)  },</code></pre>    <p>fire 方法,用得最多,但是却非常简单,调用的是 fireWidth 方法,上下文对象是 this 。</p>    <h2>.has()</h2>    <pre>  <code class="language-javascript">has: function(fn) {    return !!(list && (fn ? $.inArray(fn, list) > -1 : list.length))  },</code></pre>    <p>has 有两个作用,如果有传参时,用来查测所传入的 fn 是否存在于回调列表中,如果没有传参时,用来检测回调列表中是否已经有了回调函数。</p>    <pre>  <code class="language-javascript">fn ? $.inArray(fn, list) > -1 : list.length</code></pre>    <p>这个三元表达式前面的是判断指定的 fn 是否存在于回调函数列表中,后面的,如果 list.length 大于 0 ,则回调列表已经存入了回调函数。</p>    <h2>.empty()</h2>    <pre>  <code class="language-javascript">empty: function() {    firingLength = list.length = 0    return this  },</code></pre>    <p>empty 的作用是清空回调函数列表和正在执行的任务,但是 list 还存在,还可以向 list 中继续添加回调函数。</p>    <h2>.disable()</h2>    <pre>  <code class="language-javascript">disable: function() {    list = stack = memory = undefined    return this  },</code></pre>    <p>disable 是禁用回调函数,实质是将回调函数列表置为 undefined ,同时也将 stack 和 memory 置为 undefined ,调用 disable 后, add 、 remove 、 fire 、 fireWith 等方法不再生效,这些方法的首要条件是 list 存在。</p>    <h2>.disabled()</h2>    <pre>  <code class="language-javascript">disabled: function() {    return !list  },</code></pre>    <p>回调是否已经被禁止,其实就是检测 list 是否存在。</p>    <h2>.lock()</h2>    <pre>  <code class="language-javascript">lock: function() {    stack = undefined    if (!memory) Callbacks.disable()    return this  },</code></pre>    <p>锁定回调列表,其实是禁止 fire 和 fireWith 的执行。</p>    <p>其实是将 stack 设置为 undefined , memory 不存在时,调用的是 disable 方法,将整个列表清空。效果等同于禁用回调函数。 fire 和 add 方法都不能再执行。</p>    <h3>.lock() 和 .disable() 的区别</h3>    <p>为什么 memory 存在时, stack 为 undefined 就可以将列表的 fire 和 fireWith 禁用掉呢?在上文的 fireWith 中,我特别提到了 !fired || stack 这个判断条件。在 stack 为 undefined 时, fireWith 的执行条件看 fired 这个条件。如果回调列表已经执行过, fired 为 true , fireWith 不会再执行。如果回调列表没有执行过, memory 为 undefined ,会调用 disable 方法禁用列表, fireWith 也不能执行。</p>    <p>所以, disable 和 lock 的区别主要是在 memory 模式下,回调函数触发过后, lock 还可以调用 add 方法,向回调列表中添加回调函数,添加完毕后会立刻用 memory 的上下文和参数触发回调函数。</p>    <h2>.locked()</h2>    <pre>  <code class="language-javascript">locked: function() {    return !stack  },</code></pre>    <p>回调列表是否被锁定。</p>    <p>其实就是检测 stack 是否存在。</p>    <h2>.fired()</h2>    <pre>  <code class="language-javascript">fired: function() {    return !!fired  }</code></pre>    <p>回调列表是否已经被触发过。</p>    <p>回调列表触发一次后 fired 就会变为 true ,用 !! 的目的是将 undefined 转换为 false 返回。</p>    <p> </p>    <p>来自:http://div.io/topic/2017</p>    <p> </p>