理清执行上下文、作用域、闭包这三个关系之前,我们需要先理解一些概念。
我们知道所学的高级语言分为两类:1、编译型 2、解释型
编译型(compilation):将整个代码立刻转化为机器代码,然后写下机器代码转换为可在任何计算机上执行的可移植文件,然后在进行执行。
解释:解释器贯穿整个源代码进行一行一行的读取执行,但是还是会转为机器代码,但是发生在执行前,而不是提前到和编译一样,整体进行转换为机器代码。
对于Javascript,我们了解到的是它是一个解释型语言,但是随着后续的发展,常用于web或者服务端的开发,性能难以跟上。因此现代JavaScript是实时编译(JIT),将汇编和解释混合在一起,对整个源文件代码进行转化为机器代码,然后立刻执行,这样速度就更快些,省去了可移植文件,节省了时间。
现代JavaScript在V8引擎的工作原理:
因此JavaScript代码的整个执行过程,分为两个阶段:
明白这些之后,我们继续说执行上下文和作用域。
执行上下文是一段JavaScript代码执行的环境,包含了所有必要的信息以供其执行。
上面画过一张V8引擎运行图,当时所讲的词法环境是在 编译阶段产生,现在讲的执行上下文是在引擎执行阶段进行。
当一段js代码出现的时候,js引擎对整体代码进行编译完成转为机器代码后便可进行执行,此时会为顶级代码立刻创建一个全局上下文(顶级代码一定不在任何函数内,只有函数外的代码会被首先执行,函数内的只会在调用的时候执行),创建完成后将其推入js引擎的堆栈当中。接着进入执行阶段,执行可执行代码,该赋值赋值,遇到函数,就创建一个函数执行上下文,并往调用栈中压入该函数的执行上下文;反复循环,到最后调用栈中只剩一个全局执行上下文,除非你关闭浏览器,不然全局执行上下文不会弹出。
根据第二点,执行上下文被分为两个阶段:
如果看视频或者上下文的文章,我们了解到执行上下文在创建阶段所含的内容不同,如下图所示,目前的ES6执行上下文也又发生了变化。但本质还是一样的
那词法环境组件和变量环境组件是什么呢?
词法环境组件、变量环境组件和词法环境的区别是什么呢?
我的理解:本质上这两个是一个东西,只是由于执行上下文它是一个JavaScript代码的环境,记录一些执行所需要的信息,因此创建一个变量来存储前面已经有的词法环境(作用域),这样来保持运行。相当于在执行上下文创建的时候,除了 this,像变量环境、词法环境在编译阶段就已经确定了,其中变量环境的变量 var、function 会进行变量、函数提升,并初始化,而词法环境中的变量虽然提升了,但不会被初始化;而两者的 outer 则相同,它们都指向父作用域。这个时候就会确定我们的作用域链,变量提升也在编译的时候完成了。
作用域是表示在哪里可以访问到变量,其本质是一套规则,而这个规则的底层遵循的就是词法作用域模型,即在词法分析时生成的作用域,词法分析阶段,也可以理解为代码书写阶段,当你把函数(块级作用域同理)书写到某个位置,不用执行,它的作用域就已经确定了。简单来说,“词法作用域”就是作用域的成因。
从语言的层面来说,作用域模型分两种:
总结一下。词法作用域和动态作用域最根本区别在于生成作用域的时机:
JavaScript拥有词法作用域,因此可以访问到变量的规则是基于代码中函数和块被写在哪里,也就是它是在编译阶段就确定了。
在JS中分为作用域分三种:
作用域链是把作用域层层嵌套,当查找一个变量在内部作用域中未找到,向上父作用域去查找,找不到接着找,一直找到全局作用域这样的一个关系叫做作用域链。
变量提升的表象:使某些类型的变量在实际声明之前就可以在代码中访问。“变量被提升到其作用域的顶部”。
其深层的原因:在执行之前,扫描代码以查找变量声明,并在变量环境对象中为每个变量创建一个新属性。这都是发生在执行上下文的创建阶段。
是否变量提升 | 初始值 | 作用域 | |
函数声明 | ?是 | 实际函数 | 严格模式下:块级作用域;非严格模式:函数作用域 |
var声明的变量 | ?是 | undefined | 函数作用域 |
let、const声明的变量 | ?否 | <uninitialized>(未初始化)、TDZ(暂时性死区) | 块级作用域 |
函数表达式、箭头函数 | 取决于使用var、const、let哪一个关键字 |
理论上let和const关键字也会变量提升,但是它的初始值为未初始化,因此没有任何用处,可以说是这些声明的变量被置于暂时性死区(TDZ),这样使得我们在作用域范围最初到声明变量的位置之间是无法访问变量的,因此在声明之前访问会进行报错,但在之后就可以正常访问了。
为什么会有暂时性死区?
为什么会存在变量提升?
在说闭包背后原理之前,我们先看看闭包是什么。
闭包是内层函数能够访问外层函数声明的变量,并且内层函数在全局环境下可访问。
这个解释其实仅仅对闭包的现象进行解释了一遍。
真正的实际闭包,它背后是可以访问附加到函数身上的变量环境,这样的一个关系被称之为闭包。
任何函数都可以访问到创建它的执行上下文当中的变量环境(词法环境组件和变量环境组件),所有函数都有个[[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用,[[Environment]] 引用在函数创建时被设置并永久保存。
许多大神对闭包的定义都是去描述上方这句话。
接下来我们看一下闭包的例子:
function foo() {
var a=1;
var b=2;
return function bar() {
console.log(a++);
};
}
var baz=foo();
baz();
代码执行至 function bar 时,创建 bar 的词法环境,它没有变量,outer 指向父作用域 foo
调用函数 baz(),创建 baz() 执行上下文,并将其压入调用栈中
如此,调用完 baz,因为 baz 一直存在全局词法环境中,它的隐藏属性[[Environment]] 一直引用着 foo 函数中的 a 变量(即使 foo 函数已经被销毁了)
当再次调用 baz 时,就会再往调用栈中压入baz(),并生成一个新的 bar 的词法环境,它的 outer 还是引用 baz.[[Environment]],即上图中的 foo 词法环境。
这里我们可以通过运行这段闭包代码,打印baz函数,可以看见它含有的内部属性[[scope]]里含有闭包(closures),保存引用。查看闭包的一个优先级大于作用域链
到此,执行上下文、作用域、闭包三者的联系串联起来给大家讲清楚,也分别介绍了三者是什么。
最后用简短的一句话分别描述一下这三个是什么:
这是我根据自行阅读文章和视频总结出来的个人理解,如有不正确的地方欢迎大家进行指正。
先要明白的是,javascript和python都是解释型语言,它们的运行是需要具体的runtime的。
在本文叙述中,假定:
例如,python调用js,python就是主语言,js是副语言
适用于:
(因为与我的项目需求不太符合,所以了解的不太多)
首先,该方法的前提是两种语言都要有安装好的runtime,且能通过命令行调用runtime运行文件或一串字符脚本。例如,装好cpython后我们可以通过 python a.py 来运行python程序,装好Node.js之后我们可以通过 node a.js 或者 node -e "some script" 等来运行JS程序。
当然,最简单的情况下,如果我们只需要调用一次副语言,也没有啥交互(或者最多只有一次交互),那直接找个方法调用CLI就OK了。把给副语言的输入用stdin或者命令行参数传递,读取命令的输出当作副语言的输出。
例如,python可以用 subprocess.Popen , subprocess.call , subprocess.check_output 或者 os.system 之类的,Node.js可以用 child_process 里的方法, exec 或者 fork 之类的。 需要注意的是,如果需要引用其他包,Node.js需要注意在 node_modules 所在的目录下运行指令,python需要注意设置好PYTHONPATH环境变量。
# 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
//
如果有复杂的交互,要传递复杂的对象,有的倒还可以序列化,有的根本不能序列化,咋办?
这基本要利用 进程间通信(IPC) ,通常情况下是用 管道(Pipe) 。在 stdin , stdout 和 stderr 三者之中至少挑一个建立管道。
假设我用 stdin 从python向js传数据,用 stderr 接收数据,模式大约会是这样的:
(以下伪代码仅为示意,没有严格测试过,实际使用建议直接用库)
如果是通过这种原理javascript调用python,方法也差不多,javascript方是Node.js的话,用的是 child_process 里的指令。
文将比较全面细致的梳理一下 CSS 动画的方方面面,针对每个属性用法的讲解及进阶用法的示意,希望能成为一个比较好的从入门到进阶的教程。
首先,我们来简单介绍一下 CSS 动画。
最新版本的 CSS 动画由规范 -- CSS Animations Level 1 定义。
CSS 动画用于实现元素从一个 CSS 样式配置转换到另一个 CSS 样式配置。
动画包括两个部分: 描述动画的样式规则和用于指定动画开始、结束以及中间点样式的关键帧。
简单来说,看下面的例子:
div {
animation: change 3s;
}
@keyframes change {
0% {
color: #f00;
}
100% {
color: #000;
}
}
一个 CSS 动画一定要由上述两部分组成。
接下来,我们简单看看 CSS 动画的语法。
创建动画序列,需要使用 animation 属性或其子属性,该属性允许配置动画时间、时长以及其他动画细节,但该属性不能配置动画的实际表现,动画的实际表现是由 @keyframes 规则实现。
animation 的子属性有:
其中,对于一个动画:
上面已经给了一个简单的 DEMO, 就用上述的 DEMO,看看结果:
这就是一个最基本的 CSS 动画,本文将从 animation 的各个子属性入手,探究 CSS 动画的方方面面。
整体而言,单个的 animation-name 和 animation-duration 没有太多的技巧,非常好理解,放在一起。
首先介绍一下 animation-name,通过 animation-name,CSS 引擎将会找到对应的 @keyframes 规则。
当然,它和 CSS 规则命名一样,也存在一些骚操作。譬如,他是支持 emoji 表情的,所以代码中的 animation-name 命名也可以这样写:
div {
animation: 3s;
}
@keyframes {
0% {
color: #f00;
}
100% {
color: #000;
}
}
而 animation-duration 设置动画一个周期的时长,上述 DEMO 中,就是设定动画整体持续 3s,这个也非常好理解。
animation-delay 就比较有意思了,它可以设置动画延时,即从元素加载完成之后到动画序列开始执行的这段时间。
简单的一个 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);
}
}
比较下列两个动画,一个添加了 animation-delay,一个没有,非常直观:
上述第二个 div,关于 animation 属性,也可以简写为 animation: move 2s 1s,第一个时间值表示持续时间,第二个时间值表示延迟时间。
关于 animation-delay,最有意思的技巧在于,它可以是负数。也就是说,虽然属性名是动画延迟时间,但是运用了负数之后,动画可以提前进行。
假设我们要实现这样一个 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,另外两个元素是不会动的,只有 2s 过后,整个动画才是我们想要的:
此时,我们可以让第 2、3 个元素的延迟时间,改为负值,这样可以让动画延迟进行 -1s、-2s,也就是提前进行 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;
}
这样,每个元素都无需等待,直接就是运动状态中的,并且元素间隔位置是我们想要的结果:
还有一个有意思的小技巧。
同一个动画,我们利用一定范围内随机的 animation-duration 和一定范围内随机的 animation-delay,可以有效的构建更为随机的动画效果,让动画更加的自然。
我在下述两个纯 CSS 动画中,都使用了这样的技巧:
以纯 CSS 实现华为充电动画为例子,简单讲解一下。
仔细观察这一部分,上升的一个一个圆球,抛去这里的一些融合效果,只关注不断上升的圆球,看着像是没有什么规律可言:
我们来模拟一下,如果是使用 10 个 animation-duration 和 animation-delay 都一致的圆的话,核心伪代码:
<ul>
<li></li>
<!--共 10 个...-->
<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);
}
}
这样,小球的运动会是这样的整齐划一:
要让小球的运动显得非常的随机,只需要让 animation-duration 和 animation-delay 都在一定范围内浮动即可,改造下 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 的循环和 random() 函数,让 animation-duration 在 2-4 秒范围内随机,让 animation-delay 在 1-2 秒范围内随机,这样,我们就可以得到非常自然且不同的上升动画效果,基本不会出现重复的画面,很好的模拟了随机效果:
CodePen Demo -- 利用范围随机 animation-duration 和 animation-delay 实现随机动画效果
缓动函数在动画中非常重要,它定义了动画在每一动画周期中执行的节奏。
缓动主要分为两类:
首先先看看三次贝塞尔曲线缓动函数。在 CSS 中,支持一些缓动函数关键字。
/* Keyword values */
animation-timing-function: ease; // 动画以低速开始,然后加快,在结束前变慢
animation-timing-function: ease-in; // 动画以低速开始
animation-timing-function: ease-out; // 动画以低速结束
animation-timing-function: ease-in-out; // 动画以低速开始和结束
animation-timing-function: linear; // 匀速,动画从头到尾的速度是相同的
关于它们之间的效果对比:
除了 CSS 支持的这 5 个关键字,我们还可以使用 cubic-bezier() 方法自定义三次贝塞尔曲线:
animation-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1);
这里有个非常好用的网站 -- cubic-bezier 用于创建和调试生成不同的贝塞尔曲线参数。
关于缓动函数对动画的影响,这里有一个非常好的示例。这里我们使用了纯 CSS 实现了一个钟的效果,对于其中的动画的运动,如果是 animation-timing-function: linear,效果如下:
而如果我们我把缓动函数替换一下,变成 animation-timing-function: cubic-bezier(1,-0.21,.85,1.29),它的曲线对应如下:
整个钟的动画律动效果将变成这样,完全不一样的感觉:
CodePen Demo - 缓动不同效果不同
对于许多精益求精的动画,在设计中其实都考虑到了缓动函数。我很久之前看到过一篇《基于物理学的动画用户体验设计》,可惜如今已经无法找到原文。其中传达出的一些概念是,动画的设计依据实际在生活中的表现去考量。
譬如 linear 这个缓动,实际应用于某些动画中会显得很不自然,因为由于空气阻力的存在,程序模拟的匀速直线运动在现实生活中是很难实现的。因此对于这样一个用户平时很少感知到的运动是很难建立信任感的。这样的匀速直线运动也是我们在进行动效设计时需要极力避免的。
接下来再讲讲步骤缓动函数。在 CSS 的 animation-timing-function 中,它有如下几种表现形态:
{
/* 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 中,使用步骤缓动函数最多的,就是利用其来实现逐帧动画。假设我们有这样一张图(图片大小为 1536 x 256,图片来源于网络):
可以发现它其实是一个人物行进过程中的 6 种状态,或者可以为 6 帧,我们利用 animation-timing-function: steps(6) 可以将其用一个 CSS 动画串联起来,代码非常的简单:
<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,所以上述图片其实可以刚好均分为 6 段:
将上述 1、2、3,3 个步骤画在图上简单示意:
从上图可知,其实在动画过程中,background-position 的取值其实只有 background-position: 0 0,background-position: -256px 0,background-position: -512px 0 依次类推一直到 background-position: -1536px 0,由于背景的 repeat 的特性,其实刚好回到原点,由此又重新开始新一轮同样的动画。
所以,整个动画就会是这样,每一帧停留 0.1s 后切换到下一帧(注意这里是个无限循环动画),:
完整的代码你可以戳这里 -- CodePen Demo -- Sprite Animation with steps()
在这里再插入一个小章节,animation-duration 动画长短对动画的影响也是非常明显的。
在上述代码的基础上,我们再修改 animation-duration,缩短每一帧的时间就可以让步行的效果变成跑步的效果,同理,也可以增加每一帧的停留时间。让每一步变得缓慢,就像是在步行一样。
需要提出的是,上文说的每一帧,和浏览器渲染过程中的 FPS 的每一帧不是同一个概念。
看看效果,设置不同的 animation-duration 的效果(这里是 0.6s -> 0.2s),GIF 录屏丢失了一些关键帧,实际效果会更好点:
当然,在 steps() 中,还有 steps(6, start) 和 steps(6, end) 的差异,也就是其中关键字 start 和 end 的差异。对于上述的无限动画而言,其实基本是可以忽略不计的,它主要是控制动画第一帧的开始和持续时长,比较小的一个知识点但是想讲明白需要比较长的篇幅,限于本文的内容,在这里不做展开,读者可以自行了解。
上述的三次贝塞尔曲线缓动和步骤缓动,其实就是对应的补间动画和逐帧动画。
对于同个动画而言,有的时候两种缓动都是适用的。我们在具体使用的时候需要具体分析选取。
假设我们用 CSS 实现了这样一个图形:
现在想利用这个图形制作一个 Loading 效果,如果利用补间动画,也就是三次贝塞尔曲线缓动的话,让它旋转起来,得到的效果非常的一般:
.g-container{
animation: rotate 2s linear infinite;
}
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
动画效果如下:
但是如果这里,我们将补间动画换成逐帧动画,因为有 20 个点,所以设置成 steps(20),再看看效果,会得到完全不一样的感觉:
.g-container{
animation: rotate 2s steps(20) infinite;
}
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
动画效果如下:
整个 loading 的圈圈看上去好像也在旋转,实际上只是 20 帧关键帧在切换,整体的效果感觉更适合 Loading 的效果。
因此,两种动画效果都是很有必要掌握的,在实际使用的时候灵活尝试,选择更适合的。
上述 DEMO 效果完整的代码:CodePen Demo -- Scale Loading steps vs linear
接下来,我们讲讲 animation-play-state,顾名思义,它可以控制动画的状态 -- 运行或者暂停。类似于视频播放器的开始和暂停。是 CSS 动画中有限的控制动画状态的手段之一。
它的取值只有两个(默认为 running):
{
animation-play-state: paused | running;
}
使用起来也非常简单,看下面这个例子,我们在 hover 按钮的时候,实现动画的暂停:
<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;
}
一个简单的 CSS 动画,但是当我们 hover 按钮的时候,给动画元素添加上 animation-play-state: paused:
正常而言,按照正常思路使用 animation-play-state: paused 是非常简单的。
但是,如果我们想创造一些有意思的 CSS 动画效果,不如反其道而行之。
我们都知道,正常情况下,动画应该是运行状态,那如果我们将一些动画的默认状态设置为暂停,只有当鼠标点击或者 hover 的时候,才设置其 animation-play-state: running,这样就可以得到很多有趣的 CSS 效果。
看个倒酒的例子,这是一个纯 CSS 动画,但是默认状态下,动画处于 animation-play-state: paused,也就是暂停状态,只有当鼠标点击杯子的时,才设置 animation-play-state: running,让酒倒下,利用 animation-play-state 实现了一个非常有意思的交互效果:
完整的 DEMO 你可以戳这里:CodePen Demo -- CSS Beer!
在非常多 Web 创意交互动画我们都可以看到这个技巧的身影。
下一个属性 animation-fill-mode,很多人会误认为它只是用于控制元素在动画结束后是否复位。这个其实是不准确的,不全面的。
看看它的取值:
{
// 默认值,当动画未执行时,动画将不会将任何样式应用于目标,而是使用赋予给该元素的 CSS 规则来显示该元素的状态
animation-fill-mode: none;
// 动画将在应用于目标时立即应用第一个关键帧中定义的值,并在 `animation-delay` 期间保留此值,
animation-fill-mode: backwards;
// 目标将保留由执行期间遇到的最后一个关键帧计算值。 最后一个关键帧取决于 `animation-direction` 和 `animation-iteration-count`
animation-fill-mode: forwards;
// 动画将遵循 `forwards` 和 `backwards` 的规则,从而在两个方向上扩展动画属性
animation-fill-mode: both;
}
对于 animation-fill-mode 的解读,我在 Segment Fault 上的一个问答中(SF - 如何理解 animation-fill-mode)看到了 4 副很好的解读图,这里借用一下:
假设 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 的值与 时间 的关系:
一句话总结,元素在动画时间之外,样式只受到它的 CSS 规则限制,与 @keyframes 内的关键帧定义无关。
一句话总结,元素在动画开始之前(包含未触发动画阶段及 animation-delay 期间)的样式为动画运行时的第一帧,而动画结束后的样式则恢复为 CSS 规则设定的样式。
一句话总结,元素在动画开始之前的样式为 CSS 规则设定的样式,而动画结束后的样式则表现为由执行期间遇到的最后一个关键帧计算值(也就是停在最后一帧)。
一句话总结,综合了 animation-fill-mode: backwards 和 animation-fill-mode: forwards 的设定。动画开始前的样式为动画运行时的第一帧,动画结束后停在最后一帧。
讲到了 animation-fill-mode,我们就可以顺带讲讲这个两个比较好理解的属性 -- animation-iteration-count 和 animation-direction
在上面讲述 animation-fill-mode 时,我使用了动画运行时的第一帧替代了@keyframes 中定义的第一帧这种说法,因为动画运行的第一帧和最后一帧的实际状态还会受到动画运行方向 animation-direction 和 animation-iteration-count 的影响。
在 CSS 动画中,由 animation-iteration-count 和 animation-direction 共同决定动画运行时的第一帧和最后一帧的状态。
动画的最后一帧,也就是动画运行的最终状态,并且我们可以利用 animation-fill-mode: forwards 让动画在结束后停留在这一帧,这个还是比较好理解的,但是 animation-fill-mode: backwards 和 animation-direction 的关系很容易弄不清楚,这里简答讲解下。
设置一个 100px x 100px 的滑块,在一个 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;
}
表现如下:
那么,加入 animation 之后,在不同的 animation-iteration-count 和 animation-direction 作用下,动画的初始和结束状态都不一样。
如果设置了 animation-fill-mode: backwards,则元素在动画未开始前的状态由 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 规则中,元素没有设置位移 transform: translate(0, 0),而在动画中,第一个关键帧和最后一个关键的 translateX 分别是 100px、300px,配合不同的 animation-direction 初始状态如下。
下图假设我们设置了动画默认是暂停的 -- animation-play-state: paused,那么动画在开始前的状态为:
讲完了每一个属性,我们再来看看一些动画使用过程中的细节。
看这样一个动画:
<div></div>
div {
width: 100px;
height: 100px;
background: #000;
animation: combine 2s;
}
@keyframes combine {
100% {
transform: translate(0, 150px);
opacity: 0;
}
}
这里我们实现了一个 div 块下落动画,下落的同时产生透明度的变化:
对于这样一个多个属性变化的动画,它其实等价于:
div {
animation: falldown 2s, fadeIn 2s;
}
@keyframes falldown {
100% {
transform: translate(0, 150px);
}
}
@keyframes fadeIn {
100% {
opacity: 0;
}
}
在 CSS 动画规则中,animation 是可以接收多个动画的,这样做的目的不仅仅只是为了复用,同时也是为了分治,我们对每一个属性层面的动画能够有着更为精确的控制。
我们经常能够在各种不同的 CSS 代码见到如下两种 CSS @keyframes 的设定:
@keyframes fadeIn {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes fadeIn {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
在 CSS 动画 @keyframes 的定义中,from 等同于 0%,而 to 等同于 100%。
当然,当我们的关键帧不止 2 帧的时,更推荐使用百分比定义的方式。
除此之外,当动画的起始帧等同于 CSS 规则中赋予的值并且没有设定 animation-fill-mode,0% 和 from 这一帧是可以删除的。
我曾经在这篇文章中 -- 深入理解 CSS(Cascading Style Sheets)中的层叠(Cascading) 讲过一个很有意思的 CSS 现象。
这也是很多人对 CSS 优先级的一个认知误区,在 CSS 中,优先级还需要考虑选择器的层叠(级联)顺序。
只有在层叠顺序相等时,使用哪个值才取决于样式的优先级。
那什么是层叠顺序呢?
根据 CSS Cascading 4 最新标准:
CSS Cascading and Inheritance Level 5(Current Work)
定义的当前规范下申明的层叠顺序优先级如下(越往下的优先级越高,下面的规则按升序排列):
简单翻译一下:
按照上述算法,大概是这样:
过渡动画过程中每一帧的样式 > 用户代理、用户、页面作者设置的!important样式 > 动画过程中每一帧的样式优先级 > 页面作者、用户、用户代理普通样式。
然而,经过多个浏览器的测试,实际上并不是这样。(尴尬了)
举个例子,我们可以通过这个特性,覆盖掉行内样式中的 !important 样式:
<p class="txt" style="color:red!important">123456789</p>
.txt {
animation: colorGreen 2s infinite;
}
@keyframes colorGreen {
0%,
100% {
color: green;
}
}
在 Safari 浏览器下,上述 DEMO 文本的颜色为绿色,也就是说,处于动画状态中的样式,能够覆盖掉行内样式中的 !important 样式,属于最最高优先级的一种样式,我们可以通过无限动画、或者 animation-fill-mode: forwards,利用这个技巧,覆盖掉本来应该是优先级非常非常高的行内样式中的 !important 样式。
我在早两年的 Chrome 中也能得到同样的结果,但是到今天(2022-01-10),最新版的 Chrome 已经不支持动画过程中关键帧样式优先级覆盖行内样式 !important 的特性。
对于不同浏览器,感兴趣的同学可以利用我这个 DEMO 自行尝试,CodePen Demo - the priority of CSS Animation
这也是非常多人非常关心的一个重点。
我的 CSS 动画很卡,我应该如何去优化它?
CSS 动画很卡,其实是一个现象描述,它的本质其实是在动画过程中,浏览器刷新渲染页面的帧率过低。通常而言,目前大多数浏览器刷新率为 60 次/秒,所以通常来讲 FPS 为 60 frame/s 时动画效果较好,也就是每帧的消耗时间为 16.67ms。
页面处于动画变化时,当帧率低于一定数值时,我们就感觉到页面的卡顿。
而造成帧率低的原因就是浏览器在一帧之间处理的事情太多了,超过了 16.67ms,要优化每一帧的时间,又需要完整地知道浏览器在每一帧干了什么,这个就又涉及到了老生常谈的浏览器渲染页面。
到今天,虽然不同浏览器的渲染过程不完全相同,但是基本上大同小异,基本上都是:
简化一下也就是这个图:
这两张图,你可以在非常多不同的文章中看到。
回归本文的重点,Web 动画很大一部分开销在于层的重绘,以层为基础的复合模型对渲染性能有着深远的影响。当不需要绘制时,复合操作的开销可以忽略不计,因此在试着调试渲染性能问题时,首要目标就是要避免层的重绘。那么这就给动画的性能优化提供了方向,减少元素的重绘与回流。
这其中,如何减少页面的回流与重绘呢,这里就会运用到我们常说的** GPU 加速**。
GPU 加速的本质其实是减少浏览器渲染页面每一帧过程中的 reflow 和 repaint,其根本,就是让需要进行动画的元素,生成自己的 GraphicsLayer。
浏览器渲染一个页面时,它使用了许多没有暴露给开发者的中间表现形式,其中最重要的结构便是层(layer)。
在 Chrome 中,存在有不同类型的层: RenderLayer(负责 DOM 子树),GraphicsLayer(负责 RenderLayer 的子树)。
GraphicsLayer ,它对于我们的 Web 动画而言非常重要,通常,Chrome 会将一个层的内容在作为纹理上传到 GPU 前先绘制(paint)进一个位图中。如果内容不会改变,那么就没有必要重绘(repaint)层。
而当元素生成了自己的 GraphicsLayer 之后,在动画过程中,Chrome 并不会始终重绘整个层,它会尝试智能地去重绘 DOM 中失效的部分,也就是发生动画的部分,在 Composite 之前,页面是处于一种分层状态,借助 GPU,浏览器仅仅在每一帧对生成了自己独立 GraphicsLayer 元素层进行重绘,如此,大大的降低了整个页面重排重绘的开销,提升了页面渲染的效率。
因此,CSS 动画(Web 动画同理)优化的第一条准则就是让需要动画的元素生成了自己独立的 GraphicsLayer,强制开始 GPU 加速,而我们需要知道是,GPU 加速的本质是利用让元素生成了自己独立的 GraphicsLayer,降低了页面在渲染过程中重绘重排的开销。
当然,生成自己的独立的 GraphicsLayer,不仅仅只有 transform3d api,还有非常多的方式。对于上述一大段非常绕的内容,你可以再看看这几篇文章:
除了上述准则之外,还有一些提升 CSS 动画性能的建议:
不同样式在消耗性能方面是不同的,改变一些属性的开销比改变其他属性要多,因此更可能使动画卡顿。
例如,与改变元素的文本颜色相比,改变元素的 box-shadow 将需要开销大很多的绘图操作。box-shadow 属性,从渲染角度来讲十分耗性能,原因就是与其他样式相比,它们的绘制代码执行时间过长。这就是说,如果一个耗性能严重的样式经常需要重绘,那么你就会遇到性能问题。
类似的还有 CSS 3D 变换、mix-blend-mode、filter,这些样式相比其他一些简单的操作,会更加的消耗性能。我们应该尽可能的在动画过程中降低其使用的频率或者寻找替代方案。
当然,没有不变的事情,在今天性能很差的样式,可能明天就被优化,并且浏览器之间也存在差异。
因此关键在于,我们需要针对每一起卡顿的例子,借助开发工具来分辨出性能瓶颈所在,然后设法减少浏览器的工作量。学会 Chrome 开发者工具的 Performance 面板及其他渲染相关的面板非常重要,当然这不是本文的重点。大家可以自行探索。
will-change 为 Web 开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。 这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏。
值得注意的是,用好这个属性并不是很容易:
有人说 will-change 是良药,也有人说是毒药,在具体使用的时候,可以多测试一下。
好了,本文从多个方面,由浅入深地描述了 CSS 动画我认为的一些比较重要、值得一讲、需要注意的点。当然很多地方点到即止,或者限于篇幅没有完全展开,很多细节还需要读者进一步阅读规范或者自行尝试验证,实践出真知,纸上得来终觉浅。
OK,本文到此结束,希望本文对你有所帮助 :)
*请认真填写需求信息,我们会在24小时内与您取得联系。