理清執(zhí)行上下文、作用域、閉包這三個(gè)關(guān)系之前,我們需要先理解一些概念。
我們知道所學(xué)的高級語言分為兩類:1、編譯型 2、解釋型
編譯型(compilation):將整個(gè)代碼立刻轉(zhuǎn)化為機(jī)器代碼,然后寫下機(jī)器代碼轉(zhuǎn)換為可在任何計(jì)算機(jī)上執(zhí)行的可移植文件,然后在進(jìn)行執(zhí)行。
解釋:解釋器貫穿整個(gè)源代碼進(jìn)行一行一行的讀取執(zhí)行,但是還是會轉(zhuǎn)為機(jī)器代碼,但是發(fā)生在執(zhí)行前,而不是提前到和編譯一樣,整體進(jìn)行轉(zhuǎn)換為機(jī)器代碼。
對于Javascript,我們了解到的是它是一個(gè)解釋型語言,但是隨著后續(xù)的發(fā)展,常用于web或者服務(wù)端的開發(fā),性能難以跟上。因此現(xiàn)代JavaScript是實(shí)時(shí)編譯(JIT),將匯編和解釋混合在一起,對整個(gè)源文件代碼進(jìn)行轉(zhuǎn)化為機(jī)器代碼,然后立刻執(zhí)行,這樣速度就更快些,省去了可移植文件,節(jié)省了時(shí)間。
現(xiàn)代JavaScript在V8引擎的工作原理:
因此JavaScript代碼的整個(gè)執(zhí)行過程,分為兩個(gè)階段:
明白這些之后,我們繼續(xù)說執(zhí)行上下文和作用域。
執(zhí)行上下文是一段JavaScript代碼執(zhí)行的環(huán)境,包含了所有必要的信息以供其執(zhí)行。
上面畫過一張V8引擎運(yùn)行圖,當(dāng)時(shí)所講的詞法環(huán)境是在 編譯階段產(chǎn)生,現(xiàn)在講的執(zhí)行上下文是在引擎執(zhí)行階段進(jìn)行。
當(dāng)一段js代碼出現(xiàn)的時(shí)候,js引擎對整體代碼進(jìn)行編譯完成轉(zhuǎn)為機(jī)器代碼后便可進(jìn)行執(zhí)行,此時(shí)會為頂級代碼立刻創(chuàng)建一個(gè)全局上下文(頂級代碼一定不在任何函數(shù)內(nèi),只有函數(shù)外的代碼會被首先執(zhí)行,函數(shù)內(nèi)的只會在調(diào)用的時(shí)候執(zhí)行),創(chuàng)建完成后將其推入js引擎的堆棧當(dāng)中。接著進(jìn)入執(zhí)行階段,執(zhí)行可執(zhí)行代碼,該賦值賦值,遇到函數(shù),就創(chuàng)建一個(gè)函數(shù)執(zhí)行上下文,并往調(diào)用棧中壓入該函數(shù)的執(zhí)行上下文;反復(fù)循環(huán),到最后調(diào)用棧中只剩一個(gè)全局執(zhí)行上下文,除非你關(guān)閉瀏覽器,不然全局執(zhí)行上下文不會彈出。
根據(jù)第二點(diǎn),執(zhí)行上下文被分為兩個(gè)階段:
如果看視頻或者上下文的文章,我們了解到執(zhí)行上下文在創(chuàng)建階段所含的內(nèi)容不同,如下圖所示,目前的ES6執(zhí)行上下文也又發(fā)生了變化。但本質(zhì)還是一樣的
那詞法環(huán)境組件和變量環(huán)境組件是什么呢?
詞法環(huán)境組件、變量環(huán)境組件和詞法環(huán)境的區(qū)別是什么呢?
我的理解:本質(zhì)上這兩個(gè)是一個(gè)東西,只是由于執(zhí)行上下文它是一個(gè)JavaScript代碼的環(huán)境,記錄一些執(zhí)行所需要的信息,因此創(chuàng)建一個(gè)變量來存儲前面已經(jīng)有的詞法環(huán)境(作用域),這樣來保持運(yùn)行。相當(dāng)于在執(zhí)行上下文創(chuàng)建的時(shí)候,除了 this,像變量環(huán)境、詞法環(huán)境在編譯階段就已經(jīng)確定了,其中變量環(huán)境的變量 var、function 會進(jìn)行變量、函數(shù)提升,并初始化,而詞法環(huán)境中的變量雖然提升了,但不會被初始化;而兩者的 outer 則相同,它們都指向父作用域。這個(gè)時(shí)候就會確定我們的作用域鏈,變量提升也在編譯的時(shí)候完成了。
作用域是表示在哪里可以訪問到變量,其本質(zhì)是一套規(guī)則,而這個(gè)規(guī)則的底層遵循的就是詞法作用域模型,即在詞法分析時(shí)生成的作用域,詞法分析階段,也可以理解為代碼書寫階段,當(dāng)你把函數(shù)(塊級作用域同理)書寫到某個(gè)位置,不用執(zhí)行,它的作用域就已經(jīng)確定了。簡單來說,“詞法作用域”就是作用域的成因。
從語言的層面來說,作用域模型分兩種:
總結(jié)一下。詞法作用域和動態(tài)作用域最根本區(qū)別在于生成作用域的時(shí)機(jī):
JavaScript擁有詞法作用域,因此可以訪問到變量的規(guī)則是基于代碼中函數(shù)和塊被寫在哪里,也就是它是在編譯階段就確定了。
在JS中分為作用域分三種:
作用域鏈?zhǔn)前炎饔糜驅(qū)訉忧短祝?dāng)查找一個(gè)變量在內(nèi)部作用域中未找到,向上父作用域去查找,找不到接著找,一直找到全局作用域這樣的一個(gè)關(guān)系叫做作用域鏈。
變量提升的表象:使某些類型的變量在實(shí)際聲明之前就可以在代碼中訪問。“變量被提升到其作用域的頂部”。
其深層的原因:在執(zhí)行之前,掃描代碼以查找變量聲明,并在變量環(huán)境對象中為每個(gè)變量創(chuàng)建一個(gè)新屬性。這都是發(fā)生在執(zhí)行上下文的創(chuàng)建階段。
是否變量提升 | 初始值 | 作用域 | |
函數(shù)聲明 | ?是 | 實(shí)際函數(shù) | 嚴(yán)格模式下:塊級作用域;非嚴(yán)格模式:函數(shù)作用域 |
var聲明的變量 | ?是 | undefined | 函數(shù)作用域 |
let、const聲明的變量 | ?否 | <uninitialized>(未初始化)、TDZ(暫時(shí)性死區(qū)) | 塊級作用域 |
函數(shù)表達(dá)式、箭頭函數(shù) | 取決于使用var、const、let哪一個(gè)關(guān)鍵字 |
理論上let和const關(guān)鍵字也會變量提升,但是它的初始值為未初始化,因此沒有任何用處,可以說是這些聲明的變量被置于暫時(shí)性死區(qū)(TDZ),這樣使得我們在作用域范圍最初到聲明變量的位置之間是無法訪問變量的,因此在聲明之前訪問會進(jìn)行報(bào)錯(cuò),但在之后就可以正常訪問了。
為什么會有暫時(shí)性死區(qū)?
為什么會存在變量提升?
在說閉包背后原理之前,我們先看看閉包是什么。
閉包是內(nèi)層函數(shù)能夠訪問外層函數(shù)聲明的變量,并且內(nèi)層函數(shù)在全局環(huán)境下可訪問。
這個(gè)解釋其實(shí)僅僅對閉包的現(xiàn)象進(jìn)行解釋了一遍。
真正的實(shí)際閉包,它背后是可以訪問附加到函數(shù)身上的變量環(huán)境,這樣的一個(gè)關(guān)系被稱之為閉包。
任何函數(shù)都可以訪問到創(chuàng)建它的執(zhí)行上下文當(dāng)中的變量環(huán)境(詞法環(huán)境組件和變量環(huán)境組件),所有函數(shù)都有個(gè)[[Environment]] 的隱藏屬性,該屬性保存了對創(chuàng)建該函數(shù)的詞法環(huán)境的引用,[[Environment]] 引用在函數(shù)創(chuàng)建時(shí)被設(shè)置并永久保存。
許多大神對閉包的定義都是去描述上方這句話。
接下來我們看一下閉包的例子:
function foo() {
var a=1;
var b=2;
return function bar() {
console.log(a++);
};
}
var baz=foo();
baz();
代碼執(zhí)行至 function bar 時(shí),創(chuàng)建 bar 的詞法環(huán)境,它沒有變量,outer 指向父作用域 foo
調(diào)用函數(shù) baz(),創(chuàng)建 baz() 執(zhí)行上下文,并將其壓入調(diào)用棧中
如此,調(diào)用完 baz,因?yàn)?baz 一直存在全局詞法環(huán)境中,它的隱藏屬性[[Environment]] 一直引用著 foo 函數(shù)中的 a 變量(即使 foo 函數(shù)已經(jīng)被銷毀了)
當(dāng)再次調(diào)用 baz 時(shí),就會再往調(diào)用棧中壓入baz(),并生成一個(gè)新的 bar 的詞法環(huán)境,它的 outer 還是引用 baz.[[Environment]],即上圖中的 foo 詞法環(huán)境。
這里我們可以通過運(yùn)行這段閉包代碼,打印baz函數(shù),可以看見它含有的內(nèi)部屬性[[scope]]里含有閉包(closures),保存引用。查看閉包的一個(gè)優(yōu)先級大于作用域鏈
到此,執(zhí)行上下文、作用域、閉包三者的聯(lián)系串聯(lián)起來給大家講清楚,也分別介紹了三者是什么。
最后用簡短的一句話分別描述一下這三個(gè)是什么:
這是我根據(jù)自行閱讀文章和視頻總結(jié)出來的個(gè)人理解,如有不正確的地方歡迎大家進(jìn)行指正。
先要明白的是,javascript和python都是解釋型語言,它們的運(yùn)行是需要具體的runtime的。
在本文敘述中,假定:
例如,python調(diào)用js,python就是主語言,js是副語言
適用于:
(因?yàn)榕c我的項(xiàng)目需求不太符合,所以了解的不太多)
首先,該方法的前提是兩種語言都要有安裝好的runtime,且能通過命令行調(diào)用runtime運(yùn)行文件或一串字符腳本。例如,裝好cpython后我們可以通過 python a.py 來運(yùn)行python程序,裝好Node.js之后我們可以通過 node a.js 或者 node -e "some script" 等來運(yùn)行JS程序。
當(dāng)然,最簡單的情況下,如果我們只需要調(diào)用一次副語言,也沒有啥交互(或者最多只有一次交互),那直接找個(gè)方法調(diào)用CLI就OK了。把給副語言的輸入用stdin或者命令行參數(shù)傳遞,讀取命令的輸出當(dāng)作副語言的輸出。
例如,python可以用 subprocess.Popen , subprocess.call , subprocess.check_output 或者 os.system 之類的,Node.js可以用 child_process 里的方法, exec 或者 fork 之類的。 需要注意的是,如果需要引用其他包,Node.js需要注意在 node_modules 所在的目錄下運(yùn)行指令,python需要注意設(shè)置好PYTHONPATH環(huán)境變量。
# Need to set the working directory to the directory where `node_modules` resides if necessary
>>> import subprocess
>>> a, b=1, 2
>>> print(subprocess.check_output(["node", "-e", f"console.log({a}+{b})"]))
b'3\n'
>>> print(subprocess.check_output(["node", "-e", f"console.log({a}+{b})"]).decode('utf-8'))
3
// Need to set PYTHONPATH in advance if necessary
const a=1;
const b=2;
const { execSync }=require("child_process");
console.log(execSync(`python -c "print(${a}+${b})"`));
//<Buffer 33 0a>
console.log(execSync(`python -c "print(${a}+${b})"`).toString());
//3
//
如果有復(fù)雜的交互,要傳遞復(fù)雜的對象,有的倒還可以序列化,有的根本不能序列化,咋辦?
這基本要利用 進(jìn)程間通信(IPC) ,通常情況下是用 管道(Pipe) 。在 stdin , stdout 和 stderr 三者之中至少挑一個(gè)建立管道。
假設(shè)我用 stdin 從python向js傳數(shù)據(jù),用 stderr 接收數(shù)據(jù),模式大約會是這樣的:
(以下偽代碼僅為示意,沒有嚴(yán)格測試過,實(shí)際使用建議直接用庫)
如果是通過這種原理javascript調(diào)用python,方法也差不多,javascript方是Node.js的話,用的是 child_process 里的指令。
文將比較全面細(xì)致的梳理一下 CSS 動畫的方方面面,針對每個(gè)屬性用法的講解及進(jìn)階用法的示意,希望能成為一個(gè)比較好的從入門到進(jìn)階的教程。
首先,我們來簡單介紹一下 CSS 動畫。
最新版本的 CSS 動畫由規(guī)范 -- CSS Animations Level 1 定義。
CSS 動畫用于實(shí)現(xiàn)元素從一個(gè) CSS 樣式配置轉(zhuǎn)換到另一個(gè) CSS 樣式配置。
動畫包括兩個(gè)部分: 描述動畫的樣式規(guī)則和用于指定動畫開始、結(jié)束以及中間點(diǎn)樣式的關(guān)鍵幀。
簡單來說,看下面的例子:
div {
animation: change 3s;
}
@keyframes change {
0% {
color: #f00;
}
100% {
color: #000;
}
}
一個(gè) CSS 動畫一定要由上述兩部分組成。
接下來,我們簡單看看 CSS 動畫的語法。
創(chuàng)建動畫序列,需要使用 animation 屬性或其子屬性,該屬性允許配置動畫時(shí)間、時(shí)長以及其他動畫細(xì)節(jié),但該屬性不能配置動畫的實(shí)際表現(xiàn),動畫的實(shí)際表現(xiàn)是由 @keyframes 規(guī)則實(shí)現(xiàn)。
animation 的子屬性有:
其中,對于一個(gè)動畫:
上面已經(jīng)給了一個(gè)簡單的 DEMO, 就用上述的 DEMO,看看結(jié)果:
這就是一個(gè)最基本的 CSS 動畫,本文將從 animation 的各個(gè)子屬性入手,探究 CSS 動畫的方方面面。
整體而言,單個(gè)的 animation-name 和 animation-duration 沒有太多的技巧,非常好理解,放在一起。
首先介紹一下 animation-name,通過 animation-name,CSS 引擎將會找到對應(yīng)的 @keyframes 規(guī)則。
當(dāng)然,它和 CSS 規(guī)則命名一樣,也存在一些騷操作。譬如,他是支持 emoji 表情的,所以代碼中的 animation-name 命名也可以這樣寫:
div {
animation: 3s;
}
@keyframes {
0% {
color: #f00;
}
100% {
color: #000;
}
}
而 animation-duration 設(shè)置動畫一個(gè)周期的時(shí)長,上述 DEMO 中,就是設(shè)定動畫整體持續(xù) 3s,這個(gè)也非常好理解。
animation-delay 就比較有意思了,它可以設(shè)置動畫延時(shí),即從元素加載完成之后到動畫序列開始執(zhí)行的這段時(shí)間。
簡單的一個(gè) DEMO:
<div></div>
<div></div>
div {
width: 100px;
height: 100px;
background: #000;
animation-name: move;
animation-duration: 2s;
}
div:nth-child(2) {
animation-delay: 1s;
}
@keyframes move {
0% {
transform: translate(0);
}
100% {
transform: translate(200px);
}
}
比較下列兩個(gè)動畫,一個(gè)添加了 animation-delay,一個(gè)沒有,非常直觀:
上述第二個(gè) div,關(guān)于 animation 屬性,也可以簡寫為 animation: move 2s 1s,第一個(gè)時(shí)間值表示持續(xù)時(shí)間,第二個(gè)時(shí)間值表示延遲時(shí)間。
關(guān)于 animation-delay,最有意思的技巧在于,它可以是負(fù)數(shù)。也就是說,雖然屬性名是動畫延遲時(shí)間,但是運(yùn)用了負(fù)數(shù)之后,動畫可以提前進(jìn)行。
假設(shè)我們要實(shí)現(xiàn)這樣一個(gè) loading 動畫效果:
有幾種思路:
方案 2 的核心偽代碼如下:
.item:nth-child(1) {
animation: rotate 3s infinite linear;
}
.item:nth-child(2) {
animation: rotate 3s infinite 1s linear;
}
.item:nth-child(3) {
animation: rotate 3s infinite 2s linear;
}
但是,在動畫的前 2s,另外兩個(gè)元素是不會動的,只有 2s 過后,整個(gè)動畫才是我們想要的:
此時(shí),我們可以讓第 2、3 個(gè)元素的延遲時(shí)間,改為負(fù)值,這樣可以讓動畫延遲進(jìn)行 -1s、-2s,也就是提前進(jìn)行 1s、2s:
.item:nth-child(1) {
animation: rotate 3s infinite linear;
}
.item:nth-child(2) {
animation: rotate 3s infinite -1s linear;
}
.item:nth-child(3) {
animation: rotate 3s infinite -2s linear;
}
這樣,每個(gè)元素都無需等待,直接就是運(yùn)動狀態(tài)中的,并且元素間隔位置是我們想要的結(jié)果:
還有一個(gè)有意思的小技巧。
同一個(gè)動畫,我們利用一定范圍內(nèi)隨機(jī)的 animation-duration 和一定范圍內(nèi)隨機(jī)的 animation-delay,可以有效的構(gòu)建更為隨機(jī)的動畫效果,讓動畫更加的自然。
我在下述兩個(gè)純 CSS 動畫中,都使用了這樣的技巧:
以純 CSS 實(shí)現(xiàn)華為充電動畫為例子,簡單講解一下。
仔細(xì)觀察這一部分,上升的一個(gè)一個(gè)圓球,拋去這里的一些融合效果,只關(guān)注不斷上升的圓球,看著像是沒有什么規(guī)律可言:
我們來模擬一下,如果是使用 10 個(gè) animation-duration 和 animation-delay 都一致的圓的話,核心偽代碼:
<ul>
<li></li>
<!--共 10 個(gè)...-->
<li></li>
</ul>
ul {
display: flex;
flex-wrap: nowrap;
gap: 5px;
}
li {
background: #000;
animation: move 3s infinite 1s linear;
}
@keyframes move {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(0, -100px);
}
}
這樣,小球的運(yùn)動會是這樣的整齊劃一:
要讓小球的運(yùn)動顯得非常的隨機(jī),只需要讓 animation-duration 和 animation-delay 都在一定范圍內(nèi)浮動即可,改造下 CSS:
@for $i from 1 to 11 {
li:nth-child(#{$i}) {
animation-duration: #{random(2000)/1000 + 2}s;
animation-delay: #{random(1000)/1000 + 1}s;
}
}
我們利用 SASS 的循環(huán)和 random() 函數(shù),讓 animation-duration 在 2-4 秒范圍內(nèi)隨機(jī),讓 animation-delay 在 1-2 秒范圍內(nèi)隨機(jī),這樣,我們就可以得到非常自然且不同的上升動畫效果,基本不會出現(xiàn)重復(fù)的畫面,很好的模擬了隨機(jī)效果:
CodePen Demo -- 利用范圍隨機(jī) animation-duration 和 animation-delay 實(shí)現(xiàn)隨機(jī)動畫效果
緩動函數(shù)在動畫中非常重要,它定義了動畫在每一動畫周期中執(zhí)行的節(jié)奏。
緩動主要分為兩類:
首先先看看三次貝塞爾曲線緩動函數(shù)。在 CSS 中,支持一些緩動函數(shù)關(guān)鍵字。
/* Keyword values */
animation-timing-function: ease; // 動畫以低速開始,然后加快,在結(jié)束前變慢
animation-timing-function: ease-in; // 動畫以低速開始
animation-timing-function: ease-out; // 動畫以低速結(jié)束
animation-timing-function: ease-in-out; // 動畫以低速開始和結(jié)束
animation-timing-function: linear; // 勻速,動畫從頭到尾的速度是相同的
關(guān)于它們之間的效果對比:
除了 CSS 支持的這 5 個(gè)關(guān)鍵字,我們還可以使用 cubic-bezier() 方法自定義三次貝塞爾曲線:
animation-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1);
這里有個(gè)非常好用的網(wǎng)站 -- cubic-bezier 用于創(chuàng)建和調(diào)試生成不同的貝塞爾曲線參數(shù)。
關(guān)于緩動函數(shù)對動畫的影響,這里有一個(gè)非常好的示例。這里我們使用了純 CSS 實(shí)現(xiàn)了一個(gè)鐘的效果,對于其中的動畫的運(yùn)動,如果是 animation-timing-function: linear,效果如下:
而如果我們我把緩動函數(shù)替換一下,變成 animation-timing-function: cubic-bezier(1,-0.21,.85,1.29),它的曲線對應(yīng)如下:
整個(gè)鐘的動畫律動效果將變成這樣,完全不一樣的感覺:
CodePen Demo - 緩動不同效果不同
對于許多精益求精的動畫,在設(shè)計(jì)中其實(shí)都考慮到了緩動函數(shù)。我很久之前看到過一篇《基于物理學(xué)的動畫用戶體驗(yàn)設(shè)計(jì)》,可惜如今已經(jīng)無法找到原文。其中傳達(dá)出的一些概念是,動畫的設(shè)計(jì)依據(jù)實(shí)際在生活中的表現(xiàn)去考量。
譬如 linear 這個(gè)緩動,實(shí)際應(yīng)用于某些動畫中會顯得很不自然,因?yàn)橛捎诳諝庾枇Φ拇嬖冢绦蚰M的勻速直線運(yùn)動在現(xiàn)實(shí)生活中是很難實(shí)現(xiàn)的。因此對于這樣一個(gè)用戶平時(shí)很少感知到的運(yùn)動是很難建立信任感的。這樣的勻速直線運(yùn)動也是我們在進(jìn)行動效設(shè)計(jì)時(shí)需要極力避免的。
接下來再講講步驟緩動函數(shù)。在 CSS 的 animation-timing-function 中,它有如下幾種表現(xiàn)形態(tài):
{
/* Keyword values */
animation-timing-function: step-start;
animation-timing-function: step-end;
/* Function values */
animation-timing-function: steps(6, start)
animation-timing-function: steps(4, end);
}
在 CSS 中,使用步驟緩動函數(shù)最多的,就是利用其來實(shí)現(xiàn)逐幀動畫。假設(shè)我們有這樣一張圖(圖片大小為 1536 x 256,圖片來源于網(wǎng)絡(luò)):
可以發(fā)現(xiàn)它其實(shí)是一個(gè)人物行進(jìn)過程中的 6 種狀態(tài),或者可以為 6 幀,我們利用 animation-timing-function: steps(6) 可以將其用一個(gè) CSS 動畫串聯(lián)起來,代碼非常的簡單:
<div class="box"></div>
.box {
width: 256px;
height: 256px;
background: url('https://github.com/iamalperen/playground/blob/main/SpriteSheetAnimation/sprite.png?raw=true');
animation: sprite .6s steps(6, end) infinite;
}
@keyframes sprite {
0% {
background-position: 0 0;
}
100% {
background-position: -1536px 0;
}
}
簡單解釋一下上述代碼,首先要知道,剛好 256 x 6=1536,所以上述圖片其實(shí)可以剛好均分為 6 段:
將上述 1、2、3,3 個(gè)步驟畫在圖上簡單示意:
從上圖可知,其實(shí)在動畫過程中,background-position 的取值其實(shí)只有 background-position: 0 0,background-position: -256px 0,background-position: -512px 0 依次類推一直到 background-position: -1536px 0,由于背景的 repeat 的特性,其實(shí)剛好回到原點(diǎn),由此又重新開始新一輪同樣的動畫。
所以,整個(gè)動畫就會是這樣,每一幀停留 0.1s 后切換到下一幀(注意這里是個(gè)無限循環(huán)動畫),:
完整的代碼你可以戳這里 -- CodePen Demo -- Sprite Animation with steps()
在這里再插入一個(gè)小章節(jié),animation-duration 動畫長短對動畫的影響也是非常明顯的。
在上述代碼的基礎(chǔ)上,我們再修改 animation-duration,縮短每一幀的時(shí)間就可以讓步行的效果變成跑步的效果,同理,也可以增加每一幀的停留時(shí)間。讓每一步變得緩慢,就像是在步行一樣。
需要提出的是,上文說的每一幀,和瀏覽器渲染過程中的 FPS 的每一幀不是同一個(gè)概念。
看看效果,設(shè)置不同的 animation-duration 的效果(這里是 0.6s -> 0.2s),GIF 錄屏丟失了一些關(guān)鍵幀,實(shí)際效果會更好點(diǎn):
當(dāng)然,在 steps() 中,還有 steps(6, start) 和 steps(6, end) 的差異,也就是其中關(guān)鍵字 start 和 end 的差異。對于上述的無限動畫而言,其實(shí)基本是可以忽略不計(jì)的,它主要是控制動畫第一幀的開始和持續(xù)時(shí)長,比較小的一個(gè)知識點(diǎn)但是想講明白需要比較長的篇幅,限于本文的內(nèi)容,在這里不做展開,讀者可以自行了解。
上述的三次貝塞爾曲線緩動和步驟緩動,其實(shí)就是對應(yīng)的補(bǔ)間動畫和逐幀動畫。
對于同個(gè)動畫而言,有的時(shí)候兩種緩動都是適用的。我們在具體使用的時(shí)候需要具體分析選取。
假設(shè)我們用 CSS 實(shí)現(xiàn)了這樣一個(gè)圖形:
現(xiàn)在想利用這個(gè)圖形制作一個(gè) Loading 效果,如果利用補(bǔ)間動畫,也就是三次貝塞爾曲線緩動的話,讓它旋轉(zhuǎn)起來,得到的效果非常的一般:
.g-container{
animation: rotate 2s linear infinite;
}
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
動畫效果如下:
但是如果這里,我們將補(bǔ)間動畫換成逐幀動畫,因?yàn)橛?20 個(gè)點(diǎn),所以設(shè)置成 steps(20),再看看效果,會得到完全不一樣的感覺:
.g-container{
animation: rotate 2s steps(20) infinite;
}
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
動畫效果如下:
整個(gè) loading 的圈圈看上去好像也在旋轉(zhuǎn),實(shí)際上只是 20 幀關(guān)鍵幀在切換,整體的效果感覺更適合 Loading 的效果。
因此,兩種動畫效果都是很有必要掌握的,在實(shí)際使用的時(shí)候靈活嘗試,選擇更適合的。
上述 DEMO 效果完整的代碼:CodePen Demo -- Scale Loading steps vs linear
接下來,我們講講 animation-play-state,顧名思義,它可以控制動畫的狀態(tài) -- 運(yùn)行或者暫停。類似于視頻播放器的開始和暫停。是 CSS 動畫中有限的控制動畫狀態(tài)的手段之一。
它的取值只有兩個(gè)(默認(rèn)為 running):
{
animation-play-state: paused | running;
}
使用起來也非常簡單,看下面這個(gè)例子,我們在 hover 按鈕的時(shí)候,實(shí)現(xiàn)動畫的暫停:
<div class="btn stop">stop</div>
<div class="animation"></div>
.animation {
width: 100px;
height: 100px;
background: deeppink;
animation: move 2s linear infinite alternate;
}
@keyframes move {
100% {
transform: translate(100px, 0);
}
}
.stop:hover ~ .animation {
animation-play-state: paused;
}
一個(gè)簡單的 CSS 動畫,但是當(dāng)我們 hover 按鈕的時(shí)候,給動畫元素添加上 animation-play-state: paused:
正常而言,按照正常思路使用 animation-play-state: paused 是非常簡單的。
但是,如果我們想創(chuàng)造一些有意思的 CSS 動畫效果,不如反其道而行之。
我們都知道,正常情況下,動畫應(yīng)該是運(yùn)行狀態(tài),那如果我們將一些動畫的默認(rèn)狀態(tài)設(shè)置為暫停,只有當(dāng)鼠標(biāo)點(diǎn)擊或者 hover 的時(shí)候,才設(shè)置其 animation-play-state: running,這樣就可以得到很多有趣的 CSS 效果。
看個(gè)倒酒的例子,這是一個(gè)純 CSS 動畫,但是默認(rèn)狀態(tài)下,動畫處于 animation-play-state: paused,也就是暫停狀態(tài),只有當(dāng)鼠標(biāo)點(diǎn)擊杯子的時(shí),才設(shè)置 animation-play-state: running,讓酒倒下,利用 animation-play-state 實(shí)現(xiàn)了一個(gè)非常有意思的交互效果:
完整的 DEMO 你可以戳這里:CodePen Demo -- CSS Beer!
在非常多 Web 創(chuàng)意交互動畫我們都可以看到這個(gè)技巧的身影。
下一個(gè)屬性 animation-fill-mode,很多人會誤認(rèn)為它只是用于控制元素在動畫結(jié)束后是否復(fù)位。這個(gè)其實(shí)是不準(zhǔn)確的,不全面的。
看看它的取值:
{
// 默認(rèn)值,當(dāng)動畫未執(zhí)行時(shí),動畫將不會將任何樣式應(yīng)用于目標(biāo),而是使用賦予給該元素的 CSS 規(guī)則來顯示該元素的狀態(tài)
animation-fill-mode: none;
// 動畫將在應(yīng)用于目標(biāo)時(shí)立即應(yīng)用第一個(gè)關(guān)鍵幀中定義的值,并在 `animation-delay` 期間保留此值,
animation-fill-mode: backwards;
// 目標(biāo)將保留由執(zhí)行期間遇到的最后一個(gè)關(guān)鍵幀計(jì)算值。 最后一個(gè)關(guān)鍵幀取決于 `animation-direction` 和 `animation-iteration-count`
animation-fill-mode: forwards;
// 動畫將遵循 `forwards` 和 `backwards` 的規(guī)則,從而在兩個(gè)方向上擴(kuò)展動畫屬性
animation-fill-mode: both;
}
對于 animation-fill-mode 的解讀,我在 Segment Fault 上的一個(gè)問答中(SF - 如何理解 animation-fill-mode)看到了 4 副很好的解讀圖,這里借用一下:
假設(shè) HTML 如下:
<div class="box"></div>
CSS如下:
.box{
transform: translateY(0);
}
.box.on{
animation: move 1s;
}
@keyframes move{
from{transform: translateY(-50px)}
to {transform: translateY( 50px)}
}
使用圖片來表示 translateY 的值與 時(shí)間 的關(guān)系:
一句話總結(jié),元素在動畫時(shí)間之外,樣式只受到它的 CSS 規(guī)則限制,與 @keyframes 內(nèi)的關(guān)鍵幀定義無關(guān)。
一句話總結(jié),元素在動畫開始之前(包含未觸發(fā)動畫階段及 animation-delay 期間)的樣式為動畫運(yùn)行時(shí)的第一幀,而動畫結(jié)束后的樣式則恢復(fù)為 CSS 規(guī)則設(shè)定的樣式。
一句話總結(jié),元素在動畫開始之前的樣式為 CSS 規(guī)則設(shè)定的樣式,而動畫結(jié)束后的樣式則表現(xiàn)為由執(zhí)行期間遇到的最后一個(gè)關(guān)鍵幀計(jì)算值(也就是停在最后一幀)。
一句話總結(jié),綜合了 animation-fill-mode: backwards 和 animation-fill-mode: forwards 的設(shè)定。動畫開始前的樣式為動畫運(yùn)行時(shí)的第一幀,動畫結(jié)束后停在最后一幀。
講到了 animation-fill-mode,我們就可以順帶講講這個(gè)兩個(gè)比較好理解的屬性 -- animation-iteration-count 和 animation-direction
在上面講述 animation-fill-mode 時(shí),我使用了動畫運(yùn)行時(shí)的第一幀替代了@keyframes 中定義的第一幀這種說法,因?yàn)閯赢嬤\(yùn)行的第一幀和最后一幀的實(shí)際狀態(tài)還會受到動畫運(yùn)行方向 animation-direction 和 animation-iteration-count 的影響。
在 CSS 動畫中,由 animation-iteration-count 和 animation-direction 共同決定動畫運(yùn)行時(shí)的第一幀和最后一幀的狀態(tài)。
動畫的最后一幀,也就是動畫運(yùn)行的最終狀態(tài),并且我們可以利用 animation-fill-mode: forwards 讓動畫在結(jié)束后停留在這一幀,這個(gè)還是比較好理解的,但是 animation-fill-mode: backwards 和 animation-direction 的關(guān)系很容易弄不清楚,這里簡答講解下。
設(shè)置一個(gè) 100px x 100px 的滑塊,在一個(gè) 400px x 100px 的容器中,其代碼如下:
<div class="g-father">
<div class="g-box"></div>
</div>
.g-father {
width: 400px;
height: 100px;
border: 1px solid #000;
}
.g-box {
width: 100px;
height: 100px;
background: #333;
}
表現(xiàn)如下:
那么,加入 animation 之后,在不同的 animation-iteration-count 和 animation-direction 作用下,動畫的初始和結(jié)束狀態(tài)都不一樣。
如果設(shè)置了 animation-fill-mode: backwards,則元素在動畫未開始前的狀態(tài)由 animation-direction 決定:
.g-box {
...
animation: move 4s linear;
animation-play-state: paused;
transform: translate(0, 0);
}
@keyframes move {
0% {
transform: translate(100px, 0);
}
100% {
transform: translate(300px, 0);
}
}
注意這里 CSS 規(guī)則中,元素沒有設(shè)置位移 transform: translate(0, 0),而在動畫中,第一個(gè)關(guān)鍵幀和最后一個(gè)關(guān)鍵的 translateX 分別是 100px、300px,配合不同的 animation-direction 初始狀態(tài)如下。
下圖假設(shè)我們設(shè)置了動畫默認(rèn)是暫停的 -- animation-play-state: paused,那么動畫在開始前的狀態(tài)為:
講完了每一個(gè)屬性,我們再來看看一些動畫使用過程中的細(xì)節(jié)。
看這樣一個(gè)動畫:
<div></div>
div {
width: 100px;
height: 100px;
background: #000;
animation: combine 2s;
}
@keyframes combine {
100% {
transform: translate(0, 150px);
opacity: 0;
}
}
這里我們實(shí)現(xiàn)了一個(gè) div 塊下落動畫,下落的同時(shí)產(chǎn)生透明度的變化:
對于這樣一個(gè)多個(gè)屬性變化的動畫,它其實(shí)等價(jià)于:
div {
animation: falldown 2s, fadeIn 2s;
}
@keyframes falldown {
100% {
transform: translate(0, 150px);
}
}
@keyframes fadeIn {
100% {
opacity: 0;
}
}
在 CSS 動畫規(guī)則中,animation 是可以接收多個(gè)動畫的,這樣做的目的不僅僅只是為了復(fù)用,同時(shí)也是為了分治,我們對每一個(gè)屬性層面的動畫能夠有著更為精確的控制。
我們經(jīng)常能夠在各種不同的 CSS 代碼見到如下兩種 CSS @keyframes 的設(shè)定:
@keyframes fadeIn {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes fadeIn {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
在 CSS 動畫 @keyframes 的定義中,from 等同于 0%,而 to 等同于 100%。
當(dāng)然,當(dāng)我們的關(guān)鍵幀不止 2 幀的時(shí),更推薦使用百分比定義的方式。
除此之外,當(dāng)動畫的起始幀等同于 CSS 規(guī)則中賦予的值并且沒有設(shè)定 animation-fill-mode,0% 和 from 這一幀是可以刪除的。
我曾經(jīng)在這篇文章中 -- 深入理解 CSS(Cascading Style Sheets)中的層疊(Cascading) 講過一個(gè)很有意思的 CSS 現(xiàn)象。
這也是很多人對 CSS 優(yōu)先級的一個(gè)認(rèn)知誤區(qū),在 CSS 中,優(yōu)先級還需要考慮選擇器的層疊(級聯(lián))順序。
只有在層疊順序相等時(shí),使用哪個(gè)值才取決于樣式的優(yōu)先級。
那什么是層疊順序呢?
根據(jù) CSS Cascading 4 最新標(biāo)準(zhǔn):
CSS Cascading and Inheritance Level 5(Current Work)
定義的當(dāng)前規(guī)范下申明的層疊順序優(yōu)先級如下(越往下的優(yōu)先級越高,下面的規(guī)則按升序排列):
簡單翻譯一下:
按照上述算法,大概是這樣:
過渡動畫過程中每一幀的樣式 > 用戶代理、用戶、頁面作者設(shè)置的!important樣式 > 動畫過程中每一幀的樣式優(yōu)先級 > 頁面作者、用戶、用戶代理普通樣式。
然而,經(jīng)過多個(gè)瀏覽器的測試,實(shí)際上并不是這樣。(尷尬了)
舉個(gè)例子,我們可以通過這個(gè)特性,覆蓋掉行內(nèi)樣式中的 !important 樣式:
<p class="txt" style="color:red!important">123456789</p>
.txt {
animation: colorGreen 2s infinite;
}
@keyframes colorGreen {
0%,
100% {
color: green;
}
}
在 Safari 瀏覽器下,上述 DEMO 文本的顏色為綠色,也就是說,處于動畫狀態(tài)中的樣式,能夠覆蓋掉行內(nèi)樣式中的 !important 樣式,屬于最最高優(yōu)先級的一種樣式,我們可以通過無限動畫、或者 animation-fill-mode: forwards,利用這個(gè)技巧,覆蓋掉本來應(yīng)該是優(yōu)先級非常非常高的行內(nèi)樣式中的 !important 樣式。
我在早兩年的 Chrome 中也能得到同樣的結(jié)果,但是到今天(2022-01-10),最新版的 Chrome 已經(jīng)不支持動畫過程中關(guān)鍵幀樣式優(yōu)先級覆蓋行內(nèi)樣式 !important 的特性。
對于不同瀏覽器,感興趣的同學(xué)可以利用我這個(gè) DEMO 自行嘗試,CodePen Demo - the priority of CSS Animation
這也是非常多人非常關(guān)心的一個(gè)重點(diǎn)。
我的 CSS 動畫很卡,我應(yīng)該如何去優(yōu)化它?
CSS 動畫很卡,其實(shí)是一個(gè)現(xiàn)象描述,它的本質(zhì)其實(shí)是在動畫過程中,瀏覽器刷新渲染頁面的幀率過低。通常而言,目前大多數(shù)瀏覽器刷新率為 60 次/秒,所以通常來講 FPS 為 60 frame/s 時(shí)動畫效果較好,也就是每幀的消耗時(shí)間為 16.67ms。
頁面處于動畫變化時(shí),當(dāng)幀率低于一定數(shù)值時(shí),我們就感覺到頁面的卡頓。
而造成幀率低的原因就是瀏覽器在一幀之間處理的事情太多了,超過了 16.67ms,要優(yōu)化每一幀的時(shí)間,又需要完整地知道瀏覽器在每一幀干了什么,這個(gè)就又涉及到了老生常談的瀏覽器渲染頁面。
到今天,雖然不同瀏覽器的渲染過程不完全相同,但是基本上大同小異,基本上都是:
簡化一下也就是這個(gè)圖:
這兩張圖,你可以在非常多不同的文章中看到。
回歸本文的重點(diǎn),Web 動畫很大一部分開銷在于層的重繪,以層為基礎(chǔ)的復(fù)合模型對渲染性能有著深遠(yuǎn)的影響。當(dāng)不需要繪制時(shí),復(fù)合操作的開銷可以忽略不計(jì),因此在試著調(diào)試渲染性能問題時(shí),首要目標(biāo)就是要避免層的重繪。那么這就給動畫的性能優(yōu)化提供了方向,減少元素的重繪與回流。
這其中,如何減少頁面的回流與重繪呢,這里就會運(yùn)用到我們常說的** GPU 加速**。
GPU 加速的本質(zhì)其實(shí)是減少瀏覽器渲染頁面每一幀過程中的 reflow 和 repaint,其根本,就是讓需要進(jìn)行動畫的元素,生成自己的 GraphicsLayer。
瀏覽器渲染一個(gè)頁面時(shí),它使用了許多沒有暴露給開發(fā)者的中間表現(xiàn)形式,其中最重要的結(jié)構(gòu)便是層(layer)。
在 Chrome 中,存在有不同類型的層: RenderLayer(負(fù)責(zé) DOM 子樹),GraphicsLayer(負(fù)責(zé) RenderLayer 的子樹)。
GraphicsLayer ,它對于我們的 Web 動畫而言非常重要,通常,Chrome 會將一個(gè)層的內(nèi)容在作為紋理上傳到 GPU 前先繪制(paint)進(jìn)一個(gè)位圖中。如果內(nèi)容不會改變,那么就沒有必要重繪(repaint)層。
而當(dāng)元素生成了自己的 GraphicsLayer 之后,在動畫過程中,Chrome 并不會始終重繪整個(gè)層,它會嘗試智能地去重繪 DOM 中失效的部分,也就是發(fā)生動畫的部分,在 Composite 之前,頁面是處于一種分層狀態(tài),借助 GPU,瀏覽器僅僅在每一幀對生成了自己獨(dú)立 GraphicsLayer 元素層進(jìn)行重繪,如此,大大的降低了整個(gè)頁面重排重繪的開銷,提升了頁面渲染的效率。
因此,CSS 動畫(Web 動畫同理)優(yōu)化的第一條準(zhǔn)則就是讓需要動畫的元素生成了自己獨(dú)立的 GraphicsLayer,強(qiáng)制開始 GPU 加速,而我們需要知道是,GPU 加速的本質(zhì)是利用讓元素生成了自己獨(dú)立的 GraphicsLayer,降低了頁面在渲染過程中重繪重排的開銷。
當(dāng)然,生成自己的獨(dú)立的 GraphicsLayer,不僅僅只有 transform3d api,還有非常多的方式。對于上述一大段非常繞的內(nèi)容,你可以再看看這幾篇文章:
除了上述準(zhǔn)則之外,還有一些提升 CSS 動畫性能的建議:
不同樣式在消耗性能方面是不同的,改變一些屬性的開銷比改變其他屬性要多,因此更可能使動畫卡頓。
例如,與改變元素的文本顏色相比,改變元素的 box-shadow 將需要開銷大很多的繪圖操作。box-shadow 屬性,從渲染角度來講十分耗性能,原因就是與其他樣式相比,它們的繪制代碼執(zhí)行時(shí)間過長。這就是說,如果一個(gè)耗性能嚴(yán)重的樣式經(jīng)常需要重繪,那么你就會遇到性能問題。
類似的還有 CSS 3D 變換、mix-blend-mode、filter,這些樣式相比其他一些簡單的操作,會更加的消耗性能。我們應(yīng)該盡可能的在動畫過程中降低其使用的頻率或者尋找替代方案。
當(dāng)然,沒有不變的事情,在今天性能很差的樣式,可能明天就被優(yōu)化,并且瀏覽器之間也存在差異。
因此關(guān)鍵在于,我們需要針對每一起卡頓的例子,借助開發(fā)工具來分辨出性能瓶頸所在,然后設(shè)法減少瀏覽器的工作量。學(xué)會 Chrome 開發(fā)者工具的 Performance 面板及其他渲染相關(guān)的面板非常重要,當(dāng)然這不是本文的重點(diǎn)。大家可以自行探索。
will-change 為 Web 開發(fā)者提供了一種告知瀏覽器該元素會有哪些變化的方法,這樣瀏覽器可以在元素屬性真正發(fā)生變化之前提前做好對應(yīng)的優(yōu)化準(zhǔn)備工作。 這種優(yōu)化可以將一部分復(fù)雜的計(jì)算工作提前準(zhǔn)備好,使頁面的反應(yīng)更為快速靈敏。
值得注意的是,用好這個(gè)屬性并不是很容易:
有人說 will-change 是良藥,也有人說是毒藥,在具體使用的時(shí)候,可以多測試一下。
好了,本文從多個(gè)方面,由淺入深地描述了 CSS 動畫我認(rèn)為的一些比較重要、值得一講、需要注意的點(diǎn)。當(dāng)然很多地方點(diǎn)到即止,或者限于篇幅沒有完全展開,很多細(xì)節(jié)還需要讀者進(jìn)一步閱讀規(guī)范或者自行嘗試驗(yàn)證,實(shí)踐出真知,紙上得來終覺淺。
OK,本文到此結(jié)束,希望本文對你有所幫助 :)
*請認(rèn)真填寫需求信息,我們會在24小時(shí)內(nèi)與您取得聯(lián)系。