1. 程式人生 > >Web高效能動畫及渲染原理(1)CSS動畫和JS動畫

Web高效能動畫及渲染原理(1)CSS動畫和JS動畫

目錄

  • 一. CSS動畫 和 JS動畫
    • 1.1 CSS動畫
    • 1.2 JS動畫
    • 1.3 小結
  • 二. 使用Velocity.js實現動畫

示例程式碼託管在:http://www.github.com/dashnowords/blogs

部落格園地址:《大史住在大前端》原創博文目錄

華為雲社群地址:【你要的前端打怪升級指南】

一. CSS動畫 和 JS動畫

Web動畫的本質是元素狀態改變造成的樣式變更,CSS動畫和JS動畫的區別並不是由語言來決定的,而是由兩者的特點和適用場景來判斷的。CSS

動畫簡潔高效,提升互動體驗而編寫的程式碼可以輕鬆地和主要業務邏輯之間實現隔離,開發中建議優先使用;而當你需要更豐富的緩動函式,多物件關聯動畫或是需要在動畫執行的特定時間點關聯一些其他的業務邏輯等需要細節控制的場景中,JS動畫就會顯得更加清晰且易維護,兩者從來都不是非黑即白的選項。

1.1 CSS動畫

CSS動畫通常指使用transition實現的過渡動畫和使用animation來實現的關鍵幀動畫。

transition動畫

transition動畫也被稱為“簡易補間動畫”,需要提供起始和結束兩個關鍵幀,瀏覽器才能夠完成樣式差異比對並計算出對應的過渡動畫。開發者編寫的CSS程式碼會在渲染之前被瀏覽器使用(也就是生成CSSOM

的過程),所以對於被渲染出來的元素而言,首屏渲染的結果就可以被當做是起始關鍵幀,那麼結束關鍵幀從哪裡來?首先通過JS指令碼來修改指定元素的樣式或是類名是可行的,另一種方式就是利用帶有互動事件屬性的CSS偽類(例如:hover或是:focus),當對應的事件觸發時,新的樣式就會作用於指定元素,這種特性也可以理解為CSS語法中的事件回撥機制。當結束關鍵幀被建立後,瀏覽器就可以自動計算兩者之間的差異並完成過渡動畫。

transition動畫的要點就是具有樣式差異的兩個關鍵幀。如果CSS程式碼中只包含一般的靜態選擇器(指CSS程式碼中不包含能夠造成HTML元素狀態變更的選擇器),那麼被渲染出的元素在整個生命週期中就只會擁有一個關鍵幀,也就是首次被渲染時的樣式,而1個關鍵幀或是2個沒有樣式差異的關鍵幀都無法進行插值計算,這也就不難理解為什麼首屏渲染時transition

不會生效了。

所以transition動畫比較適合被用來實現指定元素在兩個明確的包含樣式差異的狀態之間往復切換的場景,像是滑鼠的移入移出,元素的聚焦失焦等。

animation動畫

animation動畫需要使用@keyframes關鍵詞先將動畫過程抽象出來,然後將其關聯給指定元素的animation屬性,它可以看做是transition動畫的加強版。

使用@keyframes定義動畫時通常需要指定fromto兩個狀態(也可以使用0100%),這意味著開發者只要按照語法要求去定義一個動畫過程,它至少會包含兩個關鍵幀,所以即使沒有CSS偽類或JS指令碼的幫助,依然可以獨立實現動畫。如果沒有定義from起始關鍵幀的樣式,animation動畫也不會出錯,它會預設以指定元素在動畫開始時刻的樣式作為起始關鍵幀,並結合to定義的結束關鍵幀和指定元素的animation定製引數來完成補間動畫的計算,所以即使像下面這樣的簡陋寫法在首屏渲染時依然可以生效:

<style>
    .main{
        height:100px;
        width:100px;
        animation:fadeIn 2s linear;
    }
    @keyframes fadeIn{
        to{
            background-color:yellowgreen;
        }
    }
</style>
<body>
   <div class="main"></div>
</body>

其次,和transition過渡動畫不同的是,animation動畫在不存在樣式差異的關鍵幀之間也會執行動畫,附件的示例demo中已經展示了上述幾種不同動畫實現方式,你可以使用Chrome DevTools中的Animations面板中來檢視動畫的觸發效果:

最後,animation動畫最顯著的特點就是起止狀態之間可以定義多箇中間幀,這部分就不再贅述。綜上可知,animation是一種強制執行的動畫,既對transition過渡動畫失效的場景進行了補充實現,同時也增加了動畫細節的可定製性(例如迴圈動畫或往復動畫的實現),但它的功能擴充套件仍然是針對單過程動畫的。關於animation動畫還不熟悉的讀者可以檢視【MDN-CSS Animations】。

1.2 JS動畫

JS動畫並不是指Web Animations API(MDN文件——Web Animations API ),它畢竟還只是個草案,瞭解一下即可。本節所說的JS動畫,既包括在指令碼中修改元素類名或動畫樣式的方式,也包括區別於【關鍵幀動畫】的另一種形式——【逐幀動畫】。逐幀動畫不再借助瀏覽器內部的插值機制來生成渲染畫面,而是將對應的邏輯在JavaScript中實現,每一幀的狀態都由JS來計算生成,然後藉助requestAnimationFrame來將動畫中的每一幀傳遞到渲染管線中,你可以使用任何自定義的時間函式來執行動畫,也可以同時方便地管理多個物件的多個不同動畫,另外動畫的進度也是全生命週期可感知的(CSS動畫只有animationstartanimationend等少量的事件),你可以自由地實現動畫暫停或者恢復,又或者是在動畫執行到某一特定時刻時觸發其他的邏輯,很明顯,JS動畫在細節控制能力、過程管理能力以及多物件管理能力上都要比純CSS動畫更強大,但隨之而來的複雜性也是必須要付出的代價,另一方面,JS程式碼執行在主執行緒之中,主執行緒的實時工況會對動畫的流暢度造成極大影響,而CSS動畫則不必擔心。

以一個列表項的渲染動畫為例,通常都會採用階梯交錯動畫(也稱為stagger動畫)來實現,階梯交錯動畫中,每一個元素執行的動畫實際上是一樣的,但是需要在前一個元素的動畫過程執行到特定時間點時自己才能開始執行動畫,後續的元素依次類推,就需要為每一個動畫執行項的animation屬性設定遞增的delay值,這樣的需求使用原生CSS既難編寫也難維護,它通常需要藉助預編譯器才能夠實現,但是如果在JS指令碼中來完成相同的設定,相信大部分前端開發者都可以輕鬆做到。

1.3 小結

所以綜上可知,動畫的編寫姿勢,實際上就是在CSS的簡潔性和JS的細節控制力之間找到一個平衡點。CSS動畫可以使用著名的animate.css預設動畫庫,而JS動畫可以藉助velocity.js來實現,當然他們都不是唯一的選擇。

二. 使用Velocity.js實現動畫

velocity.js是一個非常易用的輕量級動畫庫,它包含了jQuery$.animate( )方法的全部功能,但是比jQuery更流暢。velocity.js的呼叫方式非常簡單,既支援全域性函式的形式呼叫,也支援物件方法的形式呼叫,在原始碼的主檔案src/velocity.ts中可以看到下面的程式碼:

if (window) {
    const jQuery: {fn: any} = (window as any).jQuery,
        Zepto: {fn: any} = (window as any).Zepto;

    patchFn(window, true);
    patchFn(Element && Element.prototype);
    patchFn(NodeList && NodeList.prototype);
    patchFn(HTMLCollection && HTMLCollection.prototype);

    patchFn(jQuery, true);
    patchFn(jQuery && jQuery.fn);

    patchFn(Zepto, true);
    patchFn(Zepto && Zepto.fn);
}

也就是說無論你使用原生JavaScript語法,還是專案中已經引用了jQueryZepto,都可以在返回的結果集上以物件方法的形式來呼叫velocity函式(當然也可以用靜態方法的形式來呼叫),velocity方法具有多個方法過載,一般形式為接收兩個引數,第一個引數是下一個關鍵幀的樣式,它和CSS中定義關鍵幀沒什麼本質區別,第二個引數是對動畫細節的定製,當多次呼叫velocity物件方法時就可以實現多步驟動畫的效果,所以在適合的場景中下面的呼叫都是合法的:

let element = document.querySelector('div');

//全域性函式
Velocity(element, {width:200},{duration:2000});

//原生節點集合的物件方法呼叫
element.velocity({width:200},{duration:2000});

//jQuery或Zepto中的呼叫
$(element).velocity({width:200},{duration:2000});
$('div').velocity({width:200},{duration:2000});

//多步驟動畫
$('div')
    .velocity({width:200},{duration:2000})
    .velocity({height:100},{duration:2000})
    .velocity({backgroundColor:'#3498db'},{duration:2000});

velocity.jsV2版本還處在beta階段,API文件需要在官方倉庫的wiki中檢視【velocity.js V2文件】,它提供的主要擴充套件能力如下:

  • 事件鉤子

    熟悉現代SPA開發的小夥伴肯定不會對事件鉤子感到陌生,類元件中的生命週期鉤子就是這種形式,當用戶希望某些自定義方法可以在特定時刻執行時,就可以使用velocity中的事件鉤子將自定義方法和動畫的執行關聯起來,很明顯,這種機制的存在增加了動畫的互動和感知性,開發者可以在各個感興趣的階段鉤入自己期望執行的函式。velocity.js中提供的事件鉤子包括:begin(在動畫開始時觸發),complete(動畫結束時觸發),progress(動畫過程中觸發),progress鉤子每次執行時可以獲取到動畫執行情況的細節,例如元素的引用、完成進度的百分比、剩餘的時間以及和緩動函式有關的資料:

    element.velocity({
        width:100
    },{
        begin:function(){/*...*/},
        progress:function(elements, percentComplete, remaining, tweenValue, activeCall){},
        complete:function(){/*...*/}
    });
  • 動畫的編排和調控

    velocity.js可以很方便地對有約束關係的多個動畫進行管理和編排。例如通過配置queue:String引數,就可以同時維護多個佇列,以便同時管理多個併發的順序執行佇列;配置stagger:Number引數,就可以解決上一節中提到的階梯交錯動畫的場景;speed:Number引數可以改變動畫執行的速度;loop可以實現往返動畫;repeat可以實現單向重複動畫;例如前一節中提及的階梯交錯動畫就可以用下面的程式碼方便地實現:

    document.querySelectorAll('.box').velocity({marginLeft:500},{duration:5000,stagger:200});

    velocity.js中還可以用命令的方式直接控制動畫的執行,命令的使用格式方式為:

    element.velocity(COMMAND_STRING);

    常用的命令字串包括pause(暫停動畫),resume(恢復暫停的動畫),stop(停止動畫並保持當前狀態),finish(結束動畫並應用結束狀態)以及用於註冊自定義命令、自定義緩動函式甚至自定義預設動畫等的registerXXX命令。例如一段通過按鈕點選來控制動畫暫停和播放的程式碼:

    function bindControl(){
        let flag = true;
        let dom =  document.querySelector('#btn');
        dom.addEventListener('click',function(){
            dom.velocity(flag ? 'pause':'resume');
            flag = !flag;
        });
    }
  • 整合預設動畫

    如果你曾經使用過animate.css預設動畫庫,那麼恭喜你,在velocity你依然可以用同樣的預設動畫名來實現動畫,使用時需要引入額外的補丁庫:

    <script src="./jquery.min.js"></script>
    <script src="./velocity.min.js"></script>
    <script src="./velocity.ui.min.js"></script>

    預設動畫可以直接傳入關鍵詞來使用:

    document.querySelector('.box').velocity('jello'); 
    //也可以覆蓋預設的動畫引數
    document.querySelector('.box').velocity('jello',{ duration:2000 }); 

如果對各種動畫形式還不熟悉,可以直接在【Animate.css官方網站】上直接檢視預設動畫的效果。不難看出,純CSS動畫面臨的問題在JavaScript的幫助下基本都得到了解決。下一篇中將分析瀏覽器高效能動畫的實現,敬請期