八、 数组reduce高级用法
8.1 reduce()的相关说明。
-
定义:对数组中的每个元素执行一个自定义的累计器,将其结果汇总为单个返回值
-
形式:
array.reduce((t, v, i, a) => {}, initValue) -
参数
- callback:回调函数(
必选) - initValue:初始值(
可选)
- callback:回调函数(
-
回调函数的参数
- total(
t):累计器完成计算的返回值(必选) - value(
v):当前元素(必选) - index(
i):当前元素的索引(可选) - array(
a):当前元素所属的数组对象(可选)
- total(
-
过程
- 以
t作为累计结果的初始值,不设置t则以数组第一个元素为初始值 - 开始遍历,使用累计器处理
v,将v的映射结果累计到t上,结束此次循环,返回t - 进入下一次循环,重复上述操作,直至数组最后一个元素
- 结束遍历,返回最终的
t
- 以
reduce的精华所在是将累计器逐个作用于数组成员上,把上一次输出的值作为下一次输入的值。下面举个简单的栗子,看看reduce的计算结果。
const arr = [3, 5, 1, 4, 2];
const a = arr.reduce((t, v) => t + v);
// 等同于
const b = arr.reduce((t, v) => t + v, 0);
另外reduce还有一个胞弟reduceRight,两个方法的功能其实是一样的,只不过reduce是升序执行,reduceRight是降序执行。
对空数组调用reduce()和reduceRight()是不会执行其回调函数的,可认为reduce()对空数组无效
8.2 高级用法
8.21 累加累乘
function Accumulation(...vals) {
return vals.reduce((t, v) => t + v, 0);
}
function Multiplication(...vals) {
return vals.reduce((t, v) => t * v, 1);
}
Accumulation(1, 2, 3, 4, 5); // 15
Multiplication(1, 2, 3, 4, 5); // 120
8.22 权重求和
const scores = [
{ score: 90, subject: "chinese", weight: 0.5 },
{ score: 95, subject: "math", weight: 0.3 },
{ score: 85, subject: "english", weight: 0.2 }
];
const result = scores.reduce((t, v) => t + v.score * v.weight, 0); // 90.5
8.23 代替reverse
function Reverse(arr = []) {
return arr.reduceRight((t, v) => (t.push(v), t), []);
}
Reverse([1, 2, 3, 4, 5]); // [5, 4, 3, 2, 1]
8.24 代替map和filter
const arr = [0, 1, 2, 3];
// 代替map:[0, 2, 4, 6]
const a = arr.map(v => v * 2);
const b = arr.reduce((t, v) => [...t, v * 2], []);
// 代替filter:[2, 3]
const c = arr.filter(v => v > 1);
const d = arr.reduce((t, v) => v > 1 ? [...t, v] : t, []);
// 代替map和filter:[4, 6]
const e = arr.map(v => v * 2).filter(v => v > 2);
const f = arr.reduce((t, v) => v * 2 > 2 ? [...t, v * 2] : t, []);
8.25 代替some和every
const scores = [
{ score: 45, subject: "chinese" },
{ score: 90, subject: "math" },
{ score: 60, subject: "english" }
];
// 代替some:至少一门合格
const isAtLeastOneQualified = scores.reduce((t, v) => t || v.score >= 60, false); // true
// 代替every:全部合格
const isAllQualified = scores.reduce((t, v) => t && v.score >= 60, true); // false
8.26 数组分割
function Chunk(arr = [], size = 1) {
return arr.length ? arr.reduce((t, v) => (t[t.length - 1].length === size ? t.push([v]) : t[t.length - 1].push(v), t), [[]]) : [];
}
const arr = [1, 2, 3, 4, 5];
Chunk(arr, 2); // [[1, 2], [3, 4], [5]]
8.27 数组过滤
function Difference(arr = [], oarr = []) {
return arr.reduce((t, v) => (!oarr.includes(v) && t.push(v), t), []);
}
const arr1 = [1, 2, 3, 4, 5];
const arr2 = [2, 3, 6]
Difference(arr1, arr2); // [1, 4, 5]
8.28 数组填充
function Fill(arr = [], val = "", start = 0, end = arr.length) {
if (start < 0 || start >= end || end > arr.length) return arr;
return [
...arr.slice(0, start),
...arr.slice(start, end).reduce((t, v) => (t.push(val || v), t), []),
...arr.slice(end, arr.length)
];
}
const arr = [0, 1, 2, 3, 4, 5, 6];
Fill(arr, "aaa", 2, 5); // [0, 1, "aaa", "aaa", "aaa", 5, 6]
8.29 数组扁平
function Flat(arr = []) {
return arr.reduce((t, v) => t.concat(Array.isArray(v) ? Flat(v) : v), [])
}
const arr = [0, 1, [2, 3], [4, 5, [6, 7]], [8, [9, 10, [11, 12]]]];
Flat(arr); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
8.210 数组去重
function Uniq(arr = []) {
return arr.reduce((t, v) => t.includes(v) ? t : [...t, v], []);
}
const arr = [2, 1, 0, 3, 2, 1, 2];
Uniq(arr); // [2, 1, 0, 3]
8.211 数组最大最小值
function Max(arr = []) {
return arr.reduce((t, v) => t > v ? t : v);
}
function Min(arr = []) {
return arr.reduce((t, v) => t < v ? t : v);
}
const arr = [12, 45, 21, 65, 38, 76, 108, 43];
Max(arr); // 108
Min(arr); // 12
8.212 数组成员独立拆解
function Unzip(arr = []) {
return arr.reduce(
(t, v) => (v.forEach((w, i) => t[i].push(w)), t),
Array.from({ length: Math.max(...arr.map(v => v.length)) }).map(v => [])
);
}
const arr = [["a", 1, true], ["b", 2, false]];
Unzip(arr); // [["a", "b"], [1, 2], [true, false]]
8.213 数组成员个数统计
function Count(arr = []) {
return arr.reduce((t, v) => (t[v] = (t[v] || 0) + 1, t), {});
}
const arr = [0, 1, 1, 2, 2, 2];
Count(arr); // { 0: 1, 1: 2, 2: 3 }
此方法是字符统计和单词统计的原理,入参时把字符串处理成数组即可
8.214 数组成员位置记录
function Position(arr = [], val) {
return arr.reduce((t, v, i) => (v === val && t.push(i), t), []);
}
const arr = [2, 1, 5, 4, 2, 1, 6, 6, 7];
Position(arr, 2); // [0, 4]
8.215 数组成员特性分组
function Group(arr = [], key) {
return key ? arr.reduce((t, v) => (!t[v[key]] && (t[v[key]] = []), t[v[key]].push(v), t), {}) : {};
}
const arr = [
{ area: "GZ", name: "YZW", age: 27 },
{ area: "GZ", name: "TYJ", age: 25 },
{ area: "SZ", name: "AAA", age: 23 },
{ area: "FS", name: "BBB", age: 21 },
{ area: "SZ", name: "CCC", age: 19 }
]; // 以地区area作为分组依据
Group(arr, "area"); // { GZ: Array(2), SZ: Array(2), FS: Array(1) }
8.216 数组成员所含关键字统计
function Keyword(arr = [], keys = []) {
return keys.reduce((t, v) => (arr.some(w => w.includes(v)) && t.push(v), t), []);
}
const text = [
"今天天气真好,我想出去钓鱼",
"我一边看电视,一边写作业",
"小明喜欢同桌的小红,又喜欢后桌的小君,真TM花心",
"最近上班喜欢摸鱼的人实在太多了,代码不好好写,在想入非非"
];
const keyword = ["偷懒", "喜欢", "睡觉", "摸鱼", "真好", "一边", "明天"];
Keyword(text, keyword); // ["喜欢", "摸鱼", "真好", "一边"]
8.217 字符串翻转
function ReverseStr(str = "") {
return str.split("").reduceRight((t, v) => t + v);
}
const str = "reduce最牛逼";
ReverseStr(str); // "逼牛最ecuder"
8.218 数字千分化
function ThousandNum(num = 0) {
const str = (+num).toString().split(".");
const int = nums => nums.split("").reverse().reduceRight((t, v, i) => t + (i % 3 ? v : `${v},`), "").replace(/^,|,$/g, "");
const dec = nums => nums.split("").reduce((t, v, i) => t + ((i + 1) % 3 ? v : `${v},`), "").replace(/^,|,$/g, "");
return str.length > 1 ? `${int(str[0])}.${dec(str[1])}` : int(str[0]);
}
ThousandNum(1234); // "1,234"
ThousandNum(1234.00); // "1,234"
ThousandNum(0.1234); // "0.123,4"
ThousandNum(1234.5678); // "1,234.567,8"
8.219 异步累计
async function AsyncTotal(arr = []) {
return arr.reduce(async(t, v) => {
const at = await t;
const todo = await Todo(v);
at[v] = todo;
return at;
}, Promise.resolve({}));
}
const result = await AsyncTotal(); // 需要在async包围下使用
8.220 斐波那契数列
function Fibonacci(len = 2) {
const arr = [...new Array(len).keys()];
return arr.reduce((t, v, i) => (i > 1 && t.push(t[i - 1] + t[i - 2]), t), [0, 1]);
}
Fibonacci(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
8.221 URL参数反序列化
// 正则表达式需要转义的字符:* . ? + ^ $ | \ / [ ] ( ) { }
function ParseUrlSearch() {
return location.search.replace(/(\^?)|(&\$)/g, "").split("&").reduce((t, v) => {
const [key, val] = v.split("=");
t[key] = decodeURIComponent(val);
return t;
}, {});
}
// 假设URL为:https://www.baidu.com?age=25&name=TYJ
ParseUrlSearch(); // { age: "25", name: "TYJ" }
8.222 URL参数序列化
function StringifyUrlSearch(search = {}) {
return Object.entries(search).reduce(
(t, v) => `${t}${v[0]}=${encodeURIComponent(v[1])}&`,
Object.keys(search).length ? "?" : ""
).replace(/&$/, "");
}
StringifyUrlSearch({ age: 27, name: "YZW" }); // "?age=27&name=YZW"
8.223 返回对象指定键值
function GetKeys(obj = {}, keys = []) {
return Object.keys(obj).reduce((t, v) => (keys.includes(v) && (t[v] = obj[v]), t), {});
}
const target = { a: 1, b: 2, c: 3, d: 4 };
const keyword = ["a", "d"];
GetKeys(target, keyword); // { a: 1, d: 4 }
8.224 数组转对象
const people = [
{ area: "GZ", name: "YZW", age: 27 },
{ area: "SZ", name: "TYJ", age: 25 }
];
const map = people.reduce((t, v) => {
const { name, ...rest } = v;
t[name] = rest;
return t;
}, {}); // { YZW: {…}, TYJ: {…} }
8.225 Redux Compose函数原理
function Compose(...funs) {
if (funs.length === 0) {
return arg => arg;
}
if (funs.length === 1) {
return funs[0];
}
return funs.reduce((t, v) => (...arg) => t(v(...arg)));
}
8.3 兼容和性能
好用是挺好用的,但是兼容性如何呢?在Caniuse上搜索一番,兼容性绝对的好,可大胆在任何项目上使用。尽情发挥reduce的compose技能啦。对于时常做一些累计的功能,reduce绝对是首选方法。
reduce的性能如何呢?通过对for、forEach、map和reduce四个方法同时做1~100000的累加操作,看看四个方法各自的执行时间。
// 创建一个长度为100000的数组
const list = [...new Array(100000).keys()];
// for
console.time("for");
let result1 = 0;
for (let i = 0; i < list.length; i++) {
result1 += i + 1;
}
console.log(result1);
console.timeEnd("for");
// forEach
console.time("forEach");
let result2 = 0;
list.forEach(v => (result2 += v + 1));
console.log(result2);
console.timeEnd("forEach");
// map
console.time("map");
let result3 = 0;
list.map(v => (result3 += v + 1, v));
console.log(result3);
console.timeEnd("map");
// reduce
console.time("reduce");
const result4 = list.reduce((t, v) => t + v + 1, 0);
console.log(result4);
console.timeEnd("reduce");
| 累加操作 | 执行时间 |
|---|---|
| for | 6.719970703125ms |
| forEach | 3.696044921875ms |
| map | 3.554931640625ms |
| reduce | 2.806884765625ms |
九 硬核技巧带你迅速提升CSS技术
- 神奇的选择器
- 浅谈布局那些事
- 绘制三角的原理
- 完美极致的变量
- 灵活多变的障眼法
- 意向不到的内容插入
为了使各大浏览器默认样式一致,还需引入一个磨平浏览器默认样式的css文件,reset.css到本地目录里。
9.1 神奇的选择器
基础选择器
| 选择器 | 别名 | 说明 | 版本 | 常用 |
|---|---|---|---|---|
tag | 标签选择器 | 指定类型的标签 | 1 | √ |
#id | ID选择器 | 指定身份的标签 | 1 | √ |
.class | 类选择器 | 指定类名的标签 | 1 | √ |
* | 通配选择器 | 所有类型的标签 | 2 | √ |
层次选择器
| 选择器 | 别名 | 说明 | 版本 | 常用 |
|---|---|---|---|---|
elemP elemC | 后代选择器 | 元素的后代元素 | 1 | √ |
elemP>elemC | 子代选择器 | 元素的子代元素 | 2 | √ |
elem1+elem2 | 相邻同胞选择器 | 元素相邻的同胞元素 | 2 | √ |
elem1~elem2 | 通用同胞选择器 | 元素后面的同胞元素 | 3 | √ |
集合选择器
| 选择器 | 别名 | 说明 | 版本 | 常用 |
|---|---|---|---|---|
elem1,elem2 | 并集选择器 | 多个指定的元素 | 1 | √ |
elem.class | 交集选择器 | 指定类名的元素 | 1 | √ |
条件选择器
| 选择器 | 说明 | 版本 | 常用 |
|---|---|---|---|
:lang | 指定标记语言的元素 | 2 | × |
:dir() | 指定编写方向的元素 | 4 | × |
:has | 包含指定元素的元素 | 4 | × |
:is | 指定条件的元素 | 4 | × |
:not | 非指定条件的元素 | 4 | √ |
:where | 指定条件的元素 | 4 | × |
:scope | 指定元素作为参考点 | 4 | × |
:any-link | 所有包含href的链接元素 | 4 | × |
:local-link | 所有包含href且属于绝对地址的链接元素 | 4 | × |
状态选择器
| 选择器 | 说明 | 版本 | 常用 |
|---|---|---|---|
:active | 鼠标激活的元素 | 1 | × |
:hover | 鼠标悬浮的元素 | 1 | √ |
:link | 未访问的链接元素 | 1 | × |
:visited | 已访问的链接元素 | 1 | × |
:target | 当前锚点的元素 | 3 | × |
:focus | 输入聚焦的表单元素 | 2 | √ |
:required | 输入必填的表单元素 | 3 | √ |
:valid | 输入合法的表单元素 | 3 | √ |
:invalid | 输入非法的表单元素 | 3 | √ |
:in-range | 输入范围以内的表单元素 | 3 | × |
:out-of-range | 输入范围以外的表单元素 | 3 | × |
:checked | 选项选中的表单元素 | 3 | √ |
:optional | 选项可选的表单元素 | 3 | × |
:enabled | 事件启用的表单元素 | 3 | × |
:disabled | 事件禁用的表单元素 | 3 | √ |
:read-only | 只读的表单元素 | 3 | × |
:read-write | 可读可写的表单元素 | 3 | × |
:target-within | 内部锚点元素处于激活状态的元素 | 4 | × |
:focus-within | 内部表单元素处于聚焦状态的元素 | 4 | √ |
:focus-visible | 输入聚焦的表单元素 | 4 | × |
:blank | 输入为空的表单元素 | 4 | × |
:user-invalid | 输入合法的表单元素 | 4 | × |
:indeterminate | 选项未定的表单元素 | 4 | × |
:placeholder-shown | 占位显示的表单元素 | 4 | √ |
:current() | 浏览中的元素 | 4 | × |
:past() | 已浏览的元素 | 4 | × |
:future() | 未浏览的元素 | 4 | × |
:playing | 开始播放的媒体元素 | 4 | × |
:paused | 暂停播放的媒体元素 | 4 | × |
结构选择器
| 选择器 | 说明 | 版本 | 常用 |
|---|---|---|---|
:root | 文档的根元素 | 3 | × |
:empty | 无子元素的元素 | 3 | √ |
:nth-child(n) | 元素中指定顺序索引的元素 | 3 | √ |
:nth-last-child(n) | 元素中指定逆序索引的元素 | 3 | × |
:first-child | 元素中为首的元素 | 2 | √ |
:last-child | 元素中为尾的元素 | 3 | √ |
:only-child | 父元素仅有该元素的元素 | 3 | √ |
:nth-of-type(n) | 标签中指定顺序索引的标签 | 3 | √ |
:nth-last-of-type(n) | 标签中指定逆序索引的标签 | 3 | × |
:first-of-type | 标签中为首的标签 | 3 | √ |
:last-of-type | 标签中为尾标签 | 3 | √ |
:only-of-type | 父元素仅有该标签的标签 | 3 | √ |
属性选择器
| 选择器 | 说明 | 版本 | 常用 | |
|---|---|---|---|---|
[attr] | 指定属性的元素 | 2 | √ | |
[attr=val] | 属性等于指定值的元素 | 2 | √ | |
[attr*=val] | 属性包含指定值的元素 | 3 | √ | |
[attr^=val] | 属性以指定值开头的元素 | 3 | √ | |
[attr$=val] | 属性以指定值结尾的元素 | 3 | √ | |
[attr~=val] | 属性包含指定值(完整单词)的元素(不推荐使用) | 2 | × | |
| `[attr | =val]` | 属性以指定值(完整单词)开头的元素(不推荐使用) | 2 | × |
伪元素
| 选择器 | 说明 | 版本 | 常用 |
|---|---|---|---|
::before | 在元素前插入的内容 | 2 | √ |
::after | 在元素后插入的内容 | 2 | √ |
::first-letter | 元素的首字母 | 1 | × |
::first-line | 元素的首行 | 1 | × |
::selection | 鼠标选中的元素 | 3 | × |
::backdrop | 全屏模式的元素 | 4 | × |
::placeholder | 表单元素的占位 | 4 | √ |
9.2 浅谈布局那些事
9.21 全屏布局
经典的全屏布局由顶部、底部和主体三部分组成,其特点为三部分左右满屏拉伸、顶部底部高度固定和主体高度自适应。该布局很常见,也是大部分Web应用主体的主流布局。通常使用<header>、<footer>和<main>三个标签语义化排版,<main>内还可插入<aside>侧栏或其他语义化标签。
<div class="fullscreen-layout">
<header></header>
<main></main>
<footer></footer>
</div>
position + left/right/top/bottom
三部分统一声明left:0和right:0将其左右满屏拉伸;顶部和底部分别声明top:0和bottom:0将其吸顶和吸底,并声明俩高度为固定值;将主体的top和bottom分别声明为顶部高度和底部高度。通过绝对定位的方式将三部分固定在特定位置,使其互不影响。
.fullscreen-layout {
position: relative;
width: 400px;
height: 400px;
header,
footer,
main {
position: absolute;
left: 0;
right: 0;
}
header {
top: 0;
height: 50px;
background-color: #f66;
}
footer {
bottom: 0;
height: 50px;
background-color: #66f;
}
main {
top: 50px;
bottom: 50px;
background-color: #3c9;
}
}
flex
使用flex实现会更简洁。display:flex默认会令子节点横向排列,需声明flex-direction:column改变子节点排列方向为纵向排列;顶部和底部高度固定,所以主体需声明flex:1让高度自适应。
.fullscreen-layout {
display: flex;
flex-direction: column;
width: 400px;
height: 400px;
header {
height: 50px;
background-color: #f66;
}
footer {
height: 50px;
background-color: #66f;
}
main {
flex: 1;
background-color: #3c9;
}
}
若<main>需表现成可滚动状态,千万不要声明overflow:auto让容器自适应滚动,这样做有可能因为其他格式化上下文的影响而导致自适应滚动失效或产生其他未知效果。需在<main>内插入一个<div>并声明如下。
div {
overflow: hidden;
height: 100%;
}
9.22 多列布局
9.221 两列布局
经典的两列布局由左右两列组成,其特点为一列宽度固定、另一列宽度自适应和两列高度固定且相等。以下以左列宽度固定和右列宽度自适应为例,反之同理。
<div class="two-column-layout">
<div class="left"></div>
<div class="right"></div>
</div>
float + margin-left/right
左列声明float:left和固定宽度,由于float使节点脱流,右列需声明margin-left为左列宽度,以保证两列不会重叠。
.two-column-layout {
width: 400px;
height: 400px;
.left {
float: left;
width: 100px;
height: 100%;
background-color: #f66;
}
.right {
margin-left: 100px;
height: 100%;
background-color: #66f;
}
}
overflow + float
左列声明同上,右列声明overflow:hidden使其形成BFC区域与外界隔离。BFC相关详情请查看小册第4章盒模型。
.two-column-layout {
width: 400px;
height: 400px;
.left {
float: left;
width: 100px;
height: 100%;
background-color: #f66;
}
.right {
overflow: hidden;
height: 100%;
background-color: #66f;
}
}
flex
使用flex实现会更简洁。左列声明固定宽度,右列声明flex:1自适应宽度。
.two-column-layout {
display: flex;
width: 400px;
height: 400px;
.left {
width: 100px;
background-color: #f66;
}
.right {
flex: 1;
background-color: #66f;
}
}
9.222 三列布局
经典的三列布局由左中右三列组成,其特点为连续两列宽度固定、剩余一列宽度自适应和三列高度固定且相等。以下以左中列宽度固定和右列宽度自适应为例,反之同理。整体的实现原理与上述两列布局一致。
<div class="three-column-layout">
<div class="left"></div>
<div class="center"></div>
<div class="right"></div>
</div>
为了让右列宽度自适应计算,就不使用float + margin-left的方式了,若使用margin-left还得结合左中列宽度计算。
overflow + float
.three-column-layout {
width: 400px;
height: 400px;
.left {
float: left;
width: 50px;
height: 100%;
background-color: #f66;
}
.center {
float: left;
width: 100px;
height: 100%;
background-color: #66f;
}
.right {
overflow: hidden;
height: 100%;
background-color: #3c9;
}
}
flex
.three-column-layout {
display: flex;
width: 400px;
height: 400px;
.left {
width: 50px;
background-color: #f66;
}
.center {
width: 100px;
background-color: #66f;
}
.right {
flex: 1;
background-color: #3c9;
}
}
9.223 圣杯布局与双飞翼布局
经典的圣杯布局和双飞翼布局都是由左中右三列组成,其特点为左右两列宽度固定、中间一列宽度自适应和三列高度固定且相等。其实也是上述两列布局和三列布局的变体,整体的实现原理与上述N列布局一致,可能就是一些细节需注意。
圣杯布局和双飞翼布局在大体相同下也存在一点不同,区别在于双飞翼布局中间列需插入一个子节点。在常规的实现方式中也是在这个中间列里做文章,如何使中间列内容不被左右列遮挡。
-
相同
- 中间列放首位且声明其宽高占满父节点
- 被挤出的左右列使用
float和margin负值将其拉回与中间列处在同一水平线上
-
不同
- 圣杯布局:父节点声明
padding为左右列留出空位,将左右列固定在空位上 - 双飞翼布局:中间列插入子节点并声明
margin为左右列让出空位,将左右列固定在空位上
- 圣杯布局:父节点声明
圣杯布局float + margin-left/right + padding-left/right
由于浮动节点在位置上不能高于前面或平级的非浮动节点,否则会导致浮动节点下沉。因此在编写HTML结构时,将中间列节点挪到右列节点后面。
<div class="grail-layout-x">
<div class="left"></div>
<div class="right"></div>
<div class="center"></div>
</div>
.grail-layout-x {
padding: 0 100px;
width: 400px;
height: 400px;
.left {
float: left;
margin-left: -100px;
width: 100px;
height: 100%;
background-color: #f66;
}
.right {
float: right;
margin-right: -100px;
width: 100px;
height: 100%;
background-color: #66f;
}
.center {
height: 100%;
background-color: #3c9;
}
}
双飞翼布局float + margin-left/right
HTML结构大体同上,只是在中间列里插入一个子节点<div>。根据两者区别,CSS声明会与上述圣杯布局有一点点出入,可观察对比找出不同地方。
<div class="grail-layout-y">
<div class="left"></div>
<div class="right"></div>
<div class="center">
<div></div>
</div>
</div>
.grail-layout-y {
width: 400px;
height: 400px;
.left {
float: left;
width: 100px;
height: 100%;
background-color: #f66;
}
.right {
float: right;
width: 100px;
height: 100%;
background-color: #66f;
}
.center {
margin: 0 100px;
height: 100%;
background-color: #3c9;
}
}
圣杯布局/双飞翼布局flex
使用flex实现圣杯布局/双飞翼布局可忽略上述分析,左右两列宽度固定,中间列宽度自适应。
<div class="grail-layout">
<div class="left"></div>
<div class="center"></div>
<div class="right"></div>
</div>
.grail-layout {
display: flex;
width: 400px;
height: 400px;
.left {
width: 100px;
background-color: #f66;
}
.center {
flex: 1;
background-color: #3c9;
}
.right {
width: 100px;
background-color: #66f;
}
}
9.223 均分布局
经典的均分布局由多列组成,其特点为每列宽度相等和每列高度固定且相等。总体来说也是最简单的经典布局,由于每列宽度相等,所以很易找到合适的方式处理。
<div class="average-layout">
<div class="one"></div>
<div class="two"></div>
<div class="three"></div>
<div class="four"></div>
</div>
.one {
background-color: #f66;
}
.two {
background-color: #66f;
}
.three {
background-color: #f90;
}
.four {
background-color: #09f;
}
float + width
每列宽度声明为相等的百分比,若有4列则声明width:25%。N列就用公式100 / n求出最终百分比宽度,记得保留2位小数,懒人还可用width:calc(100% / n)自动计算呢。
.average-layout {
width: 400px;
height: 400px;
div {
float: left;
width: 25%;
height: 100%;
}
}
flex
使用flex实现会更简洁。节点声明display:flex后,生成的FFC容器里所有子节点的高度都相等,因为容器的align-items默认为stretch,所有子节点将占满整个容器的高度。每列声明flex:1自适应宽度。
.average-layout {
display: flex;
width: 400px;
height: 400px;
div {
flex: 1;
}
}
9.23 居中布局
居中布局由父容器与若干个子容器组成,子容器在父容器中横向排列或竖向排列且呈水平居中或垂直居中。
display:flex与margin:auto的强行组合。
<div class="center-layout">
<div></div>
</div>
.center-layout {
display: flex;
width: 400px;
height: 400px;
background-color: #f66;
div {
margin: auto;
width: 100px;
height: 100px;
background-color: #66f;
}
}
9.3 绘制三角的原理
盒模型从理论上来说是一个标准的矩形,很难将其联想到基于盒模型绘制一个三角形。当然存在一个叫clip-path的属性,可绘制三角形,鉴于其兼容性较差通常不会大范围使用它绘制三角形。
绘制一个边框分别为四种颜色的正方形。
<div class="triangle"></div>
.triangle {
border: 50px solid;
border-left-color: #f66;
border-right-color: #66f;
border-top-color: #f90;
border-bottom-color: #09f;
width: 200px;
height: 200px;
}
分别将width和height累减到0,发现正方形由四个不同颜色的等腰三角形组成。
.triangle {
border: 50px solid;
border-left-color: #f66;
border-right-color: #66f;
border-top-color: #f90;
border-bottom-color: #09f;
width: 0;
height: 0;
}
尝试将右边框颜色声明为透明,会发现右边框隐藏起来。
.triangle {
border: 50px solid;
border-left-color: #f66;
border-right-color: transparent;
border-top-color: #f90;
border-bottom-color: #09f;
width: 0;
height: 0;
}
同样原理,将上边框颜色和下边框颜色同时声明为透明,就会得到一个指向右边的三角形。
.triangle {
border: 50px solid;
border-left-color: #f66;
border-right-color: transparent;
border-top-color: transparent;
border-bottom-color: transparent;
width: 0;
height: 0;
}
可简写成以下代码。细心的同学可能还会发现三角形的宽是高的2倍,而高正好是边框宽度border-width。从中可得出一个技巧:若绘制三角形的方向为左右上下,则将四条边框颜色声明为透明且将指定方向的反方向的边框着色,即可得到所需方向的三角形。
.triangle {
border: 50px solid transparent;
border-left-color: #f66;
width: 0;
height: 0;
}
若绘制左上角、左下角、右上角或右下角的三角形,使用上述技巧就无法完成了。可稍微变通思维,其实指向左上角的三角形是由左边框和上边框组成,其他三角形也是如此。
.triangle {
border: 50px solid transparent;
border-left-color: #f66;
border-top-color: #f66;
width: 0;
height: 0;
}
基于上述原理,可得心应手绘制出左右上下、左上角、左下角、右上角和右下角的三角形了,再结合绝对定位(position/left/right/top/bottom)、边距(margin/margin-*)或变换(transform)调整位置即可。
.triangle {
border: 50px solid transparent;
width: 0;
height: 0;
&.left {
border-right-color: #f66;
}
&.right {
border-left-color: #f66;
}
&.top {
border-bottom-color: #f66;
}
&.bottom {
border-top-color: #f66;
}
&.left-top {
border-left-color: #f66;
border-top-color: #f66;
}
&.left-bottom {
border-left-color: #f66;
border-bottom-color: #f66;
}
&.right-top {
border-right-color: #f66;
border-top-color: #f66;
}
&.right-bottom {
border-right-color: #f66;
border-bottom-color: #f66;
}
}
9.4 完美极致的变量
变量又名自定义属性,指可在整个文档中重复使用的值。它由自定义属性--var和函数var()组成,var()用于引用自定义属性。使用变量能带来以下好处。
- 减少样式代码的重复性
- 增加样式代码的扩展性
- 提高样式代码的灵活性
- 增多一种CSS与JS的通讯方式
- 不用深层遍历DOM改变某个样式
同时变量也是浏览器原生特性,无需经过任何转译可直接运行,也是DOM对象,极大便利了CSS与JS间的联系。变量除了具备简洁性和复用性,在重构组件样式时能让代码更易控制,同时还隐藏了一个强大的技巧,那就是与calc()结合使用。
看看一个简单的例子。一个条形加载条通常由几条线条组成,每条线条对应一个存在不同时延的相同动画,通过时间差运行相同动画,从而产生加载效果。估计大部分同学可能会把代码编写成以下形式。
<ul class="strip-loading">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
.strip-loading {
display: flex;
justify-content: center;
align-items: center;
width: 200px;
height: 200px;
li {
border-radius: 3px;
width: 6px;
height: 30px;
background-color: #f66;
animation: beat 1s ease-in-out infinite;
& + li {
margin-left: 5px;
}
&:nth-child(2) {
animation-delay: 200ms;
}
&:nth-child(3) {
animation-delay: 400ms;
}
&:nth-child(4) {
animation-delay: 600ms;
}
&:nth-child(5) {
animation-delay: 800ms;
}
&:nth-child(6) {
animation-delay: 1s;
}
}
}
@keyframes beat {
0%,
100% {
transform: scaleY(1);
}
50% {
transform: scaleY(.5);
}
}
分析代码发现,每个<li>只是animation-delay不同,其余代码则完全相同,换成其他类似的List集合,那岂不是有10个<li>就写10个:nth-child(n)。显然这种方式不灵活也不易封装成组件,若能像JS那样封装成一个函数,并根据参数输出不同样式效果,那就更棒了。
对于HTML部分的修改,让每个<li>拥有一个自己作用域下的变量。对于CSS部分的修改,就需分析哪些属性是随着index递增而发生规律变化的,对规律变化的部分使用变量表达式代替即可。当然以下<li style="--line-index: n;"></li>可用React JSX或Vue Template的遍历语法编写。
<ul class="strip-loading">
<li style="--line-index: 1;"></li>
<li style="--line-index: 2;"></li>
<li style="--line-index: 3;"></li>
<li style="--line-index: 4;"></li>
<li style="--line-index: 5;"></li>
<li style="--line-index: 6;"></li>
</ul>
.strip-loading {
display: flex;
justify-content: center;
align-items: center;
width: 200px;
height: 200px;
li {
--time: calc((var(--line-index) - 1) * 200ms);
border-radius: 3px;
width: 6px;
height: 30px;
background-color: #f66;
animation: beat 1.5s ease-in-out var(--time) infinite;
& + li {
margin-left: 5px;
}
}
}
@keyframes beat {
0%,
100% {
transform: scaleY(1);
}
50% {
transform: scaleY(.5);
}
}
代码中的变量--line-index和--time使每个<li>拥有一个属于自己的作用域。例如第二个<li>,--line-index的值为2,--time的计算值为200ms,换成第三个<li>后这两个值又会不同了。这就是变量的作用范围所致(在当前节点块作用域及其子节点块作用域下有效)。
calc(var(--line-index) * 200ms)就像一个JS函数,在当前节点的作用域上读取相应的变量,从而计算出具体数值并交由浏览器初始化。从中可得出一个技巧:List集合里具备与索引递增相关的属性值都可用变量与calc()结合使用生成出来。
还记得小学时代学习圆周率的场景吗,曾经有学者将一个圆形划分为很多很小的矩形,若这些矩形划分得足够细,那么也可拼在一起变成一个圆形。
将圆形划分为360个小矩形且每个矩形相对于父容器绝对定位,声明transform-origin为center bottom将小矩形的变换基准变更为最底部最中间,每个小矩形按照递增角度顺时针旋转N度,就会形成一个圆形。此时按照递增角度调整小矩形的背景色相,就会看到意想不到的渐变效果了。
- 每个小矩形的递增角度:
--Θ:calc(var(--line-index) / var(--line-count) * 1turn) - 每个小矩形的背景色相:
filter:hue-rotate(var(--Θ)) - 每个小矩形的旋转角度:
transform:rotate(var(--Θ))
若将小矩形的尺寸和数量设置更细更多,整体的渐变效果就会更均匀。
<ul class="gradient-circular" style="--line-count: 360;">
<li style="--line-index: 1;"></li>
...
<li style="--line-index: 360;"></li>
<!-- 360个<li>,可用模板语法生成 -->
</ul>
.gradient-circular {
position: relative;
width: 4px;
height: 200px;
li {
--Θ: calc(var(--line-index) / var(--line-count) * 1turn);
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #3c9;
filter: hue-rotate(var(--Θ));
transform-origin: center bottom;
transform: rotate(var(--Θ));
}
}
9.5 灵活多变的障眼法
以下是整个带边框气泡对话框的拆解,整体由三部分组成:带边框圆角矩形、黑色三角形、橙色三角形。先将两个三角形错位叠加生成一个箭头状的图形,再将该图形叠加到带边框圆角矩形的右边,最后将黑色三角形着色成白色,就能得到上图的带边框气泡对话框了。
<div class="bubble-empty-box">iCSS</div>
.bubble-empty-box {
position: relative;
border: 2px solid #f90;
border-radius: 5px;
width: 200px;
height: 50px;
line-height: 46px;
text-align: center;
font-size: 20px;
color: #f90;
&::before {
position: absolute;
left: 100%;
top: 50%;
margin: -5px 0 0 2px;
border: 5px solid transparent;
border-left-color: #f90;
content: "";
}
&::after {
position: absolute;
left: 100%;
top: 50%;
margin-top: -4px;
border: 4px solid transparent;
border-left-color: #fff;
content: "";
}
}
整体实现思路就是一种障眼法,正确来说就是将图形错位叠加产生另一种效果,在平面设计中叫做占位叠加。有了这种设计思想,其实能使用CSS创造出很多意向不到的障眼法效果。
9.6 意向不到的内容插入
上述提到::before/::after必须结合content使用,那么content就真的只能插入普通字符串吗?content何止这么简单,以下推广几种少见但强大的内容插入技巧。通过这几种技巧,就能很方便地将读取到的数据动态插入到::before或::after中。
- 内容拼接
- 结合
attr()使用 - 结合
变量和计数器使用
<div class="state-ball" style="--offset: 0;">
<div class="wave"></div>
<div class="progress"></div>
</div>
.state-ball {
overflow: hidden;
position: relative;
padding: 5px;
border: 3px solid #3c9;
border-radius: 100%;
width: 150px;
height: 150px;
background-color: #fff;
&::before,
&::after {
position: absolute;
left: 50%;
bottom: 5px;
z-index: 9;
margin-left: -100px;
width: 200px;
height: 200px;
content: "";
}
&::before {
margin-bottom: calc(var(--offset) * 1.34px);
border-radius: 45%;
background-color: rgba(#fff, .5);
animation: rotate 10s linear -5s infinite;
}
&::after {
margin-bottom: calc(var(--offset) * 1.34px + 10px);
border-radius: 40%;
background-color: rgba(#fff, .8);
animation: rotate 15s infinite;
}
.wave {
position: relative;
border-radius: 100%;
width: 100%;
height: 100%;
background-image: linear-gradient(to bottom, #af8 13%, #3c9 91%);
}
.progress::after {
display: flex;
position: absolute;
left: 0;
top: 0;
z-index: 99;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
font-weight: bold;
font-size: 16px;
color: #09f;
content: counter(progress) "%";
counter-reset: progress var(--offset);
}
}
@keyframes rotate {
to {
transform: rotate(1turn);
}
}
十、 妙用CSS变量,让你的CSS变得更心动
10.1 认识
阮一峰老师写的教程《CSS变量教程》。
-
声明:
--变量名 -
读取:
var(--变量名, 默认值) -
类型
- 普通:只能用作
属性值不能用作属性名 - 字符:与字符串拼接
"Hello, "var(--name) - 数值:使用
calc()与数值单位连用var(--width) * 10px
- 普通:只能用作
-
作用域
- 范围:在
当前元素块作用域及其子元素块作用域下有效 - 优先级别:
内联样式 > ID选择器 > 类选择器 = 属性选择器 = 伪类选择器 > 标签选择器 = 伪元素选择器
- 范围:在
10.2 使用场景
其实CSS变量有一个特别好用的场景,那就是结合List元素集合使用。
10.21 条形加载条
<ul class="strip-loading flex-ct-x">
<li v-for="v in 6" :key="v" :style="`--line-index: ${v}`"></li>
</ul>
.strip-loading {
width: 200px;
height: 200px;
li {
--time: calc((var(--line-index) - 1) * 200ms);
border-radius: 3px;
width: 6px;
height: 30px;
background-color: #f66;
animation: beat 1.5s ease-in-out var(--time) infinite;
& + li {
margin-left: 5px;
}
}
}
代码中的变量--line-index和--time使每个<li>拥有一个属于自己的作用域。例如第2个<li>,--line-index的值为2,--time的计算值为200ms,换成第3个<li>后这两个值又会不同了。
这就是CSS变量的作用范围所致(在当前元素块作用域及其子元素块作用域下有效),因此在.strip-loading的块作用域下调用--line-index是无效的。
10.22 心形加载条
<div class="heart-loading flex-ct-x">
<ul style="--line-count: 9;">
<li v-for="v in 9" :key="v" :class="`line-${v}`" :style="`--line-index: ${v}`"></li>
</ul>
</div>
.heart-loading {
width: 200px;
height: 200px;
ul {
display: flex;
justify-content: space-between;
width: 150px;
height: 10px;
}
li {
--Θ: calc(var(--line-index) / var(--line-count) * .5turn);
--time: calc((var(--line-index) - 1) * 40ms);
border-radius: 5px;
width: 10px;
height: 10px;
background-color: #3c9;
filter: hue-rotate(var(--Θ));
animation-duration: 1s;
animation-delay: var(--time);
animation-iteration-count: infinite;
}
.line-1,
.line-9 {
animation-name: line-move-1;
}
.line-2,
.line-8 {
animation-name: line-move-2;
}
.line-3,
.line-7 {
animation-name: line-move-3;
}
.line-4,
.line-6 {
animation-name: line-move-4;
}
.line-5 {
animation-name: line-move-5;
}
}
10.23 悬浮跟踪按钮
<a class="track-btn pr tac" @mousemove="move">
<span>妙用CSS变量,让你的CSS变得更心动</span>
</a>
.track-btn {
display: block;
overflow: hidden;
border-radius: 100px;
width: 400px;
height: 50px;
background-color: #66f;
line-height: 50px;
cursor: pointer;
font-weight: bold;
font-size: 18px;
color: #fff;
span {
position: relative;
}
&::before {
--size: 0;
position: absolute;
left: var(--x);
top: var(--y);
width: var(--size);
height: var(--size);
background-image: radial-gradient(circle closest-side, #09f, transparent);
content: "";
transform: translate3d(-50%, -50%, 0);
transition: all 200ms ease;
}
&:hover::before {
--size: 400px;
}
}
export default {
name: "track-btn",
methods: {
move(e) {
const x = e.pageX - e.target.offsetLeft;
const y = e.pageY - e.target.offsetTop;
e.target.style.setProperty("--x", `${x}px`);
e.target.style.setProperty("--y", `${y}px`);
}
}
};