变量声明和定义的区别_寄存器变量声明_变量声明 定义

栏目介绍

《Top Writer》是专门为OPPO工程师解读当下热点技术的专栏。 在这里,你不仅可以看到最新最热的动态,还可以跟OPPO优秀的工程师一起学习技术干货。

第一作家

■鲍勃

■前端“IT新生代民工”,在前端领域工作9年多,立志分享各种前端冷知识。

01

背景

相信用过UI框架的同学应该或多或少修改过它的样式,最有可能修改的样式应该是:color,而且大多数时候可能是框架官方提供了主题配置,比如如 ElementUI 的 Ant Design 的主题色配置(使用 Sass 变量),或者 Ant Design 的主题色配置(使用 Less 变量),可以通过定义一个变量如@primary-color(Ant Design) 或 $– color-primary(ElementUI) 自定义想要的主题色,比如OPPO绿。

但是相信大家都有体会,因为是使用的CSS预处理器的能力,而不是CSS的原生支持,这不仅意味着需要打包工具的支持,也意味着这些变量并不是真正的“变量” . 它将替换为 CSS 可以识别的颜色值,并且不再可能通过更改其中一种原色来更改页面上所有相关的颜色。 这对于只使用一组 OPPO 绿色的场景来说还好,但如果是需要更换动态主题色的场景呢? 或者像夜间模式这样的变色场景呢? 这对于CSS来说其实并不容易。

不是用CSS做不到,而是开发体验不好。 比如可以为不同的主题设置不同的class,根据不同的class重写所有用到颜色的地方。 想象一下,在业务 CSS 中有 20 个地方使用了主题颜色。 每增加一套主题,就需要重写这20个地方的颜色定义。 这实际上是我们过去的做法。

当然,还有一种方法是将浏览器版本的 CSS 预处理器嵌入到页面中。 如果你在网上使用过这种方法,或者有这样的想法,请务必先阅读这篇文章,然后再考虑是否还有其他方法。

遇到过需要动态改变主题的同学一定搜索过,了解过CSS自定义变量的概念。 如果你还没有使用过,别着急,看完你就会明白。

不知道有没有人注意到,题目中有个奇怪的–custom-property,有点类似于ElementUI的变量定义方式$–color-primary。 那么这是什么,这就是CSS自定义变量的定义方式。

02

基本定义

让我们看一下基本定义。 CSS自定义变量由两个-加名字组成,名字和JavaScript变量名规则几乎一样,比如区分大小写(这点不同于CSS普通的属性名,CSS属性名一般不区分大小写-敏感),但与 JavaScript 不同的是,CSS 自定义变量名可以以数字开头,包括纯数字。 比如–1是合法的CSS自定义变量,甚至是中文变量声明和定义的区别,甚至是emoji。

有句老话说:你能做到并不意味着你应该做到。 想想你在别人的代码里看到–1:5px,–2:red写的,然后用到的地方都是不知名的–1和–2,试着表达一下你内心的感受。

事实上,CSS规范和MDN文档都使用了“自定义属性”这个词,但是“自定义属性”给人的感觉没有“自定义变量”那么清晰和吸引人,所以本文一直使用“自定义变量”这个词,请记住对应于“自定义属性”或者规范中也提到了与变量相关的词:“级联变量”。

这里之所以可以用数字开头或者纯数字,正是因为 — 以上原因不能作为变量名,因为实际的名字应该是包含 — 的整体。

但是有名字并不是可以使用的变量,需要给它赋值才有用,而CSS自定义变量必须存在于CSS中元素规则的定义中,不能写在最外层像 JavaScript 作为全局变量,当然也有相应的方法来定义全局变量,后面会提到。 所以一个完整的CSS自定义变量的定义应该是:

<pre class="code-snippet__js" data-lang="css“>:root { --custom-variable: ;}
/* 举几个栗子 */html { --color-primary: green; --color-disabled: gray; --wide-border: 3px;}

除了在 CSS 文件中定义还有样式属性和相应的 JavaScript 方法。 但是注意,这个时候其实是“在某个元素规则的定义中”,因为它只会对这个属性所在的元素及其子元素有效。

03

使用自定义变量

用法

好的,我们有一个有效的 CSS 自定义变量,我们如何使用它呢? CSS定义了var()方法,比如可以用var(–color-primary)来读取。 所以一个浅绿色背景的 div 可以这样设计:

div {  background-color: var(--color-primary);}

var() 方法也支持默认值。 当对应变量未定义或值为initial时,将读取默认值。 定义方法很简单,用 隔开,后面的值是默认值。 例如var(–color-primary, blue),当–color-primary未定义或值为initial时,返回blue。

但请注意,如果有多个逗号,第一个逗号后的整体将被用作默认值,而不是用逗号分隔。 所以 var(–color-primary, blue, cyan) 的默认值类似于 blue, cyan。

当定义了自定义变量时,也可以使用其他自定义变量。 同时var()支持多级嵌套,所以默认值也可以是另一个自定义变量。 例如下面的形式:

div.bordered {  --color-border: var(--color-secondary, green);  /* 注意 --color-secondary 并未定义 */  border: var(--wide-border) var(--color-border) solid;  color: var(--color-text, var(--color-disabled, black));}

看到这里应该知道怎么用了,不过怎么讲了这么久,好像和css预处理器的变量定义看起来一样。 好了,别着急,我们先问一句,大家还记得缩写CSS中的C是哪个单词吗? 嗯,就是“Cascading”:“级联”。 这个词也是CSS独特的魅力所在,也是最容易引起混淆的地方,不过相信看过文章的小伙伴应该很清楚级联的优先级。 不熟悉的赶紧上教程。 为什么突然提到,因为CSS自定义变量也遵循级联的概念,相同的变量定义默认会被继承,同时高优先级的定义会覆盖低优先级的定义。 这是变量可以“动态”改变其值的一个重要原因。

那么想一想,如果在同一个页面上依次定义前面所有的代码块,页面中有一个div和一个div.bordered,它们应该是什么样子的呢?

寄存器变量声明_变量声明 定义_变量声明和定义的区别

在您了解了如何使用之后,假设您需要为该页面添加多种主题样式。 结合上面提到的级联变量声明和定义的区别,你想到怎么添加了吗? 是的,我们可以使用新样式来覆盖变量:

.pink-theme {    --color-primary: pink;    --color-border: deeppink;}.gold-theme {    --color-primary: gold;    --color-border: goldenrod;}/* 还可以加更多 */

寄存器变量声明_变量声明 定义_变量声明和定义的区别

当然,如果你不想加class,也可以直接在对应的标签上写style来覆盖。

文章开头提到过,这种在CSS中变量值的动态变化,在CSS预处理器中是不能轻易做到的,因为它们的变量定义和名字一样,都是经过预处理后放在CSS中的。 这也是CSS自定义变量相对于预处理器的一大优势。

全局变量

那么看完前面的用法部分,你有没有想通如何定义一个全局的CSS变量。 因为自定义变量是默认继承的,所以干脆把样式放在覆盖范围最广的根元素上,也就是HTML中的元素(其实一般情况下应该够用了)。

在前面的例子中,写了一个 :root 。 这个伪类也是指根元素,根元素指的是HTML中的html元素。 有什么不同? 比如下面的定义,div的背景应该是什么颜色?

:root {  --color-bg: red;}html {  --color-bg: blue;}div {  background-color: var(--color-bg);}

如果你看实际页面,你会发现它是红色的,为什么? :root 是伪类,所以算作类(class)的优先级。 回忆一下优先级定义,类的优先级高于元素类型选择器的html。

除了这种通过继承来增加覆盖率的形式,还有一种定义全局变量的方式,后面会提到,我们来看一些问题。

无效值

从上述用法来看,一般情况下,CSS自定义变量可以简单理解为在调用var()的地方将其值替换为文本,但在实现上有一些区别。 抛开css解析计算的过程细节不谈,可以看成是css解析器忽略了使用自定义变量对属性值的语法检测,即不管值本身是否可以用在对应的属性上。 所以即使给自定义变量传递了一个非法的值,这个属性仍然会被正常解析,只是在计算值的时候会出错; 这不同于直接写入非法值,后者的CSS解析器会检测语法错误,从而提前忽略此规则。

这么说可能有点绕,就拿MDN上的例子作为简单的扩展解释:

:root { --text-color: 16px; }p { color: blue; }p { color: var(--text-color); }
div { color: blue; }div { color: 16px; }

打开演示页面,第一行<p>为黑色,第二行<div>为蓝色。 这是因为color:var(–text-color)在<p>的定义中使用CSS自定义变量正常解析,覆盖了之前color:blue的定义(CSS解析器一般会直接丢弃这个无用的规则定义),然后在替换–text-color时发现不是一个值,也不是合法的颜色,导致<p>元素的无效颜色定义被重置。 因为color是一个继承的属性,<p>会先尝试获取继承的值,因为demo中没有在parent上定义color,所以使用浏览器默认样式中的继承颜色:black。 而这里因为​​color:16px被浏览器认为是非法的,在解析时被忽略,所以选择了之前有效的定义color:blue。

这样无效值的重置与CSS全局关键字中unset的效果是一致的,即当支持继承的属性被unset时,相当于从parent继承,不支持的属性支持继承,相当于initial初始值。 这三个词正好是三个 CSS 全局关键字。 另请注意,这三个关键字在使用自定义变量时也有效。 给它们设置自定义变量值时,表示对这个自定义变量进行unset、inherit和initial操作:

:root {  --text-color: green;  color: red;}div {  --text-color: inherit;  color: var(--text-color);}

在上面的例子中,div 的文本颜色会是绿色而不是红色,因为这个inherit 意味着自定义变量是从parent 继承的,即–text-color 的值被计算为绿色而不是替换后的颜色属性的文本变为 color: inherit;。

另一个具有这种能力的关键字可能是“臭名昭著的”!important。 在自定义变量中使用时,也意味着自定义变量本身的优先级是!important,而不是将其作为文本替换。 这个我就不写例子了,留给大家自己去尝试吧。

因为不是文本替换,有些地方不能写自定义变量。 例如,CSS 属性名不能被自定义变量替换,值中的数字和单位也不能被自定义变量分隔和替换:

:root {  --property-name: padding;  --padding:10;}
p { var(--property-name):100px;/* 语法错误,忽略 */}p { padding:10px; padding:var(--padding)px; /* 正常解析但计算值失败,等同于 padding: unset,所以前一条规则会被覆盖 */}p.correct { padding: calc(var(--padding)*1px); /* 注意可以利用 calc() 来实现单位 */}

注意上面第二个p的定义,padding:10px; 不生效,因为var(–padding)px在语法上不会被认为是无效的,但是实际计算的时候发现不能正确计算出这个值,所以相当于unset。 但是如果你真的只想定义一个数,并且在使用的时候加上单位,也可以使用例子中的calc()方法来实现。

虽然不能像本例那样拼接单元,但是可以通过CSS自定义变量将一个属性值拆分为多个部分值,后面的例子会提到。

循环引用

CSS自定义变量允许在var()中嵌入另一个var(),所以有一个不可避免的问题:

:root {  --margin:10px;}div {  --padding: calc(var(--margin)-10px);  margin:var(--margin);  padding:var(--padding);}div.cyclic {   --margin: calc(var(--padding)+10px);}

这种情况下div.cyclic的–margin和–padding应该如何解决? 规范定义如果同一个元素下发生循环引用,则本次循环中的所有自定义变量都等于初始值,即未定义值。

注意同一个元素的关键字,因为自定义变量默认是继承的,继承行为是在值计算之后,所以不在同一个元素下的定义不一定构成循环引用。 比如我们把最后一个定义改成下面的div p:

:root {  --margin:20px;}div {  --padding: calc(var(--margin)-10px);  margin:var(--margin);  padding:var(--padding);  background:#f00;}div.cyclic p {   --margin: calc(var(--padding)+10px);   margin:var(--margin);   background:#0f0;}

本例中div.cyclic p的–padding可以正常计算得到20px,因为p计算–margin时,–padding继承自div,已经计算为10px。

更多例子

前面的例子中提到,CSS自定义变量在取值时基本相当于文本替换,没有类型的概念。 这样的操作在某些场景下也会出乎意料的方便,比如CSS-Trick上提到的例子:

button {  --h: 100;  --s: 50%;  --l: 50%;  --a: 1;    background: hsl(var(--h) var(--s) var(--l) / var(--a));}button:hover { /* Change the lightness on hover */  --l: 75%;}button:focus { /* Change the saturation on focus */  --s: 75%;}button[disabled] {  /* Make look disabled */  --s: 0%;  --a: 0.5;}

甚至有使用自定义变量作为开关的做法,但请谨慎使用…

这里只是一些使用示例。 相信很多同学在实际工作中都有自定义变量,包括我们的项目。

04

关于动画

由于自定义变量是 CSS 的属性,它们在动画中是受支持的。 但是由于没有类型可言,CSS 解析器不知道如何应用动画样式。 效果不会和想象中的一样:

.color-div {  --angle: 0deg;  background: linear-gradient(var(--angle), red, yellow, blue, purple);  animation: rotate 5s ease-in-out both alternate infinite;}@keyframes rotate {  to {    --angle: 180deg;   }}

打开这个demo,你会发现背景没有“动”,只有颜色跳动,其实这应该符合预期,毕竟渐变算作一张图片,本身不支持动画。

其实看一眼就能明白这段CSS代码的意思,但是替换的方式让自定义变量支持过渡动画和动画样式,但实际上并没有什么用。 但是这么明显的问题,规范也考虑到了这一点,那么接下来,我们看看他们是如何解决这个问题的?

另一个“全局变量”定义(注册)

鉴于CSS自定义变量语法松散,无法定义值的类型、是否继承、初始值,导致动画无法很好实现等问题。 CSS Houdini 添加@property 进行定义。 准确的说,注册(register)一个规范使用的CSS自定义变量可以设置它的类型语法(或者它遵循的语法),是否可以继承inherits以及初始值initial-value。 下面结合上面的例子,简单说明一下@property的用法和一些需要注意的地方。 前面的例子只要加上这个定义语句就可以正常运行了~

@property --angle { syntax: "";  inherits: false;  initial-value: ‘0deg’;}

同时@property还有一个等价的JavaScript接口可以调用,比如前面的声明,可以使用下面的JavaScript实现同样的声明效果:

CSS.registerProperty({  name: '--angle',  syntax: '',  inherits: false,  initialValue: ‘0deg’;});

从上面的例子和解释来看,这些属性的含义相信是非常直截了当的。 但是在实际使用中有几点需要注意:

●首先,与大多数@语句一致,@property目前必须出现在CSS的最外层,不能嵌套在其他样式声明中,也不能出现在ShadowDOM中的最外层(但这种行为可能会改变,规范正在讨论中)。 可以嵌套在条件判断的@语句中,比如@media:

:root {  @property --primary-color {    /*不会生效*/  }}
@media (min-width: 1200px) { @property --width { /* 有效 */ }}

●其次,必须设置所有属性,否则整个定义将被忽略。 唯一的特殊情况是语法属性为 * 时,初始值可以留空。 但是,语法为*的自定义变量的性能与普通定义基本相同。

●需要填写initial-value时,其值必须符合syntax定义的文法,必须是计算上独立的值,否则整个定义被忽略。 什么是“计算独立性”? 简单理解就是不再依赖其他CSS属性值。 比如1em就不符合条件,因为它依赖于font-size的定义。 例如,var(–other-property) 是不允许的,但是像 1px 和#F00 这样的值是可以的。

@property --width {  syntax: "";  inherits: true;  initial-value: #f00; /* 不合定义语法的值,整个定义被忽略 */}@property --width {  syntax: "";  inherits: true;  initial-value: 1rem; /* 依赖根元素font-size,不符合条件,整个定义被忽略 */}

这里再补充一点关于initial-value,因为它在替换var()中的存在与之前略有不同,沿用上篇文章的例子:

:root { --text-color: 16px; }p { color: blue; }p { color: var(--text-color); }
div { color: blue; }div { color: 16px; }

在这个例子中,上面提到的<p>的颜色会因为定义错误而被取消设置,所以会变成黑色。 但是如果我们添加定义:

@property --text-color {  syntax: "";  inherits: true;  initial-value: #f00;}

在这种情况下,在计算值时,–text-color: 16px; 等同于 –text-color: unset; 因为不符合声明定义的语法,所以结果是 –text-color: #f00 ; (即初始值),因此 <p> 将显示为红色而不是黑色。 但是要注意校验是在div计算值的时候发生的,而不是在解析css的时候,所以我们把–text-color的定义改成下面这种情况,效果还是一样的:

:root {   --text-color: #0f0;   --text-color: 16px; /* 依旧是unset,而不是选择读取前一条,因为不是在解析时判断的语法错误 */}

由于各种原因,规范选择在解析时不做语法检查,所以即使在计算值时有语法检查,这也不符合常规的解析错误。 如果你不太理解这句话,请回想一下前面验证错误示例中的 div 文本是蓝色的。

另请注意,对于具有初始值的全局变量,请使用 var(–property, someValue),即使您手动设置了 –property: initial; someValue 不会被读取,而是相应的 initial-value 定义的初始值。

语法支持多种类型的填充,也支持复合类型。 前面可以猜到的“”,或者“”和“>”都是合法的值。 可以在此处找到详细列表。 请注意,引号是必需的,因为此值需要字符串类型。

关于多个同名声明的优先级,如果是CSS定义中的@property同名声明,最后一个生效; 但如果有同名的JavaScript声明,则优先级最高,JavaScript方法不允许重复声明同名变量。

最后,目前还没有注销变量的方法,但规范中提到可能会在以后添加。

好了,本文到这里就告一段落了,感谢大家抽空阅读。 希望大家看完之后对CSS自定义变量有更好的理解。