讀Zepto源碼之Fx模塊

fx 模塊爲利用 CSS3 的過渡和動畫的屬性爲 Zepto 提供了動畫的功能,在 fx 模塊中,只作了事件和樣式瀏覽器前綴的補全,沒有作太多的兼容。對於不支持 CSS3 過渡和動畫的, Zepto 的處理也相對簡單,動畫當即完成,立刻執行回調。javascript

讀 Zepto 源碼系列文章已經放到了github上,歡迎star: reading-zeptocss

源碼版本

本文閱讀的源碼爲 zepto1.2.0html

GitBook

reading-zeptojava

內部方法

dasherize

function dasherize(str) { return str.replace(/([A-Z])/g, '-$1').toLowerCase() }

這個方法是將駝峯式( camleCase )的寫法轉換成用 - 鏈接的連詞符的寫法( camle-case )。轉換的目的是讓寫法符合 css 的樣式規範。git

normalizeEvent

function normalizeEvent(name) { return eventPrefix ? eventPrefix + name : name.toLowerCase() }

爲事件名增長瀏覽器前綴。github

爲事件和樣式增長瀏覽器前綴

變量

var prefix = '', eventPrefix,
    vendors = { Webkit: 'webkit', Moz: '', O: 'o' },
    testEl = document.createElement('div'),
    supportedTransforms = /^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i,
    transform,
    transitionProperty, transitionDuration, transitionTiming, transitionDelay,
    animationName, animationDuration, animationTiming, animationDelay,
    cssReset = {}

vendors 定義了瀏覽器的樣式前綴( key ) 和事件前綴 ( value ) 。web

testEl 是爲檢測瀏覽器前綴所建立的臨時節點。瀏覽器

cssReset 用來保存加完前綴後的樣式規則,用來過渡或動畫完成後重置樣式。微信

瀏覽器前綴檢測

if (testEl.style.transform === undefined) $.each(vendors, function(vendor, event){
  if (testEl.style[vendor + 'TransitionProperty'] !== undefined) {
    prefix = '-' + vendor.toLowerCase() + '-'
    eventPrefix = event
    return false
  }
})

檢測到瀏覽器不支持標準的 transform 屬性,則依次檢測加了不一樣瀏覽器前綴的 transitionProperty 屬性,直至找到合適的瀏覽器前綴,樣式前綴保存在 prefix 中, 事件前綴保存在 eventPrefix 中。app

初始化樣式

transform = prefix + 'transform'
cssReset[transitionProperty = prefix + 'transition-property'] =
cssReset[transitionDuration = prefix + 'transition-duration'] =
cssReset[transitionDelay    = prefix + 'transition-delay'] =
cssReset[transitionTiming   = prefix + 'transition-timing-function'] =
cssReset[animationName      = prefix + 'animation-name'] =
cssReset[animationDuration  = prefix + 'animation-duration'] =
cssReset[animationDelay     = prefix + 'animation-delay'] =
cssReset[animationTiming    = prefix + 'animation-timing-function'] = ''

獲取瀏覽器前綴後,爲全部的 transitionanimation 屬性加上對應的前綴,都初始化爲 '',方便後面使用。

方法

$.fx

$.fx = {
  off: (eventPrefix === undefined && testEl.style.transitionProperty === undefined),
  speeds: { _default: 400, fast: 200, slow: 600 },
  cssPrefix: prefix,
  transitionEnd: normalizeEvent('TransitionEnd'),
  animationEnd: normalizeEvent('AnimationEnd')
}
  • off: 表示瀏覽器是否支持過渡或動畫,若是既沒有瀏覽器前綴,也不支持標準的屬性,則斷定該瀏覽器不支持動畫
  • speeds: 定義了三種動畫持續的時間, 默認爲 400ms
  • cssPrefix: 樣式瀏覽器兼容前綴,即 prefix
  • transitionEnd: 過渡完成時觸發的事件,調用 normalizeEvent 事件加了瀏覽器前綴補全
  • animationEnd: 動畫完成時觸發的事件,一樣加了瀏覽器前綴補全

animate

$.fn.animate = function(properties, duration, ease, callback, delay){
  if ($.isFunction(duration))
    callback = duration, ease = undefined, duration = undefined
  if ($.isFunction(ease))
    callback = ease, ease = undefined
  if ($.isPlainObject(duration))
    ease = duration.easing, callback = duration.complete, delay = duration.delay, duration = duration.duration
  if (duration) duration = (typeof duration == 'number' ? duration :
                            ($.fx.speeds[duration] || $.fx.speeds._default)) / 1000
  if (delay) delay = parseFloat(delay) / 1000
  return this.anim(properties, duration, ease, callback, delay)
}

咱們平時用得最多的是 animate 這個方法,可是這個方法最終調用的是 anim 這個方法,animate 這個方法至關靈活,由於它主要作的是參數修正的工做,作得參數適應 anim 的接口。

參數:

  • properties:須要過渡的樣式對象,或者 animation 的名稱,只有這個參數是必傳的
  • duration: 過渡時間
  • ease: 緩動函數
  • callback: 過渡或者動畫完成後的回調函數
  • delay: 過渡或動畫延遲執行的時間

修正參數

if ($.isFunction(duration))
  callback = duration, ease = undefined, duration = undefined

這是處理傳參爲 animate(properties, callback) 的狀況。

if ($.isFunction(ease))
    callback = ease, ease = undefined

這是處理 animate(properties, duration, callback) 的狀況,此時 callback 在參數 ease 的位置

if ($.isPlainObject(duration))
  ease = duration.easing, callback = duration.complete, delay = duration.delay, duration = duration.duration

這是處理 animate(properties, { duration: msec, easing: type, complete: fn }) 的狀況。除了 properties ,後面的參數還能夠寫在一個對象中傳入。

若是檢測到爲對象的傳參方式,則將對應的值從對象中取出。

if (duration) duration = (typeof duration == 'number' ? duration :
                          ($.fx.speeds[duration] || $.fx.speeds._default)) / 1000

若是過渡時間爲數字,則直接採用,若是是 speeds 中指定的 key ,即 slowfast 甚至 _default ,則從 speeds 中取值,不然用 speends_default 值。

由於在樣式中是用 s 取值,因此要將毫秒數除 1000

if (delay) delay = parseFloat(delay) / 1000

也將延遲時間轉換爲秒。

anim

$.fn.anim = function(properties, duration, ease, callback, delay){
  var key, cssValues = {}, cssProperties, transforms = '',
      that = this, wrappedCallback, endEvent = $.fx.transitionEnd,
      fired = false

  if (duration === undefined) duration = $.fx.speeds._default / 1000
  if (delay === undefined) delay = 0
  if ($.fx.off) duration = 0

  if (typeof properties == 'string') {
    // keyframe animation
    cssValues[animationName] = properties
    cssValues[animationDuration] = duration + 's'
    cssValues[animationDelay] = delay + 's'
    cssValues[animationTiming] = (ease || 'linear')
    endEvent = $.fx.animationEnd
  } else {
    cssProperties = []
    // CSS transitions
    for (key in properties)
      if (supportedTransforms.test(key)) transforms += key + '(' + properties[key] + ') '
    else cssValues[key] = properties[key], cssProperties.push(dasherize(key))

    if (transforms) cssValues[transform] = transforms, cssProperties.push(transform)
    if (duration > 0 && typeof properties === 'object') {
      cssValues[transitionProperty] = cssProperties.join(', ')
      cssValues[transitionDuration] = duration + 's'
      cssValues[transitionDelay] = delay + 's'
      cssValues[transitionTiming] = (ease || 'linear')
    }
  }

  wrappedCallback = function(event){
    if (typeof event !== 'undefined') {
      if (event.target !== event.currentTarget) return // makes sure the event didn't bubble from "below"
      $(event.target).unbind(endEvent, wrappedCallback)
    } else
      $(this).unbind(endEvent, wrappedCallback) // triggered by setTimeout

    fired = true
    $(this).css(cssReset)
    callback && callback.call(this)
  }
  if (duration > 0){
    this.bind(endEvent, wrappedCallback)
    // transitionEnd is not always firing on older Android phones
    // so make sure it gets fired
    setTimeout(function(){
      if (fired) return
      wrappedCallback.call(that)
    }, ((duration + delay) * 1000) + 25)
  }

  // trigger page reflow so new elements can animate
  this.size() && this.get(0).clientLeft

  this.css(cssValues)

  if (duration <= 0) setTimeout(function() {
    that.each(function(){ wrappedCallback.call(this) })
  }, 0)

  return this
}

animation 最終調用的是 anim 方法,Zepto 也將這個方法暴露了出去,其實我以爲只提供 animation 方法就能夠了,這個方法徹底能夠做爲私有的方法調用。

參數默認值

if (duration === undefined) duration = $.fx.speeds._default / 1000
if (delay === undefined) delay = 0
if ($.fx.off) duration = 0

若是沒有傳遞持續時間 duration ,則默認爲 $.fx.speends._default 的定義值 400ms ,這裏須要轉換成 s

若是沒有傳遞 delay ,則默認不延遲,即 0

若是瀏覽器不支持過渡和動畫,則 duration 設置爲 0 ,即沒有動畫,當即執行回調。

處理animation動畫參數

if (typeof properties == 'string') {
  // keyframe animation
  cssValues[animationName] = properties
  cssValues[animationDuration] = duration + 's'
  cssValues[animationDelay] = delay + 's'
  cssValues[animationTiming] = (ease || 'linear')
  endEvent = $.fx.animationEnd
}

若是 propertiesstring, 即 properties 爲動畫名,則設置動畫對應的 cssdurationdelay 都加上了 s 的單位,默認的緩動函數爲 linear

處理transition參數

else {
  cssProperties = []
  // CSS transitions
  for (key in properties)
    if (supportedTransforms.test(key)) transforms += key + '(' + properties[key] + ') '
  else cssValues[key] = properties[key], cssProperties.push(dasherize(key))

  if (transforms) cssValues[transform] = transforms, cssProperties.push(transform)
  if (duration > 0 && typeof properties === 'object') {
    cssValues[transitionProperty] = cssProperties.join(', ')
    cssValues[transitionDuration] = duration + 's'
    cssValues[transitionDelay] = delay + 's'
    cssValues[transitionTiming] = (ease || 'linear')
  }
}

supportedTransforms 是用來檢測是否爲 transform 的正則,若是是 transform ,則拼接成符合 transform 規則的字符串。

不然,直接將值存入 cssValues 中,將 css 的樣式名存入 cssProperties 中,而且調用了 dasherize 方法,使得 propertiescss 樣式名( key )支持駝峯式的寫法。

if (transforms) cssValues[transform] = transforms, cssProperties.push(transform)

這段是檢測是否有 transform ,若是有,也將 transform 存入 cssValuescssProperties 中。

接下來判斷動畫是否開啓,而且是否有過渡屬性,若是有,則設置對應的值。

回調函數的處理

wrappedCallback = function(event){
  if (typeof event !== 'undefined') {
    if (event.target !== event.currentTarget) return // makes sure the event didn't bubble from "below"
    $(event.target).unbind(endEvent, wrappedCallback)
  } else
    $(this).unbind(endEvent, wrappedCallback) // triggered by setTimeout

  fired = true
  $(this).css(cssReset)
  callback && callback.call(this)
}

若是瀏覽器支持過渡或者動畫事件,則在動畫結束的時候,取消事件監聽,注意在 unbind 時,有個 event.target !== event.currentTarget 的斷定,這是排除冒泡事件。

若是事件不存在時,直接取消對應元素上的事件監聽。

而且將狀態控制 fired 設置爲 true ,表示回調已經執行。

動畫完成後,再將涉及過渡或動畫的樣式設置爲空。

最後,調用傳遞進來的回調函數,整個動畫完成。

綁定過渡或動畫的結束事件

if (duration > 0){
  this.bind(endEvent, wrappedCallback)
  setTimeout(function(){
    if (fired) return
    wrappedCallback.call(that)
  }, ((duration + delay) * 1000) + 25)
}

綁定過渡或動畫的結束事件,在動畫結束時,執行處理過的回調函數。

注意這裏有個 setTimeout ,是避免瀏覽器不支持過渡或動畫事件時,能夠經過 setTimeout 執行回調。setTimeout 的回調執行比動畫時間長 25ms ,目的是讓事件響應在 setTimeout 以前,若是瀏覽器支持過渡或動畫事件, fired 會在回調執行時設置成 truesetTimeout 的回調函數不會再重複執行。

觸發頁面迴流

// trigger page reflow so new elements can animate
this.size() && this.get(0).clientLeft

this.css(cssValues)

這裏用了點黑科技,讀取 clientLeft 屬性,觸發頁面的迴流,使得動畫的樣式設置上去時能夠當即執行。

具體能夠這篇文章中的解釋:2014-02-07-hidden-documentation.md

過渡時間不大於零的回調處理

if (duration <= 0) setTimeout(function() {
  that.each(function(){ wrappedCallback.call(this) })
}, 0)

duration 不大於零時,能夠是參數設置錯誤,也多是瀏覽器不支持過渡或動畫,就當即執行回調函數。

系列文章

  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模塊
  12. 讀Zepto源碼之Callbacks模塊
  13. 讀Zepto源碼之Deferred模塊
  14. 讀Zepto源碼之Ajax模塊
  15. 讀Zepto源碼之Assets模塊
  16. 讀Zepto源碼之Selector模塊
  17. 讀Zepto源碼之Touch模塊
  18. 讀Zepto源碼之Gesture模塊
  19. 讀Zepto源碼之IOS3模塊

附文

參考

License

署名-非商業性使用-禁止演繹 4.0 國際 (CC BY-NC-ND 4.0)

最後,全部文章都會同步發送到微信公衆號上,歡迎關注,歡迎提意見:

做者:對角另外一面

相關文章
相關標籤/搜索