前端如何實(shí)現(xiàn)動(dòng)畫過渡效果
簡介
動(dòng)畫這個(gè)概念非常寬泛,涉及各個(gè)領(lǐng)域,這里我們把范圍縮小到前端網(wǎng)頁應(yīng)用層面上,不用講游戲領(lǐng)域的Animate,一切從最簡單的開始。
目前大部分網(wǎng)頁應(yīng)用都是基于框架開發(fā)的,比如Vue,React等,它們都是基于數(shù)據(jù)驅(qū)動(dòng)視圖的,那么讓我們來對(duì)比一下,還沒有這些框架的時(shí)候我們?nèi)绾螌?shí)現(xiàn)動(dòng)畫或者過渡效果,然后使用數(shù)據(jù)驅(qū)動(dòng)又是如何實(shí)現(xiàn)的。
傳統(tǒng)過渡動(dòng)畫
動(dòng)畫效果對(duì)體驗(yàn)有著非常重要的效果,但是對(duì)于很多開發(fā)者來講,可能是個(gè)非常薄弱的環(huán)節(jié)。在css3出現(xiàn)之后,很多初學(xué)者最常用的動(dòng)畫過渡可能就是css3的能力了。
css過渡動(dòng)畫
css啟動(dòng)過渡動(dòng)畫非常簡單,書寫transition屬性就可以了,下面寫一個(gè)demo
<div id="app" class="normal"></div>
.normal { width: 100px; height: 100px; background-color: red; transition: all 0.3s; } .normal:hover { background-color: yellow; width: 200px; height: 200px; }
效果還是很贊的,css3的transition基本滿足了大部分動(dòng)畫需求,如果不滿足還有真正的css3 animation。
大名鼎鼎的css動(dòng)畫庫,誰用誰知道。
不管是css3 transition 還是 css3 animation,我們簡單使用都是通過切換class類名,如果要做回調(diào)處理,瀏覽器也提供了 ontransitionend , onanimationend等動(dòng)畫幀事件,通過js接口進(jìn)行監(jiān)聽即可。
var el = document.querySelector('#app') el.addEventListener('transitionstart', () => { console.log('transition start') }) el.addEventListener('transitionend', () => { console.log('transition end') })
ok,這就是css動(dòng)畫的基礎(chǔ)了,通過js封裝也可以實(shí)現(xiàn)大部分的動(dòng)畫過渡需求,但是局限性在與只能控制css支持的屬性動(dòng)畫,相對(duì)來說控制力還是稍微弱一點(diǎn)。
js動(dòng)畫
js畢竟是自定義編碼程序,對(duì)于動(dòng)畫的控制力就很強(qiáng)大了,而且能實(shí)現(xiàn)各種css不支持的效果。 那么 js 實(shí)現(xiàn)動(dòng)畫的基礎(chǔ)是什么?
簡單來講,所謂動(dòng)畫就是在 時(shí)間軸上不斷更新某個(gè)元素的屬性,然后交給瀏覽器重新繪制,在視覺上就成了動(dòng)畫。廢話少說,還是先來個(gè)栗子:
<div id="app" class="normal"></div>
// Tween僅僅是個(gè)緩動(dòng)函數(shù) var el = document.querySelector('#app') var time = 0, begin = 0, change = 500, duration = 1000, fps = 1000 / 60; function startSport() { var val = Tween.Elastic.easeInOut(time, begin, change, duration); el.style.transform = 'translateX(' + val + 'px)'; if (time <= duration) { time += fps } else { console.log('動(dòng)畫結(jié)束重新開始') time = 0; } setTimeout(() => { startSport() }, fps) } startSport()
在時(shí)間軸上不斷更新屬性,可以通過setTimeout或者requestAnimation來實(shí)現(xiàn)。至于Tween緩動(dòng)函數(shù),就是類似于插值的概念,給定一系列變量,然后在區(qū)間段上可以獲取任意時(shí)刻的值,純數(shù)學(xué)公式,幾乎所有的動(dòng)畫框架都會(huì)使用,想了解的可以參考張鑫旭的Tween.js
OK,這個(gè)極簡demo也是js實(shí)現(xiàn)動(dòng)畫的核心基礎(chǔ)了,可以看到我們通過程序完美的控制了過渡值的生成過程,所有其他復(fù)雜的動(dòng)畫機(jī)制都是這個(gè)模式。
傳統(tǒng)和Vue/React框架對(duì)比
通過前面的例子,無論是css過渡還是js過渡,我們都是直接獲取到 dom元素的,然后對(duì)dom元素進(jìn)行屬性操作。
Vue/React都引入了虛擬dom的概念,數(shù)據(jù)驅(qū)動(dòng)視圖,我們盡量不去操作dom,只控制數(shù)據(jù),那么我們?nèi)绾卧跀?shù)據(jù)層面驅(qū)動(dòng)動(dòng)畫呢?
Vue框架下的過渡動(dòng)畫
可以先看一遍文檔
我們就不講如何使用了,我們來分析一下Vue提供的transition組件是如何實(shí)現(xiàn)動(dòng)畫過渡支持的。
transition組件
先看transition組件代碼,路徑 “src/platforms/web/runtime/components/transition.js”
核心代碼如下:
// 輔助函數(shù),復(fù)制props的數(shù)據(jù) export function extractTransitionData (comp: Component): Object { const data = {} const options: ComponentOptions = comp.$options // props for (const key in options.propsData) { data[key] = comp[key] } // events. const listeners: ?Object = options._parentListeners for (const key in listeners) { data[camelize(key)] = listeners[key] } return data } export default { name: 'transition', props: transitionProps, abstract: true, // 抽象組件,意思是不會(huì)真實(shí)渲染成dom,輔助開發(fā) render (h: Function) { // 通過slots獲取到真實(shí)渲染元素children let children: any = this.$slots.default const mode: string = this.mode const rawChild: VNode = children[0] // 添加唯一key // component instance. This key will be used to remove pending leaving nodes // during entering. const id: string = `__transition-${this._uid}-` child.key = getKey(id) : child.key // data上注入transition屬性,保存通過props傳遞的數(shù)據(jù) const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this) const oldRawChild: VNode = this._vnode const oldChild: VNode = getRealChild(oldRawChild) // important for dynamic transitions! const oldData: Object = oldChild.data.transition = extend({}, data) // handle transition mode if (mode === 'out-in') { // return placeholder node and queue update when leave finishes this._leaving = true mergeVNodeHook(oldData, 'afterLeave', () => { this._leaving = false this.$forceUpdate() }) return placeholder(h, rawChild) } else if (mode === 'in-out') { let delayedLeave const performLeave = () => { delayedLeave() } mergeVNodeHook(data, 'afterEnter', performLeave) mergeVNodeHook(data, 'enterCancelled', performLeave) mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave }) } return rawChild } }
可以看到,這個(gè)組件本身功能比較簡單,就是通過slots拿到需要渲染的元素children,然后把 transition的props屬性數(shù)據(jù)copy到data的transtion屬性上,供后續(xù)注入生命周期使用,mergeVNodeHook就是做生命周期管理的。
modules/transition
接著往下看生命周期相關(guān),路徑:
src/platforms/web/runtime/modules/transition.js
先看默認(rèn)導(dǎo)出:
function _enter (_: any, vnode: VNodeWithData) { if (vnode.data.show !== true) { enter(vnode) } } export default inBrowser ? { create: _enter, activate: _enter, remove (vnode: VNode, rm: Function) { if (vnode.data.show !== true) { leave(vnode, rm) } } } : {}
這里inBrowser就當(dāng)做true,因?yàn)槲覀兎治龅氖菫g覽器環(huán)境。
接著看enter 和 leave函數(shù),先看enter:
export function addTransitionClass (el: any, cls: string) { const transitionClasses = el._transitionClasses || (el._transitionClasses = []) if (transitionClasses.indexOf(cls) < 0) { transitionClasses.push(cls) addClass(el, cls) } } export function removeTransitionClass (el: any, cls: string) { if (el._transitionClasses) { remove(el._transitionClasses, cls) } removeClass(el, cls) } export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) { const el: any = vnode.elm // call leave callback now if (isDef(el._leaveCb)) { el._leaveCb.cancelled = true el._leaveCb() } // 上一步注入data的transition數(shù)據(jù) const data = resolveTransition(vnode.data.transition) if (isUndef(data)) { return } /* istanbul ignore if */ if (isDef(el._enterCb) || el.nodeType !== 1) { return } const { css, type, enterClass, enterToClass, enterActiveClass, appearClass, appearToClass, appearActiveClass, beforeEnter, enter, afterEnter, enterCancelled, beforeAppear, appear, afterAppear, appearCancelled, duration } = data let context = activeInstance let transitionNode = activeInstance.$vnode const isAppear = !context._isMounted || !vnode.isRootInsert if (isAppear && !appear && appear !== '') { return } // 獲取合適的時(shí)機(jī)應(yīng)該注入的className const startClass = isAppear && appearClass ? appearClass : enterClass const activeClass = isAppear && appearActiveClass ? appearActiveClass : enterActiveClass const toClass = isAppear && appearToClass ? appearToClass : enterToClass const beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter const enterHook = isAppear ? (typeof appear === 'function' ? appear : enter) : enter const afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter const enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled const explicitEnterDuration: any = toNumber( isObject(duration) ? duration.enter : duration ) const expectsCSS = css !== false && !isIE9 const userWantsControl = getHookArgumentsLength(enterHook) // 過渡結(jié)束之后的回調(diào)處理,刪掉進(jìn)入時(shí)的class const cb = el._enterCb = once(() => { if (expectsCSS) { removeTransitionClass(el, toClass) removeTransitionClass(el, activeClass) } if (cb.cancelled) { if (expectsCSS) { removeTransitionClass(el, startClass) } enterCancelledHook && enterCancelledHook(el) } else { afterEnterHook && afterEnterHook(el) } el._enterCb = null }) // dom進(jìn)入時(shí),添加start class進(jìn)行過渡 beforeEnterHook && beforeEnterHook(el) if (expectsCSS) { // 設(shè)置過渡開始之前的默認(rèn)樣式 addTransitionClass(el, startClass) addTransitionClass(el, activeClass) // 瀏覽器渲染下一幀 刪除默認(rèn)樣式,添加toClass // 添加end事件監(jiān)聽,回調(diào)就是上面的cb nextFrame(() => { removeTransitionClass(el, startClass) if (!cb.cancelled) { addTransitionClass(el, toClass) if (!userWantsControl) { if (isValidDuration(explicitEnterDuration)) { setTimeout(cb, explicitEnterDuration) } else { whenTransitionEnds(el, type, cb) } } } }) } if (vnode.data.show) { toggleDisplay && toggleDisplay() enterHook && enterHook(el, cb) } if (!expectsCSS && !userWantsControl) { cb() } }
enter里使用了一個(gè)函數(shù)whenTransitionEnds,其實(shí)就是監(jiān)聽過渡或者動(dòng)畫結(jié)束的事件:
export let transitionEndEvent = 'transitionend' export let animationEndEvent = 'animationend' export function whenTransitionEnds ( el: Element, expectedType: ?string, cb: Function ) { const { type, timeout, propCount } = getTransitionInfo(el, expectedType) if (!type) return cb() const event: string = type === TRANSITION ? transitionEndEvent : animationEndEvent let ended = 0 const end = () => { el.removeEventListener(event, onEnd) cb() } const onEnd = e => { if (e.target === el) { if (++ended >= propCount) { end() } } } setTimeout(() => { if (ended < propCount) { end() } }, timeout + 1) el.addEventListener(event, onEnd) }
OK,到了這里,根據(jù)上面源代碼的注釋分析,我們可以發(fā)現(xiàn):
- Vue先是封裝了一些列操作dom className的輔助方法addClass/removeClass等。
- 然后在生命周期enterHook之后,馬上設(shè)置了startClass也就是enterClass的默認(rèn)初始樣式,還有activeClass
- 緊接著在瀏覽器nextFrame下一幀,移除了startClass,添加了toClass,并且添加了過渡動(dòng)畫的end事件監(jiān)聽處理
- 監(jiān)聽到end事件之后,調(diào)動(dòng)cb,移除了toClass和activeClass
leave的過程和enter的處理過程是一樣,只不過是反向添加移除className
結(jié)論:Vue的動(dòng)畫過渡處理方式和 傳統(tǒng)dom本質(zhì)上是一樣,只不過融入了Vue的各個(gè)生命周期里進(jìn)行處理,本質(zhì)上還是在dom 添加刪除的時(shí)機(jī)進(jìn)行處理
React里的過渡動(dòng)畫
噢,我們翻篇了React的文檔,也沒有發(fā)現(xiàn)有過渡動(dòng)畫的處理。嘿,看來官方不原生支持。
但是我們可以自己實(shí)現(xiàn),比如通過useState維護(hù)一個(gè)狀態(tài),在render里根據(jù)狀態(tài)進(jìn)行className的切換,但是復(fù)雜的該怎么辦?
所幸在社區(qū)找到了一個(gè)輪子插件react-transition-group
嗯,直接貼源碼,有了前面Vue的分析,這個(gè)非常容易理解,反而更簡單:
class Transition extends React.Component { static contextType = TransitionGroupContext constructor(props, context) { super(props, context) let parentGroup = context let appear = parentGroup && !parentGroup.isMounting ? props.enter : props.appear let initialStatus this.appearStatus = null if (props.in) { if (appear) { initialStatus = EXITED this.appearStatus = ENTERING } else { initialStatus = ENTERED } } else { if (props.unmountOnExit || props.mountOnEnter) { initialStatus = UNMOUNTED } else { initialStatus = EXITED } } this.state = { status: initialStatus } this.nextCallback = null } // 初始dom的時(shí)候,更新默認(rèn)初始狀態(tài) componentDidMount() { this.updateStatus(true, this.appearStatus) } // data更新的時(shí)候,更新對(duì)應(yīng)的狀態(tài) componentDidUpdate(prevProps) { let nextStatus = null if (prevProps !== this.props) { const { status } = this.state if (this.props.in) { if (status !== ENTERING && status !== ENTERED) { nextStatus = ENTERING } } else { if (status === ENTERING || status === ENTERED) { nextStatus = EXITING } } } this.updateStatus(false, nextStatus) } updateStatus(mounting = false, nextStatus) { if (nextStatus !== null) { // nextStatus will always be ENTERING or EXITING. this.cancelNextCallback() if (nextStatus === ENTERING) { this.performEnter(mounting) } else { this.performExit() } } else if (this.props.unmountOnExit && this.state.status === EXITED) { this.setState({ status: UNMOUNTED }) } } performEnter(mounting) { const { enter } = this.props const appearing = this.context ? this.context.isMounting : mounting const [maybeNode, maybeAppearing] = this.props.nodeRef ? [appearing] : [ReactDOM.findDOMNode(this), appearing] const timeouts = this.getTimeouts() const enterTimeout = appearing ? timeouts.appear : timeouts.enter // no enter animation skip right to ENTERED // if we are mounting and running this it means appear _must_ be set if ((!mounting && !enter) || config.disabled) { this.safeSetState({ status: ENTERED }, () => { this.props.onEntered(maybeNode) }) return } this.props.onEnter(maybeNode, maybeAppearing) this.safeSetState({ status: ENTERING }, () => { this.props.onEntering(maybeNode, maybeAppearing) this.onTransitionEnd(enterTimeout, () => { this.safeSetState({ status: ENTERED }, () => { this.props.onEntered(maybeNode, maybeAppearing) }) }) }) } performExit() { const { exit } = this.props const timeouts = this.getTimeouts() const maybeNode = this.props.nodeRef ? undefined : ReactDOM.findDOMNode(this) // no exit animation skip right to EXITED if (!exit || config.disabled) { this.safeSetState({ status: EXITED }, () => { this.props.onExited(maybeNode) }) return } this.props.onExit(maybeNode) this.safeSetState({ status: EXITING }, () => { this.props.onExiting(maybeNode) this.onTransitionEnd(timeouts.exit, () => { this.safeSetState({ status: EXITED }, () => { this.props.onExited(maybeNode) }) }) }) } cancelNextCallback() { if (this.nextCallback !== null) { this.nextCallback.cancel() this.nextCallback = null } } safeSetState(nextState, callback) { // This shouldn't be necessary, but there are weird race conditions with // setState callbacks and unmounting in testing, so always make sure that // we can cancel any pending setState callbacks after we unmount. callback = this.setNextCallback(callback) this.setState(nextState, callback) } setNextCallback(callback) { let active = true this.nextCallback = event => { if (active) { active = false this.nextCallback = null callback(event) } } this.nextCallback.cancel = () => { active = false } return this.nextCallback } // 監(jiān)聽過渡end onTransitionEnd(timeout, handler) { this.setNextCallback(handler) const node = this.props.nodeRef ? this.props.nodeRef.current : ReactDOM.findDOMNode(this) const doesNotHaveTimeoutOrListener = timeout == null && !this.props.addEndListener if (!node || doesNotHaveTimeoutOrListener) { setTimeout(this.nextCallback, 0) return } if (this.props.addEndListener) { const [maybeNode, maybeNextCallback] = this.props.nodeRef ? [this.nextCallback] : [node, this.nextCallback] this.props.addEndListener(maybeNode, maybeNextCallback) } if (timeout != null) { setTimeout(this.nextCallback, timeout) } } render() { const status = this.state.status if (status === UNMOUNTED) { return null } const { children, // filter props for `Transition` in: _in, mountOnEnter: _mountOnEnter, unmountOnExit: _unmountOnExit, appear: _appear, enter: _enter, exit: _exit, timeout: _timeout, addEndListener: _addEndListener, onEnter: _onEnter, onEntering: _onEntering, onEntered: _onEntered, onExit: _onExit, onExiting: _onExiting, onExited: _onExited, nodeRef: _nodeRef, ...childProps } = this.props return ( // allows for nested Transitions <TransitionGroupContext.Provider value={null}> {typeof children === 'function' ? children(status, childProps) : React.cloneElement(React.Children.only(children), childProps)} </TransitionGroupContext.Provider> ) } }
可以看到,和Vue是非常相似的,只不過這里變成了在React的各個(gè)生命周期函數(shù)了進(jìn)行處理。
到了這里,我們會(huì)發(fā)現(xiàn)不管是Vue的transiton組件,還是React這個(gè)transiton-group組件,著重處理的都是css屬性的動(dòng)畫。
數(shù)據(jù)驅(qū)動(dòng)的動(dòng)畫
而實(shí)際場景中總是會(huì)遇到css無法處理的動(dòng)畫,這個(gè)時(shí)候,可以有兩種解決方案:
通過ref獲取dom,然后采用我們傳統(tǒng)的js方案。
通過state狀態(tài)維護(hù)繪制dom的數(shù)據(jù),不斷通過setState更新state類驅(qū)動(dòng)視圖自動(dòng)刷新
以上就是前端如何實(shí)現(xiàn)動(dòng)畫過渡效果的詳細(xì)內(nèi)容,更多關(guān)于前端實(shí)現(xiàn)動(dòng)畫過渡效果的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
DropDownList控件綁定數(shù)據(jù)源的三種方法
本文給大家分享web 中 DropDownList綁定數(shù)據(jù)源的幾種方式以及DropdownList控件動(dòng)態(tài)綁定數(shù)據(jù)源的兩種方法,下面通過本文給大家詳細(xì)介紹,感興趣的朋友一起看看2016-12-12琥珀無限級(jí)聯(lián)動(dòng)菜單-JavaScript版
琥珀無限級(jí)聯(lián)動(dòng)菜單-JavaScript版...2006-11-11js實(shí)現(xiàn)按Ctrl+Enter發(fā)送效果
按Ctrl+Enter發(fā)送,思路是監(jiān)聽textarea的onkeydown事件,當(dāng)ctrl鍵被按下,并且,keycode為13(回車),時(shí),調(diào)用發(fā)送表單的函數(shù)2014-09-09