读zoomerang.js的源码

 

最近利用空闲时间看了两个极简js库(都是来自尤大的Repositoris)的源码实现,有点小收获 🎉

这篇就先来记录一下我对zoomerang.js这个库源码实现的理解:

zoomerang.js

zoomerang.js是个操作dom的js库,支持放大缩小(几乎)页面中的任何元素

整体这个库实现的思路

其实这个库的大体思路很简单,就是通过给dom绑定点击事件去触发放大/缩小(恢复)事件。有意思的是其中的一些细节、动画、以及之前没有见过的原生api。一步步的开始

IIF

立即执行函数,见了很多js库的源码都是用IIF,zoomerang也不例外,好处在于隔离作用域污染全局变量。以后自己写库的时候也可以借鉴

代码格式风格

看了这个库的基本代码结构觉得自己就是个野路子

(function() {
  // webkit prefix helper
  ......
  // regex
  ......
  // elements
  ......
  // state
  ......

  // helpers ----------------------------------------------------------------
  function setStyle() {
    ...
  }
  ......
  var api = {
    config: function,
    open: function,
    close: function,
    listen: function
  }

  // umd expose
})()

首先把能用到的全局的变量声明,尽量都统一分类写在最前面; 其次就是heplers 功能function 还有 主要的api; 最后做expose; 觉得这是个好习惯,嗯~~~

css兼容性的处理

这个库的核心就是使用CSS3完成动画,在做CSS兼容性处理的时候 我觉得有几点我可以学的地方

WebkitAppearance

var prefix = 'WebkitAppearance' in document.documentElement.style ? '-webkit-' : ''

用WebkitAppearance属性来检测是否为webkit,倒是没见过这种写法

compatibility

// compatibility stuff
var trans = sniffTransition(),
    transitionProp = trans.transition,
    transformProp = trans.transform,
    transformCssProp = transformProp.replace(/(.*)Transform/, '-$1-transform'),
    transEndEvent = trans.transEnd

定义了一个trans来承载sniffTransition()这个函数处理兼容后的css

function sniffTransition () {
    var ret   = {},
        trans = ['webkitTransition', 'transition', 'mozTransition'],
        tform = ['webkitTransform', 'transform', 'mozTransform'],
        end   = {
            'transition'       : 'transitionend',
            'mozTransition'    : 'transitionend',
            'webkitTransition' : 'webkitTransitionEnd'
        }
    trans.some(function (prop) {
        if (overlay.style[prop] !== undefined) {
            ret.transition = prop
            ret.transEnd = end[prop]
            return true
        }
    })
    tform.some(function (prop) {
        if (overlay.style[prop] !== undefined) {
            ret.transform = prop
            return true
        }
    })
    return ret
}

这个sniffTransition函数做的就是检查兼容性并且处理兼容的事情,定义了三组css动画兼容的范围(webkit, moz),分别是transiton属性、transform属性、以及transitionend事件(这个transitionend事件是第一次见,可以通过addEventlistener(transitionend)监听到css动画完成)。

然后遍历div.style是否包含兼容性写法的属性,如果包含就添加到ret。这里有一点我当时候比较疑问的为什么要用到some去遍历而不用forEach filter …,后来一想这个场景用 some 去处理最好不过了,一旦检查到了某个元素符合要求直接就返回true了,同时循环终止不多余浪费性能(警告自己,别一到需要循环遍历的地方就直接无脑的 forEach for)

处理好后的变量transitionProp 、transformProp 、transformCssProp怎么来用,就看到了声明的另一个函数checkTrans

function checkTrans (styles) {
    var value
    if (styles.transition) {
        value = styles.transition
        delete styles.transition
        styles[transitionProp] = value
    }
    if (styles.transform) {
        value = styles.transform
        delete styles.transform
        styles[transformProp] = value
    }
}

checkTrans 这个功能函数做两件事情,检查传入的style是否存在transition相关的动画属性,如果存在先save属性值,delete这个属性,add处理后的transProp属性,set保留的属性值

options

// options
    var options = {
        transitionDuration: '.4s',
        transitionTimingFunction: 'cubic-bezier(.4,0,0,1)',
        bgColor: '#fff',
        bgOpacity: 1,
        maxWidth: 300,
        maxHeight: 300,
        onOpen: null,
        onClose: null,
        onBeforeClose: null,
        onBeforeOpen: null
    }

之前定义的默认的options,包含了动画的时间,过渡的特效,遮盖层的背景颜色,以及放大后的最大宽高,还有四个钩子函数onOpen,onClose,onBeforeClose, onBeforeOpen

setStyle

function setStyle (el, styles, remember) {
    checkTrans(styles)
    var s = el.style,
        original = {}
    for (var key in styles) {
        if (remember) {
            original[key] = s[key] || ''
        }
        s[key] = styles[key]
    }
    return original
}

这个函数的作用是先处理兼容性用到了checkTrans函数,然后再覆盖之前的样式。并且根据传入的第三个参数来判断是否需要返回值,如果第三个参数为true 将会返回el的原样式。

overlay wrapper

创建了两个el元素一个是遮盖层overlay 一个是wrapper

setStyle(overlay, {
    position: 'fixed',
    display: 'none',
    zIndex: 99998,
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    opacity: 0,
    backgroundColor: options.bgColor,
    cursor: prefix + 'zoom-out',
    transition: 'opacity ' +
        options.transitionDuration + ' ' +
        options.transitionTimingFunction
})

通过设置fixed布局 top left right bottom 为0 来撑满整个全屏的效果,dispaly为none。

这里的transition 第三个值是一个cubic-bezier贝塞尔曲线,这个平时几乎用不到但是也了解一下。

wrapper的设置也是fixed布局,left top 都为50%。长宽度都为0的垂直居中的元素。

api

最主要的核心是api这个对象,包含了 config open close listen四个方法,使用这个库也就是调用api提供的这几个方法就能实现el放大缩小的功能

api.cofig({…options})

使用这个库之前需要一些配置选项

config: function (opts) {

            if (!opts) return options
            for (var key in opts) {
                options[key] = opts[key]
            }
            setStyle(overlay, {
                backgroundColor: options.bgColor,
                transition: 'opacity ' +
                    options.transitionDuration + ' ' +
                    options.transitionTimingFunction
            })
            return this
        }

传入一个cofig对象来配置css options, 如果没传入就使用默认的options

api.open(el, cb)

这个方法就是点击之后触发的事件,放大dom。

两个参数一个是target元素,一个放大事件后的callback,默认是config中提供的onOpen钩子函数。

if (shown || lock) return

target = typeof el === 'string'
    ? document.querySelector(el)
    : el

// onBeforeOpen event
if (options.onBeforeOpen) options.onBeforeOpen(target)

shown = true
lock = true

之前就定义好的两个变量 show lock 都为false,open放大事件会先判断show\false两个是否为true,有一个为true 就直接return了。先不明白这个地方放在这。

target就是绑定当前点击el,这个地方的写法还巧妙的。 先判断传入的第一个arg是不是String,这样的话 既可以传 ‘#元素id & .元素class’,又可以直接传这个dom元素。

如果你通过api.config({…options})设置了 onBeforeOpen这个callback的话,那么会在执行放大动画操作前先执行

parent = target.parentNode

var p     = target.getBoundingClientRect(),
    scale = Math.min(options.maxWidth / p.width, options.maxHeight / p.height),
    dx    = p.left - (window.innerWidth - p.width) / 2,
    dy    = p.top - (window.innerHeight - p.height) / 2

placeholder = copy(target, p)

因为是我自己知识的一个总结,不想像写文章那样说的那么细致 提几个有意思的点

dx dy

这两个变量是计算el 自身位置-translate-垂直居中 位置的偏移量,放大动画效果的动画过渡的一段移动距离就是这么来的,这个公式挺好。

getComputedStyle(el)

取el的style对象,不需要重复的去写el.style[‘…‘]这种代码,可以很轻松复制一个el的样式。

el.getBoundingClientRect()

同样返回一个对象,包含left top width height等值。

整个放大的逻辑是这样:

先使用setStyle改变target的样式设置translate(dx, dy)让target偏移,但是会保留原样式赋值给originalStyles这个全局变量。然后往parent父元素里面append遮盖层,和wrapper,另外在target前面添加一个placeholder占位元素(这个占位元素其实就是利用getComputedStyle来复target,设置不可见。)这样做的目的是防止target改变布局会错乱。wrapper是垂直居中的,再往target里添加target。这个时候因为我们先前设置了target偏移,所以el虽在wrapper下,但是却(偏移到)还在原来的位置。

overlay.style.opacity = options.bgOpacity
setStyle(target, {
    transition:
        transformCssProp + ' ' +
        options.transitionDuration + ' ' +
        options.transitionTimingFunction,
    transform: 'scale(' + scale + ')'
})

这时候改变transform 为scale,并设置transition。 因为覆盖之前的transform属性,现在时刻的target将会不再偏移位置,伴随过渡动画就会回到垂直居中的位置。

这个做法没错,但是我自己尝试去写的时候发现并不是这样,当执行到改变transform为scale的时候,动画似乎并没有移动回垂直位置的过渡轨迹。

requestAnimationFrame()

可能是因为浏览器重绘,需要在下次重绘前去更新下一帧动画,我尝试在requestAnimationFrame()中去改变target的transform,之后可以看到完整的动画。

requestAnimationFrame(() => {
    target.style['transition'] = `transform .3s cubic-bezier(.4,0,0,1)`
    target.style['transform'] = `scale(2)`
})

再看源码里并没有刻意的处理这个事情,也没有用setTime 也没用requestAnimationFrame()。我又看了一下 才发现,在改变target的transform之前声明了一个变量force,但是这个变量也没用。那么我猜想,其实作者声明这个no use的变量就是为了处理这个问题。这个变量通过获取DOM的offsetHeight来主动触发了重绘。这样一想就明白了~~~ (其实是我没学问不认识英文🤭,源码上的备注写的很清楚force layout)

// force layout
var force = target.offsetHeight

最后在为target添加一个transEndEvent的事件,用来执行onOpen钩子

api.close(cb)

放大后再点击,触发的缩小恢复事件

传入一个参数,缩小事件后的callback,默认是config中提供的onClose钩子函数

close的逻辑大致和open反着来,将target样式和位置恢复到最初originalStyles,removeChild之前append的overlay,wrapper,placeholder。改变shown 和 el为false。最后执行onOpen钩子。

api.listen(el)

通过listen来添加click事件, 通过shown 和 el 来判断当前是调用api.close()还是api.open(el)


好好学习,天天向上~