读Zepto源码之Fx模块

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)

作者:对角另一面

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏BestSDK

年薪30万的前端面试题,你能答对几道?|附答案

HTML面试题 1.XHTML和HTML有什么区别 HTML是一种基本的WEB网页设计语言,XHTML是一个基于XML的置标语言 最主要的不同: XHTML 元...

4276
来自专栏菜鸟计划

javascript简史

一、javascript简介 1.1 javascript简史 javascript诞生于1995年。当时它的主要目的是处理以前由服务器端语言负责的一些输入验证...

2835
来自专栏Web 开发

iOS9.1终于可以关闭讨厌的300ms延迟了

https://developer.apple.com/library/prerelease/mac/releasenotes/General/WhatsNew...

960
来自专栏菩提树下的杨过

ExtJs学习笔记(10)_Window窗口的Border布局

以下源自ExtJs的官方示例,稍加注释而已 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional/...

2007
来自专栏Web 开发

WordPress主题修改之Html5语义化

Html5最大的特点在于多媒体和移动互联网方面,小弟不才,对那些了解不多,本次修改仅仅是为了那么一点点语义化。

1280
来自专栏Python攻城狮

Selenium 的使用1.网站模拟登录2.动态页面模拟点击3.执行 JavaScript 语句

812
来自专栏非著名程序员

基础篇章:关于 React Native 之 KeyboardAvoidingView 组件的讲解

友情提示:RN学习,从最基础的开始,大家不要嫌弃太基础,会的同学请自行略过,希望不要耽误已经会的同学的宝贵时间) 看完了这个组件的名字 KeyboardAvoi...

4465
来自专栏从零开始学自动化测试

python测试开发django-6.模板中include使用

当我们打开一个网站的时候,在打开不同的页面时候,会发现每个页面的顶部、底部内容都差不多,这样就可以把这些公共的部分,单独抽出来。 类似于python里面的函数,...

1323
来自专栏Taylor技术日志

Laravel-Excel导出功能文档

可以在闭包中修改一些属性,很多属性可在配置文件中设置默认值 config/excel.php

2.1K50
来自专栏Google Dart

AngularDart Material Design 列表 顶

它构成了选择和菜单组件的基础。 MaterialListComponent类充当提供样式和收集项事件的能力的列表的根节点。

1182

扫码关注云+社区

领取腾讯云代金券