讀Zepto原始碼之Fx模組

對角發表於2017-10-08

fx 模組為利用 CSS3 的過渡和動畫的屬性為 Zepto 提供了動畫的功能,在 fx 模組中,只做了事件和樣式瀏覽器字首的補全,沒有做太多的相容。對於不支援 CSS3 過渡和動畫的, Zepto 的處理也相對簡單,動畫立即完成,馬上執行回撥。

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

原始碼版本

本文閱讀的原始碼為 zepto1.2.0

GitBook

reading-zepto

內部方法

dasherize

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

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

normalizeEvent

function normalizeEvent(name) { return eventPrefix ? eventPrefix + name : name.toLowerCase() }複製程式碼

為事件名增加瀏覽器字首。

為事件和樣式增加瀏覽器字首

變數

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 ) 。

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 中。

初始化樣式

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)

最後,所有文章都會同步傳送到微信公眾號上,歡迎關注,歡迎提意見:

作者:對角另一面

相關文章