前端如何實(shí)現(xiàn)動(dòng)畫(huà)過(guò)渡效果
簡(jiǎn)介
動(dòng)畫(huà)這個(gè)概念非常寬泛,涉及各個(gè)領(lǐng)域,這里我們把范圍縮小到前端網(wǎng)頁(yè)應(yīng)用層面上,不用講游戲領(lǐng)域的Animate,一切從最簡(jiǎn)單的開(kāi)始。
目前大部分網(wǎng)頁(yè)應(yīng)用都是基于框架開(kāi)發(fā)的,比如Vue,React等,它們都是基于數(shù)據(jù)驅(qū)動(dòng)視圖的,那么讓我們來(lái)對(duì)比一下,還沒(méi)有這些框架的時(shí)候我們?nèi)绾螌?shí)現(xiàn)動(dòng)畫(huà)或者過(guò)渡效果,然后使用數(shù)據(jù)驅(qū)動(dòng)又是如何實(shí)現(xiàn)的。
傳統(tǒng)過(guò)渡動(dòng)畫(huà)
動(dòng)畫(huà)效果對(duì)體驗(yàn)有著非常重要的效果,但是對(duì)于很多開(kāi)發(fā)者來(lái)講,可能是個(gè)非常薄弱的環(huán)節(jié)。在css3出現(xiàn)之后,很多初學(xué)者最常用的動(dòng)畫(huà)過(guò)渡可能就是css3的能力了。
css過(guò)渡動(dòng)畫(huà)
css啟動(dòng)過(guò)渡動(dòng)畫(huà)非常簡(jiǎn)單,書(shū)寫(xiě)transition屬性就可以了,下面寫(xiě)一個(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)畫(huà)需求,如果不滿足還有真正的css3 animation。
大名鼎鼎的css動(dòng)畫(huà)庫(kù),誰(shuí)用誰(shuí)知道。
不管是css3 transition 還是 css3 animation,我們簡(jiǎn)單使用都是通過(guò)切換class類名,如果要做回調(diào)處理,瀏覽器也提供了 ontransitionend , onanimationend等動(dòng)畫(huà)幀事件,通過(guò)js接口進(jìn)行監(jiān)聽(tīng)即可。
var el = document.querySelector('#app')
el.addEventListener('transitionstart', () => {
console.log('transition start')
})
el.addEventListener('transitionend', () => {
console.log('transition end')
})
ok,這就是css動(dòng)畫(huà)的基礎(chǔ)了,通過(guò)js封裝也可以實(shí)現(xiàn)大部分的動(dòng)畫(huà)過(guò)渡需求,但是局限性在與只能控制css支持的屬性動(dòng)畫(huà),相對(duì)來(lái)說(shuō)控制力還是稍微弱一點(diǎn)。
js動(dòng)畫(huà)
js畢竟是自定義編碼程序,對(duì)于動(dòng)畫(huà)的控制力就很強(qiáng)大了,而且能實(shí)現(xiàn)各種css不支持的效果。 那么 js 實(shí)現(xiàn)動(dòng)畫(huà)的基礎(chǔ)是什么?
簡(jiǎn)單來(lái)講,所謂動(dòng)畫(huà)就是在 時(shí)間軸上不斷更新某個(gè)元素的屬性,然后交給瀏覽器重新繪制,在視覺(jué)上就成了動(dòng)畫(huà)。廢話少說(shuō),還是先來(lái)個(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)畫(huà)結(jié)束重新開(kāi)始')
time = 0;
}
setTimeout(() => {
startSport()
}, fps)
}
startSport()
在時(shí)間軸上不斷更新屬性,可以通過(guò)setTimeout或者requestAnimation來(lái)實(shí)現(xiàn)。至于Tween緩動(dòng)函數(shù),就是類似于插值的概念,給定一系列變量,然后在區(qū)間段上可以獲取任意時(shí)刻的值,純數(shù)學(xué)公式,幾乎所有的動(dòng)畫(huà)框架都會(huì)使用,想了解的可以參考張?chǎng)涡竦腡ween.js
OK,這個(gè)極簡(jiǎn)demo也是js實(shí)現(xiàn)動(dòng)畫(huà)的核心基礎(chǔ)了,可以看到我們通過(guò)程序完美的控制了過(guò)渡值的生成過(guò)程,所有其他復(fù)雜的動(dòng)畫(huà)機(jī)制都是這個(gè)模式。
傳統(tǒng)和Vue/React框架對(duì)比
通過(guò)前面的例子,無(wú)論是css過(guò)渡還是js過(guò)渡,我們都是直接獲取到 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)畫(huà)呢?
Vue框架下的過(guò)渡動(dòng)畫(huà)
可以先看一遍文檔
我們就不講如何使用了,我們來(lái)分析一下Vue提供的transition組件是如何實(shí)現(xiàn)動(dòng)畫(huà)過(guò)渡支持的。
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,輔助開(kāi)發(fā)
render (h: Function) {
// 通過(guò)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屬性,保存通過(guò)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è)組件本身功能比較簡(jiǎn)單,就是通過(guò)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)
// 過(guò)渡結(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)行過(guò)渡
beforeEnterHook && beforeEnterHook(el)
if (expectsCSS) {
// 設(shè)置過(guò)渡開(kāi)始之前的默認(rèn)樣式
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
// 瀏覽器渲染下一幀 刪除默認(rèn)樣式,添加toClass
// 添加end事件監(jiān)聽(tīng),回調(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)聽(tīng)過(guò)渡或者動(dòng)畫(huà)結(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,并且添加了過(guò)渡動(dòng)畫(huà)的end事件監(jiān)聽(tīng)處理
- 監(jiān)聽(tīng)到end事件之后,調(diào)動(dòng)cb,移除了toClass和activeClass
leave的過(guò)程和enter的處理過(guò)程是一樣,只不過(guò)是反向添加移除className
結(jié)論:Vue的動(dòng)畫(huà)過(guò)渡處理方式和 傳統(tǒng)dom本質(zhì)上是一樣,只不過(guò)融入了Vue的各個(gè)生命周期里進(jìn)行處理,本質(zhì)上還是在dom 添加刪除的時(shí)機(jī)進(jìn)行處理
React里的過(guò)渡動(dòng)畫(huà)
噢,我們翻篇了React的文檔,也沒(méi)有發(fā)現(xiàn)有過(guò)渡動(dòng)畫(huà)的處理。嘿,看來(lái)官方不原生支持。
但是我們可以自己實(shí)現(xiàn),比如通過(guò)useState維護(hù)一個(gè)狀態(tài),在render里根據(jù)狀態(tài)進(jìn)行className的切換,但是復(fù)雜的該怎么辦?
所幸在社區(qū)找到了一個(gè)輪子插件react-transition-group
嗯,直接貼源碼,有了前面Vue的分析,這個(gè)非常容易理解,反而更簡(jiǎn)單:
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)聽(tīng)過(guò)渡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是非常相似的,只不過(guò)這里變成了在React的各個(gè)生命周期函數(shù)了進(jìn)行處理。
到了這里,我們會(huì)發(fā)現(xiàn)不管是Vue的transiton組件,還是React這個(gè)transiton-group組件,著重處理的都是css屬性的動(dòng)畫(huà)。
數(shù)據(jù)驅(qū)動(dòng)的動(dòng)畫(huà)
而實(shí)際場(chǎng)景中總是會(huì)遇到css無(wú)法處理的動(dòng)畫(huà),這個(gè)時(shí)候,可以有兩種解決方案:
通過(guò)ref獲取dom,然后采用我們傳統(tǒng)的js方案。
通過(guò)state狀態(tài)維護(hù)繪制dom的數(shù)據(jù),不斷通過(guò)setState更新state類驅(qū)動(dòng)視圖自動(dòng)刷新
以上就是前端如何實(shí)現(xiàn)動(dòng)畫(huà)過(guò)渡效果的詳細(xì)內(nèi)容,更多關(guān)于前端實(shí)現(xiàn)動(dòng)畫(huà)過(guò)渡效果的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
DropDownList控件綁定數(shù)據(jù)源的三種方法
本文給大家分享web 中 DropDownList綁定數(shù)據(jù)源的幾種方式以及DropdownList控件動(dòng)態(tài)綁定數(shù)據(jù)源的兩種方法,下面通過(guò)本文給大家詳細(xì)介紹,感興趣的朋友一起看看2016-12-12
微信小程序?qū)崿F(xiàn)簡(jiǎn)易table表格
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)簡(jiǎn)易table表格,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02
琥珀無(wú)限級(jí)聯(lián)動(dòng)菜單-JavaScript版
琥珀無(wú)限級(jí)聯(lián)動(dòng)菜單-JavaScript版...2006-11-11
js監(jiān)聽(tīng)F11觸發(fā)全屏事件簡(jiǎn)單代碼舉例
這篇文章主要給大家介紹了關(guān)于js監(jiān)聽(tīng)F11觸發(fā)全屏事件的相關(guān)資料,自己工作中遇到的一點(diǎn)點(diǎn)小問(wèn)題,記錄一下,希望也能對(duì)你們有幫助,需要的朋友可以參考下2024-05-05
js實(shí)現(xiàn)按Ctrl+Enter發(fā)送效果
按Ctrl+Enter發(fā)送,思路是監(jiān)聽(tīng)textarea的onkeydown事件,當(dāng)ctrl鍵被按下,并且,keycode為13(回車),時(shí),調(diào)用發(fā)送表單的函數(shù)2014-09-09

