jQuery原始碼分析系列(40): 動畫設計
前言
jQuery動畫是通過animate這個API設定執行的,其內部也是按照每一個animate的劃分封裝了各自動畫組的行為,
包括資料過濾、緩動公式、一些動畫預設引數的設定、元素狀態的調整、事件的處理通知機制、執行等等
換句話說,我們可以把animate看作一個物件,物件封裝自己的一系列屬性與方法。
jQuery可以支援連續動畫,那麼animate與animate之間的切換就是通過佇列.queue,這個之前就已經詳細的解釋過了
動畫的引數
jQuery的內部的方法都是針對API的處理範圍設計的
我們看看Animation方法的支援情況:
.animate( properties [, duration ] [, easing ] [, complete ] ) .animate( properties, options )
- 區別就與第二組資料的傳遞了,options是支援物件傳參
- properties引數就是寫一個CSS屬性和值的物件,動畫都是涉及變化的,那麼什麼值才能變化?
- 理論上來說有數值的屬性都是可以變化的,
width
,height
或者left
可以執行動畫,但是background-color
不能,但是也不是絕對的,主要看資料的解析度,可以用外掛支援 - 除了樣式屬性, 一些非樣式的屬性,如
scrollTop
和scrollLeft
,以及自定義屬性,也可應用於動畫 - 除了定義數值,每個屬效能使用
'show'
,'hide'
, 和'toggle'
。這些快捷方式允許定製隱藏和顯示動畫用來控制元素的顯示或隱藏。為了使用jQuery內建的切換狀態跟蹤,'toggle'
簡單的來說,就是把一對的引數丟大animate方法裡面,然後animate就開始執行你引數規定的動畫了,
那麼動畫每執一次就會通過回撥通知告訴開發者,具體有complete/done/fail/always/step介面等等
理解定義
<img id="book" alt="" width="100" height="123" style="background:red;opacity:1;position: relative; left: 500px;" /> book.animate({ opacity: 0.25, left:'50', height: 'toggle' }, { duration :1000, specialEasing: { height: 'linear' }, step: function(now, fx) { console.log('step') }, progress:function(){ console.log('progress') }, complete:function(){ console.log('動畫完成') } })
首先,動畫的引數都是最終值都是相對資料
如上img元素的起始
opacity是1,那麼通過動畫改成成0.25
left是500,那麼通過動畫改成成50
height為'toggle' 意味著如果是隱藏與顯示的自動切換
step:是針對opacity/left/height各自動畫,每次改變通知三次
progress 是把opacity/left/height看成一組了,每次改變只通知一次
動畫的原理
jQuery動畫的原理還是很簡單的,靠定時器不斷的改變元素的屬性
我們模擬下animate的大致實現
讓元素執行一個2秒的動畫到座標為left 50的區域
animate({
left: '50', duration: '2000' }
按照常規的思路,我們需要3個引數
- 動畫開始位置
- 動畫的結束位置
- 動畫的執行時間
思路一:等值變化
我們在animate內部還需要計算出當然元素的初始化佈局的位置(比如500px),那麼我們在2秒的時間內需變換成50px,也就是執行的路勁長就是500-50 = 450px
那麼演算法是不是呼之欲出了?
每毫秒移動的距離 pos = 450/2000 = 0.225px
每毫秒移動left = 初始位置 (+/-) 每毫秒遞增的距離(pos * 時間)
這樣演算法我們放到setInterval就會發現錯的一塌糊塗,我們錯最本質的東西:JS是單執行緒,定時器都是排佇列的,理論上也達不到1ms繪製一次dom
所以每次產生的這個下一次繪製的時間差根本不是一個等比的,所以我們按照線性的等值遞增是有誤的
function animate(elem, options){ //動畫初始值 var start = 500 //動畫結束值 var end = options.left //動畫id var timerId; var createTime = function(){ return (+new Date) } var startTime = createTime(); //需要執行動畫的長度 var anminLength = start - end; //每13毫秒要跑的位置 var pos = anminLength/options.time * 13 var pre = start; var newValue; function tick(){ if(createTime() - startTime < options.time){ newValue = pre - pos //動畫執行 elem.style['left'] = newValue + 'px'; pre = newValue }else{ //停止動畫 clearInterval(timerId); timerId = null; console.log(newValue) } } //開始執行動畫 var timerId = setInterval(tick, 13); }
思路一實現:
思路二:動態計算
setInterval的呼叫是不規律的,但是呼叫的時間是(2秒)是固定的,我們可以在每次呼叫的時候演算法時間差的比值,用這個比值去計算移動的距離就比較準確了
remaining = Math.max(0, startTime + duration - currentTime),
通過這個公司我們計算出,每次setInterval呼叫的時候,當前時間在總時間中的一個位置
remaining
看到沒有,這個值其實很符合定時器的特性,也是一個沒有規律的值
根據這個值,我們可以得出當前位置的一個百分比了
var remaining = Math.max(0, startTime + options.duration - createTime()) var temp = remaining / options.duration || 0; var percent = 1 - temp;
pecent
那麼這個移動的距離就很簡單了
我把整個公式就直接列出來了
var createTime = function(){ return (+new Date) } //元素初始化位置 var startLeft = 500; //元素終點位置 var endLeft = 50; //動畫執行時間 var duration = 2000; //動畫開始時間 var startTime = createTime(); function tick(){ //每次變化的時間 var remaining = Math.max(0, startTime + duration - createTime()) var temp = remaining / duration || 0; var percent = 1 - temp; //最終每次移動的left距離 var leftPos = (endLeft- startLeft) * percent +startLeft; } //開始執行動畫 setInterval(tick, 13);
leftPos就是每次移動的距離了,基本上比較準確了,事實上jQuery內部也就是這麼幹的
這裡13代表了動畫每秒執行幀數,預設是13毫秒。屬性值越小,在速度較快的瀏覽器中(例如,Chrome),動畫執行的越流暢,但是會影響程式的效能並且佔用更多的 CPU 資源
在新的遊覽器中,我們都可以採用requestAnimationFrame更優
思路二實現:
動畫的擴充套件
知道動畫處理的基本原理與演算法了,那麼jQuery在這個基礎上封裝擴充套件,讓動畫使用起來更靈活方便
我歸納有幾點:
- 引數的多形式傳遞
- 基於promise的事件反饋
- 增加屬性的show/hide/toggle的快捷方式
- 可以給css屬性設定獨立的緩動函式
基於promise的事件通知
得益於deferred的機制,可以讓一個物件轉化成帶有promise的特性,實現了done/fail/always/progress等等一系列的事件反饋介面
這樣的設計我們並不陌生在ready、ajax包括動畫都是基於這樣的非同步模型的結構
deferred = jQuery.Deferred() //生成一個動畫物件了 animation = deferred.promise({}) //混入動畫的屬性與方法
那麼這樣操作的一個好處就是,可以把邏輯處理都放到一塊
我們在程式碼的某一個環節針對特別的處理,需要臨時改變一些東西,但是在之後我們希望又恢復原樣,為了邏輯的清晰,我們可以引入deferred.alway方法
在某一個環節改了一個屬性,然後註冊到alway方法上一個完成的回撥用來恢復,這樣的邏輯塊是很清晰的
style.overflow = "hidden"; anim.always(function() { //完成後恢復溢位 style.overflow = opts.overflow[0]; style.overflowX = opts.overflow[1]; style.overflowY = opts.overflow[2]; });
增加屬性的show/hide/toggle的快捷方式
指定中文引數是比較特殊的,這種方式也是jQuery自己擴充套件的行為,邏輯上也很容易處理
ook.animate({ left: '50', height:'hide' },
height高度在動畫結束之後隱藏元素,那麼意味著元素本身的高度height也是需要改變的從初始的位置慢慢的遞減到0然後隱藏起來
程式碼中有這麼一段,針對hide的動作,我們在done之後會給元素直接隱藏起來
//目標是顯示 if (hidden) { jQuery(elem).show(); } else { //目標是隱藏 anim.done(function() { jQuery(elem).hide(); }); }
其實show與hide的流程是一樣的,只是針對元素在初始與結束的一個狀態的改變
css屬性設定獨立的緩動函式
在動畫預初始化之後(為了支援動畫,臨時改變元素的一些屬性與狀態),我們就需要給每一個屬性生成一個獨立的緩動物件了createTween,主要用於封裝這個動畫的演算法與執行的一些流程操作控制
////////////////// //生成對應的緩動動畫 // ////////////////// createTween: function(prop, end) { var tween = jQuery.Tween(elem, animation.opts, prop, end, animation.opts.specialEasing[prop] || animation.opts.easing); //加入到緩動佇列 animation.tweens.push(tween); return tween; },
tween物件
通過這個結構大概就知道了,這個就是用於生成動畫演算法所需要的一些資料與演算法的具體流程控制了
屬性預處理
- 針對height/width動畫的時候,要先處理本身元素溢位
- 針對height/width動畫的時候,元素本身的inline狀態處理
我們知道元素本身在佈局的時候可以用很多屬性對其設定,可是一旦進行動畫化的話,某些屬性的設定可能會對動畫的執行產生副作用,所以針對這樣的屬性,jQuery直接在內部做了最優的處理
如果我們進行元素height/width變化的時候,比如height:1,這樣的處理jQuery就需要針對元素做一些強制性的處理
1 新增overflow =“hidden”
2.如果設定了內聯並且沒有設定浮動 display = "inline-block";
因為內容溢位與內聯元素在執行動畫的時候,與這個height/width的邏輯是符合的
當然針對這樣的修改jQuery非常巧妙了用到了deferred.always方法,我們在執行動畫的時候,由於動畫的需要改了原始的屬性,但是動畫在結束之後,我們還是需要還原成其狀態
deferred量身定做的always方法,不管成功與失敗都會執行這個復原的邏輯
//設定溢位隱藏 if (opts.overflow) { style.overflow = "hidden"; anim.always(function() { //完成後恢復溢位 style.overflow = opts.overflow[0]; style.overflowX = opts.overflow[1]; style.overflowY = opts.overflow[2]; }); }
總結
通過上面不難看出,jQuery動畫其實原理上本身是不復雜的。量變產生質變,通過擴充套件大量的便捷方式加大了邏輯上的難度,但是從根本上來說:
主要包括:
- 屬性過濾specialEasing處理的propFilter方法
- 通過Deferred生成流程控制體系
- 通過defaultPrefilter方法對動畫執行的臨時修正
- 通過createTween方法,生成動畫的演算法與流程控制器
- 最後通過setInterval來控制每一個createTween物件的執行
大體上jQuery的動畫就這麼些內容,當然還有一些細節的話 遇到在提出來了,下章就會通過上面的這些處理,實現一個類jquery動畫的模擬了,加強理解