zepto源碼研究 - fx.js


簡要:zepto 提供了一個基礎方法animate來方便我們運用css動畫。主要針對transform,animate以及普通屬性(例如left,right,height,width等等)的transition過渡。

在js中能方便的,靈活的調用animate方法來操作元素動畫。

源碼如下:

//     Zepto.js
//     (c) 2010-2015 Thomas Fuchs
//     Zepto.js may be freely distributed under the MIT license.

;(function($, undefined){
  var prefix = '', eventPrefix,    // prefix瀏覽器前綴 -webkit等,eventPrefix事件前綴
      vendors = { Webkit: 'webkit', Moz: '', O: 'o' }, //前綴數據源 不包含IE
      testEl = document.createElement('div'),  //臨時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 = {}

  //將駝峰字符串轉成css屬性,如aB-->a-b
  function dasherize(str) { return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase() }

  //修正事件名
  function normalizeEvent(name) { return eventPrefix ? eventPrefix + name : name.toLowerCase() }


  /**
   * 根據瀏覽器內核,設置CSS前綴,事件前綴
   * 如-webkit, css:-webkit-  event:webkit
   * 這里會在vendors存儲webkit,moz,o三種前綴
   */
  $.each(vendors, function(vendor, event){
    if (testEl.style[vendor + 'TransitionProperty'] !== undefined) {
      prefix = '-' + vendor.toLowerCase() + '-'
      eventPrefix = event
      return false
    }
  })

  transform = prefix + 'transform'     //變形

  //過渡,對於css屬性重新設置前綴
  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'] = ''

  /**
   * 動畫常量數據源,默認設置
   * @type {{off: boolean, speeds: {_default: number, fast: number, slow: number}, cssPrefix: string, transitionEnd: *, animationEnd: *}}
   */
  $.fx = {
    off: (eventPrefix === undefined && testEl.style.transitionProperty === undefined), //能力檢測是否支持動畫,具體檢測是否支持過渡,支持過渡事件
    speeds: { _default: 400, fast: 200, slow: 600 },
    cssPrefix: prefix,                                //css 前綴  如-webkit-
    transitionEnd: normalizeEvent('TransitionEnd'), //過渡結束事件
    animationEnd: normalizeEvent('AnimationEnd')     //動畫播放結束事件
  }

  /**
   * 創建自定義動畫
   * @param properties  樣式集
   * @param duration 持續事件
   * @param ease    速率
   * @param callback  完成時的回調
   * @param delay     動畫延遲
   * @returns {*}
   */
  // 這里是對參數的修正和處理,真正操作的是anim方法
  $.fn.animate = function(properties, duration, ease, callback, delay){
    //參數修正,傳參為function(properties,callback)
    if ($.isFunction(duration))
      callback = duration, ease = undefined, duration = undefined
    if ($.isFunction(ease))  //傳參為function(properties,duration,callback)
      callback = ease, ease = undefined
    if ($.isPlainObject(duration))  //傳參為function(properties,{})
      ease = duration.easing, callback = duration.complete, delay = duration.delay, duration = duration.duration
    // duration 數字:持續時間  字符串:取speeds: { _default: 400, fast: 200, slow: 600 }對應數字
    if (duration) duration = (typeof duration == 'number' ? duration :
            ($.fx.speeds[duration] || $.fx.speeds._default)) / 1000     //動畫持續時間默認值
    if (delay) delay = parseFloat(delay) / 1000    //延遲時間,除以1000轉換成s
    return this.anim(properties, duration, ease, callback, delay)
  }

  /**
   * 動畫核心方法
   * @param properties  樣式集
   * @param duration 持續事件
   * @param ease    速率
   * @param callback  完成時的回調
   * @param delay     動畫延遲
   * @returns {*}
   */
  $.fn.anim = function(properties, duration, ease, callback, delay){
    var key, cssValues = {}, cssProperties, transforms = '',      // transforms 變形   cssValues設置給DOM的樣式
        that = this, wrappedCallback, endEvent = $.fx.transitionEnd,
        fired = false

    //修正持續時間
    if (duration === undefined) duration = $.fx.speeds._default / 1000
    if (delay === undefined) delay = 0

    //如果瀏覽器不支持動畫,持續時間設為0,直接跳動畫結束
    if ($.fx.off) duration = 0

    // properties是動畫名
    if (typeof properties == 'string') {
      // keyframe [animationName] = properties
      cssValues[animationName] = properties
      cssValues[animationDuration] = duration + 's'
      cssValues[animationDelay] = delay + 's'
      cssValues[animationTiming] = (ease || 'linear')
      endEvent = $.fx.animationEnd  //動畫結束事件
    } else {  //properties 是樣式集
      cssProperties = []
      // CSS transitionsanimation
      cssValues
      for (key in properties)
          // supportedTransforms.test(key) 正則檢測是否為變形
          // key + '(' + properties[key] + ') '拼湊成變形方法
        if (supportedTransforms.test(key)) transforms += key + '(' + properties[key] + ') '
        else cssValues[key] = properties[key], cssProperties.push(dasherize(key))
        console.log(transforms)

      // 變形統一存入  cssValues   cssProperties
      if (transforms) cssValues[transform] = transforms, cssProperties.push(transform)

      // duration > 0可以播放動畫,且properties是對象,表明為過渡,上面有字符串,則為animate
      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
      // TODO 既然已經執行完了,為什么這里要重復css一下,不太理解
      $(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

      //延時ms后執行動畫,注意這里加了25ms,保持endEvent,動畫先執行完。
      //綁定過事件還做延時處理,是transitionEnd在older Android phones不一定觸發
      setTimeout(function(){
        //如果觸發過,就不處理
        if (fired) return
        wrappedCallback.call(that)
      }, ((duration + delay) * 1000) + 25)
    }

    // trigger page reflow so new elements can animate
    //主動觸發頁面回流,刷新DOM,讓接下來設置的動畫可以正確播放
    //更改 offsetTop、offsetLeft、 offsetWidth、offsetHeight;scrollTop、scrollLeft、scrollWidth、scrollHeight;clientTop、clientLeft、clientWidth、clientHeight;getComputedStyle() 、currentStyle()。這些都會觸發回流。回流導致DOM重新渲染,平時要盡可能避免,但這里,為了動畫即時生效播放,則主動觸發回流,刷新DOM。
    // 與.length屬性一致
    this.size() && this.get(0).clientLeft

    //設置樣式,啟動動畫
    this.css(cssValues)

    // duration為0,即瀏覽器不支持動畫的情況,直接執行動畫結束,執行回調。
    if (duration <= 0) setTimeout(function() {
      that.each(function(){ wrappedCallback.call(this) })
    }, 0)

    return this;
  }

  testEl = null   //去掉不必要的數據存儲,便於垃圾回收
})(Zepto)

整個fx.js的大致結構如下:

1:根據瀏覽器屬性獲取前綴,並設置cssReset的屬性名稱前加入前綴,

2:設置$.fx全局默認值,(animate方法里面沒有傳的參數會取此處的默認值)

3:$.fn.animate 的主要功能其實是判斷並修正參數,最后調用的$.fn.anim才是操作動畫的核心方法。

4:$.fn.anim :

      4.1 首先若properties == 'string'  ,則為操作animate動畫的情況,cssValues存入動畫名稱,動畫過渡時間,動畫延遲時間以及動畫函數

      4.2 若properties為對象,則將里面的屬性加入到cssValues里面,其中對於transform的值先做加工處理。

5:觸發動畫結束的事件,這里有個技巧:先綁定endEvent,然后setTimeout,這是為了處理綁定endEvent不被兼容的情況。

 

demo如下:

<style>
        @keyframes mymove
        {
            from {top:0px;}
            to {top:200px;}
        }

        @-moz-keyframes mymove /* Firefox */
        {
            from {top:0px;}
            to {top:200px;}
        }

        @-webkit-keyframes mymove /* Safari 和 Chrome */
        {
            from {top:0px;}
            to {top:200px;}
        }

        @-o-keyframes mymove /* Opera */
        {
            from {top:0px;}
            to {top:200px;}
        }
    </style>


<script>

    $("#high").anim({
            transform:'translate(20px)',
            /!*scale:3,*!/
            /!*translate:'20px,20px',*!/
/!*            rotateX:'90deg',
            rotateY:'40deg',*!/
            height:'100px'
        },1,'linear',function () {
            alert("調用完成");
        },1);


  $("#high").anim('mymove',1,'linear',function () {
            alert('調用完成');
        },1);
</script>

 

 

這里有幾個非常高明的技巧需要提一下:

1:因為要運用js來給元素設置css樣式,但css樣式需要寫多個前綴來兼容,所以這里利用   testEl.style[vendor + 'TransitionProperty'] !== undefined 來獲取當前瀏覽器所支持的前綴然后運用到css屬性設置上去。

 /**
   * 根據瀏覽器內核,設置CSS前綴,事件前綴
   * 如-webkit, css:-webkit-  event:webkit
   * 這里會在vendors存儲webkit,moz,o三種前綴
   */
  $.each(vendors, function(vendor, event){
    if (testEl.style[vendor + 'TransitionProperty'] !== undefined) {
      prefix = '-' + vendor.toLowerCase() + '-'
      eventPrefix = event
      return false
    }
  })

  transform = prefix + 'transform'     //變形

  //過渡,對於css屬性重新設置前綴
  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'] = ''

2:還是zepto對於參數處理的老套路,在$.fn.animate中針對與形參的不同的輸入形式,對參數做對應的處理,使得這個方法能夠靈活的執行。

// 這里是對參數的修正和處理,真正操作的是anim方法
  $.fn.animate = function(properties, duration, ease, callback, delay){
    //參數修正,傳參為function(properties,callback)
    if ($.isFunction(duration))
      callback = duration, ease = undefined, duration = undefined
    if ($.isFunction(ease))  //傳參為function(properties,duration,callback)
      callback = ease, ease = undefined
    if ($.isPlainObject(duration))  //傳參為function(properties,{})
      ease = duration.easing, callback = duration.complete, delay = duration.delay, duration = duration.duration
    // duration 數字:持續時間  字符串:取speeds: { _default: 400, fast: 200, slow: 600 }對應數字
    if (duration) duration = (typeof duration == 'number' ? duration :
            ($.fx.speeds[duration] || $.fx.speeds._default)) / 1000     //動畫持續時間默認值
    if (delay) delay = parseFloat(delay) / 1000    //延遲時間,除以1000轉換成s
    return this.anim(properties, duration, ease, callback, delay)
  }

3:動畫前主動觸發頁面回流: this.size() && this.get(0).clientLeft

4:動畫結束后觸發事件:使用setTimeout不失為一種好辦法

//處理動畫結束事件
    if (duration > 0){
      //綁定動畫結束事件
      this.bind(endEvent, wrappedCallback)
      // transitionEnd is not always firing on older Android phones
      // so make sure it gets fired

      //延時ms后執行動畫,注意這里加了25ms,保持endEvent,動畫先執行完。
      //綁定過事件還做延時處理,是transitionEnd在older Android phones不一定觸發
      setTimeout(function(){
        //如果觸發過,就不處理
        if (fired) return
        wrappedCallback.call(that)
      }, ((duration + delay) * 1000) + 25)
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM