前端开发让前端飞Web前端之路

读Zepto源码之Fx模块

2017-10-09  本文已影响110人  对角另一面

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')
}

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 的接口。

参数:

修正参数

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)

最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:

作者:对角另一面

上一篇下一篇

猜你喜欢

热点阅读