0%

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

数据类型分类

JS的数据类型分为两大类,一类是原始类型(primitive type),一类是对象类型(object
type)。

原始类型

原始类型又称为基本类型,分为 Number , String , Boolean , Undefined , Null 几类。比较特殊的是, undefined 是 Undefined 类型中的唯一一个值;同样地, null 是 Null 类型中的唯一一个值。

ES6 引入了一个比较特殊的原始类型 Symbol ,用于表示一个独一无二的值。Symbol是原始类型,不是对象类型。因为Symbol 是没有构造函数 constructor 的,不能通过new Symbol() 获得实例。

但是获取 symbol 类型的值是通过调用Symbol 函数得到的。

1
const symbol1 = Symbol('Tusi')

Symbol 值是唯一的,所以下面的等式是不成立的。

1
Symbol(1) === Symbol(1) // false

对象类型

对象类型也叫引用类型,简单地理解呢,对象就是键值对 key:value 的集合。常见的对象类型有 Object , Array , Function , Date , RegExp 等。

JS还有全局对象。全局对象并不意味着它就是一种对象类型。比如JSON是一个全局对象,但是它不是一个对象类型。

对象可以 new 出来,所以对象类型都有构造函数, Object 类型对应的构造函数是Object() , Array 类型对应的构造函数是 Array()。

1
2
3
var obj = new Object() // 不过我们⼀般也不会这么写⼀个普通对象
var arr1 = new Array(1) // 创建⼀个length是1的空数组
var arr2 = new Array(1, 2) // 创建数组[1, 2]

栈内存和堆内存

栈内存的优势是存取速度比堆内存快,考虑这一点可以优化代码性能。

栈内存

原始类型是按值访问的,其值存储在栈内存中,所占内存大小是已知的或是有范围的;

对基本类型变量的重新赋值,其本质上是进行压栈操作,写入新的值,并让变量指向一块栈顶元素。

1
2
var a = 1; // 压栈,1成为栈顶元素,其值赋给变量a
a = 2; // 压栈,2成为栈顶元素,并赋值给变量a(内存地址变了)

堆内存

对象类型是按引用访问的,通过指针访问对象。
指针是一个地址值,类似于基本类型,存储于栈内存中,是变量访问对象的中间媒介。
对象本身存储在堆内存中,其占用内存大小是可变的,未知的。

举例:

1
var b = { name: 'Tusi' }

运行这行代码,会在堆内存中开辟一段内存空间,存储对象 {name: ‘Tusi’} ,同时声明一个指针,其值为上述对象的内存地址,指针赋值给引用变量 b ,意味着 b 引用了上述对象。

对象可以新增或删除属性,所以说对象类型占用的内存大小一般是未知的。

1
b.age = 18; // 对象新增了age属性

按引用访问是对引用变量进行对象操作,其本质上改变的是引用变量所指向的堆内存地址中的对象本身。

如果有两个或两个以上的引用变量指向同一个对象,那么对其中一个引用变量的对象操作,会影响指向该对象的其他引用变量。

1
2
3
4
5
var b = { name: 'Tusi' }; // 创建对象,变量b指向该对象
var c = b; // 声明变量c,指向与b⼀致
b.age = 18; // 通过变量b修改对象
// 产⽣副作⽤,c受到影响
console.log(c); // {name: "Tusi", age: 18}

考虑到对象操作的副作用,我们会在业务代码中经常使用深拷贝来规避这个问题。

数据类型的判断

typeof

typeof操作符返回一个字符串,表示未经计算的操作数的类型。

数据类型 运算结果
Undefined “undefined”
Null “object”
Boolean “boolean”
Number “number”
String “string”
Symbol “symbol”
Function “function”
其他对象 “object”
宿主对象(由JS环境提供,如Nodejs有global,浏览器有window) 取决于具体实现
  1. typeof null的结果也是"object"
  2. 对象的种类很多,typeof得到的结果无法判断出数组,普通对象,其他特殊对象

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

利用instanceof,我们可以判断一个对象是不是某个构造函数的实例。那么结合typeof,我们可以封装一个基本的判断数据类型的函数。

基本思想是:首先看typeof是不是返回"object",如果不是,说明是普通数据类型,那么直接返回typeof运算结果即可;如果是,则需要先把null这个坑货摘出来,然后依次判断其他对象类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function getType(val) {
const type = typeof val;
if (type === 'object') {
if (val === null) {
// null不是对象,所以不能用instanceof判断
return 'null'
} else if (val instanceof Array) {
return 'array'
} else if (val instanceof Date) {
return 'date'
} else if (// 其他对象的instanceof判断) {
return 'xxx'
} else if (val instanceof Object) {
// 所有对象都是Object的实例,所以放最后
return 'object'
}
} else {
return type
}
}
// 测试下
getType(Symbol(1)) // "symbol"
getType(null) // "null"
getType(new Date()) // "date"
getType([1, 2, 3]) // "array"
getType({}) // "object"

toString

Object.prototype.toString。也可以判断ES6引入的一些新的对象类型,比如MapSet等。

1
2
3
4
5
6
7
8
// 利用了Object.prototype.toString和正则表达式的捕获组
function getType(val) {
return Object.prototype.toString.call(val).replace(/\[object\s(\w+)\]/, '$1').toLowerCase();
}

getType(new Map()) // "map"
getType(new Set()) // "set"
getType(new Promise((resolve, reject) => {})) // "promise"

为什么普通的调用toString不能判断数据类型,而Object.prototype.toString可以呢?

因为Object是基类,而各个派生类,如Date, Array等在继承Object的时候,一般都重写(overwrite)了toString方法,用以表达自身业务,从而失去了判断类型的能力。

装箱和拆箱

把原始类型转换为对应的对象类型的操作称为装箱,反之是拆箱。

装箱

只有对象才可以拥有属性和方法,但是我们在使用一些基本类型数据的时候,却可以直接调用它们的一些属性或方法,这是怎么回事呢?

1
2
3
4
5
6
var a = 1;
a.toFixed(2); // "1.00"

var b = 'I love study';
b.length; // 12
b.substring(2, 6); // "love"

其实在读取一些基本类型数据的属性或方法时,javascript会创建临时对象(也称为“包装对象”),通过这个临时对象来读取属性或方法。以上代码等价于:

1
2
3
4
5
6
7
8
9
var a = 1;
var aObj = new Number(a);
aObj.toFixed(2); // "1.00"

var b = 'I love study';
var bObj1 = new String(b);
bObj1.length; // 12
var bObj2 = new String(b);
bObj2.substring(2, 6); // "love"

临时对象是只读的,可以理解为它们在发生读操作后就销毁了,所以不能给它们定义新的属性,也不能修改它们现有的属性。

1
2
3
4
5
6
var c = '123';
c.name = 'jack'; // 给临时对象加新属性是无效的
c.name; // undefined
c.length; // 3
c.length = 2; // 修改临时对象的属性值,是无效的
c.length; // 3

我们也可以显示地进行装箱操作,即通过String()Number()Boolean()构造函数来显示地创建包装对象。

1
2
var b = 'I love study';
var bObj = new String(b);

拆箱

对象的拆箱操作是通过valueOftoString完成的。

类型的转换

javascript在某些场景会自动执行类型转换操作,而我们也会根据业务的需要进行数据类型的转换。类型的转换规则如下:

202208311703006.png

对象到原始值的转换

toString

toString()是默认的对象到字符串的转换方法。

1
2
var a = {};
a.toString(); // "[object Object]"

但是很多类都自定义了toString()方法,举例如下:

  • Array:将数组元素用逗号拼接成字符串作为返回值。
1
2
var a = [1, 2, 3];
a.toString(); // 1,2,3
  • Function:返回一个字符串,字符串的内容是函数源代码。
  • Date:返回一个日期时间字符串。
1
2
var a = new Date();
a.toString(); // "Sun May 10 2020 11:19:29 GMT+0800 (中国标准时间)"
  • RegExp:返回表示正则表达式直接量的字符串。
1
2
var a = /\d+/;
a.toString(); // "/\d+/"

valueOf

valueOf()会默认地返回对象本身,包括ObjectArrayFunctionRegExp

日期类Date重写了valueOf()方法,返回一个1970年1月1日以来的毫秒数。

1
2
var a = new Date();
a.toString(); // 1589095600419

对象–>布尔值

从上表可见,对象(包括数组和函数)转换为布尔值都是true

对象–>字符串

对象转字符串的基本规则如下:

  • 如果对象具有toString()方法,则调用这个方法。如果它返回字符串,则作为转换的结果;如果它返回其他原始值,则将原始值转为字符串,作为转换的结果。
  • 如果对象没有toString()方法,或toString()不返回原始值(不返回原始值这种情况好像没见过,一般是自定义类的toString()方法吧),那么javascript会调用valueOf()方法。如果存在valueOf()方法并且valueOf()方法返回一个原始值,javascript将这个值转换为字符串(如果这个原始值本身不是字符串),作为转换的结果。
  • 否则,javascript无法从toString()valueOf()获得一个原始值,会抛出异常。

对象–>数字

与对象转字符串的规则类似,只不过是优先调用valueOf()

  • 如果对象具有valueOf()方法,且valueOf()返回一个原始值,则javascript将这个原始值转换为数字(如果原始值本身不是数字),作为转换结果。
  • 否则,如果对象有toString()方法且返回一个原始值,javascript将这个原始值转换为数字,作为转换结果。
  • 否则,javascript将抛出一个类型错误异常。

显式转换

使用String()Number()Boolean()函数强制转换类型。

1
2
3
var a = 1;
var b = String(a); // "1"
var c = Boolean(a); // true

隐式转换

加法运算符+

因为加法运算符+可以用于数字加法,也可以用于字符串连接,所以加法运算符的两个操作数可能是类型不一致的。

  • 如果其中一个运算符是对象,则会遵循对象到原始值的转换规则,对于非日期对象来说,对象到原始值的转换基本上是对象到数字的转换,所以首先调用valueOf(),然而大部分对象的valueOf()返回的值都是对象本身,不是一个原始值,所以最后也是调用toString()去获得原始值。对于日期对象来说,会使用对象到字符串的转换,所以首先调用toString()
1
2
1 + {}; // "1[object Object]"
1 + new Date(); // "1Sun May 10 2020 22:53:24 GMT+0800 (中国标准时间)"
  • 在进行了对象到原始值的转换后,如果加法运算符+的其中一个操作数是字符串的话,就将另一个操作数也转换为字符串,然后进行字符串连接。
1
2
3
var a = {} + false; // "[object Object]false"

var b = 1 + []; // "1"
  • 否则,两个操作数都将转换为数字(或者NaN),然后进行加法操作。
1
2
3
4
5
var a = 1 + true; // 2

var b = 1 + undefined; // NaN

var c = 1 + null; // 1

[] == ![]

[] == ![],其结果是true

  1. 首先,我们要知道运算符的优先级是这样的,一元运算符!的优先级高于关系运算符==

    202208311745552.png

  2. 所以,右侧的![]首先会执行,而逻辑非运算符!会首先将其操作数转为布尔值,再进行求反。[]转为布尔值是true,所以![]的结果是false。此时的比较变成了[] == false

  3. 根据比较规则,如果==的其中一个值是false,则将其转换为数字0,再与另一个操作数比较。此时的比较变成了[] == 0

  4. 接着,再参考比较规则,如果一个值是对象,另一个值是数字或字符串,则将对象转为原始值,再进行比较。左侧的[]转为原始值是空字符串"",所以此时的比较变成了"" == 0

  5. 最后,如果一个值是数字,另一个是字符串,先将字符串转换为数字,再进行比较。空字符串会转为数字000自然是相等的。

也可以分析下为什么{} == !{}的结果是false了。

缓存就是,当第一次访问网站的时候,电脑会把图片和数据下载到电脑上,再次访问时,网站就会直接加载出来。

缓存的好处:

  1. 缓解服务器压力,不用每次都去请求某些数据了。
  2. 提升性能,打开本地资源肯定会比请求服务器来的快。
  3. 减少带宽消耗,当我们使用缓存时,只会产生很小的网络消耗,至于为什么打开本地资源也会产生网络消耗,下面会有说明。

Web 缓存种类: 数据库缓存,CDN 缓存,代理服务器缓存,浏览器缓存。

所谓浏览器缓存其实就是指在本地使用的计算机中开辟⼀个内存区,同时也开辟一个硬盘区作为数据传输的缓冲区,然后用这个缓冲区来暂时保存用户以前访问过的信息。

浏览器缓存过程:强缓存,协商缓存。

浏览器缓存位置⼀般分为四类: Service Worker–>Memory Cache–>Disk Cache–>Push Cache。

强缓存

强缓存是当我们访问 URL 的时候,不会向服务器发送请求,直接从缓存中读取资源,但是会返回200 的状态码。

如何设置强缓存?

我们第⼀次进入页面,请求服务器,然后服务器进行应答,浏览器会根据 response Header 来判断是否对资源进行缓存,如果响应头中 expires、pragma 或者 cache-control 字段,代表这是强缓存,浏览器就会把资源缓存在 memory cache 或 disk cache 中。

第二次请求时,浏览器判断请求参数,如果符合强缓存条件就直接返回状态码 200,从本地缓存中拿数据。否则把响应参数存在 request header 请求头中,看是否符合协商缓存,符合则返回状态码 304,不符合则服务器会返回全新资源。

202208301502686.png

expires

是 HTTP1.0 控制网页缓存的字段,值为一个时间戳,准确来讲是格林尼治时间,服务器返回该请求结果缓存的到期时间,意思是,再次发送请求时,如果未超过过期时间,直接使用该缓存,如果过期了则重新请求。

有个缺点,就是它判断是否过期是用本地时间来判断的,本地时间是可以自己修改的。

Cache-Control

是 HTTP1.1 中控制网页缓存的字段,当 Cache-Control 都存在时,Cache-Control 优先级更高,主要取值为:

public:资源客户端和服务器都可以缓存。
privite:资源只有客户端可以缓存。
no-cache:客户端缓存资源,但是是否缓存需要经过协商缓存来验证。
no-store:不使用缓存。
max-age:缓存保质期。

202208301526543.png

Cache-Control 使用了 max-age 相对时间,解决了 expires 的问题。

pragma

这个是 HTTP1.0 中禁用网页缓存的字段,其取值为 no-cache,和 Cache-Control 的 no-cache效果⼀样。

202208301530413.png

缓存位置

什么资源放在memory cache,什么资源放在 disk cache 中?

202208301535610.png

存储图像和网页等资源主要缓存在 disk cache,操作系统缓存文件等资源大部分都会缓存在memory cache 中。具体操作浏览器自动分配,看谁的资源利用率不高就分给谁。

memory cache 请求时间都是 0ms

查找浏览器缓存时会按顺序查找: Service Worker–>Memory Cache–>Disk Cache–>Push Cache。

Service Worker

是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker 的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Memory Cache

内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。

读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。⼀旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

Disk Cache

存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。

在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源⼀旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自Disk Cache。

memory cache 要比 disk cache 快的多。举个例子:从远程 web 服务器直接提取访问文件可能需要 500 毫秒 (半秒),那么磁盘访问可能需要 10-20 毫秒,而内存访问只需要 100 纳秒,更高级的还有 L1 缓存访问 (最快和最小的 CPU 缓存) 只需要 0.5 纳秒。

202208301604901.png

prefetch cache (预取缓存)

link 标签上带了 prefetch,再次加载会出现。

prefetch 是预加载的一种方式,被标记为 prefetch 的资源,将会被浏览器在空闲时间加载。

Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。
它只在会话(Session)中存在,⼀旦会话结束就被释放,并且缓存时间也很短暂,在Chrome 浏览器中只有 5 分钟左右,同时它也并非严格执行HTTP 头中的缓存指令。

CPU、内存、硬盘

CPU、内存、硬盘都是计算机的主要组成部分。

CPU:中央处理单元 (CentralProcessingUnit) 的缩写,也叫处理器,是计算机的运算核心和控制核心。电脑靠 CPU 来运算、控制。让电脑的各个部件顺利工作,起到协调和控制作用。

硬盘:存储资料和软件等数据的设备,有容量大,断电数据不丢失的特点。

内存:负责硬盘等硬件上的数据与 CPU 之间数据交换处理。特点是体积小,速度快,有电可存,无电清空,即电脑在开机状态时内存中可存储数据,关机后将自动清空其中的所有数据。

协商缓存

协商缓存就是强缓存失效后,浏览器携带缓存标识向服务器发送请求,由服务器根据缓存标识来决定是否使用缓存的过程。

以下两种情况:

协商缓存生效,返回 304
202208301638199.png

协商缓存失效,返回 200 和请求结果
202208301640340.png

如何设置协商缓存?

Last-Modified/If-Modified-Since

Last-Modified 是服务器响应请求时,返回该资源文件在服务器最后被修改的时间。

202208301647514.png

If-Modified-Since 则是客户端再次发起该请求时,携带上次请求返回的 Last-Modified 值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有 If-Modified-Since 字段,则会根据 If-Modified-Since 的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于 If-Modified-Since 的字段值,则重新返回资源,状态码为 200;否则则返回 304,代表资源无更新,可继续使用缓存文件。

202208301654404.png

Etag/If-None-Match

Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识 (由服务器生成)。

202208301658330.png

If-None-Match 是客户端再次发起该请求时,携带上次请求返回的唯⼀标识 Etag 值,通过此字段值告诉服务器该资源上次请求返回的唯⼀标识值。服务器收到该请求后,发现该请求头中含有 If-None-Match,则会根据 If-None-Match 的字段值与该资源在服务器的 Etag 值做对比,一致则返回 304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为 200。

202208301700729.png

Etag / If-None-Match 优先级高于 Last-Modified / If-Modified-Since,同时存在则只有Etag / If-None-Match 生效。

缓存方案

目前的项目大多使用这种缓存方案的:
HTML: 协商缓存;
css、js、图片:强缓存,文件名带上 hash。

强缓存与协商缓存的区别

  1. 强缓存不发请求到服务器,所以有时候资源更新了浏览器还不知道,但是协商缓存会发请求到服务器,所以资源是否更新,服务器肯定知道。
  2. 大部分 web 服务器都默认开启协商缓存。

刷新对于强缓存和协商缓存的影响

  1. 当 ctrl+f5 强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存。
  2. 当 f5 刷新网页时,跳过强缓存,但是会检查协商缓存。
  3. 浏览器地址栏中写入 URL,回车浏览器发现缓存中有这个文件了,不用继续请求了,直接去缓存拿。(最快)

前言

优先级是基于不同种类选择器组成的匹配规则。浏览器通过优先级来判断哪些样式与一个元素最为相关,从而在该元素上应用这些样式。

CSS选择器的分类

不同种类选择器的用法

基本选择器以外的选择器的用法。

属性选择器

通过已经存在的属性名或属性值匹配元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<style>
div { margin-top: 20px;}
/* 带有属性 title 的元素 */
[title]{ background: #faf3e0; }
/* 带有属性 class 且值为 div1 的元素 */
[class=div1]{ background: #eabf9f; }
/* 带有属性 attr 并且该属性是一个以空格作为分隔的值列表,其中至少有一个值为 attr-test2 的元素 */
[attr~=attr-test2]{ background: #b68973; }
/* 带有属性 attr 且值以 te 为开头的元素 */
[attr^=te]{ background: #f39189; }
/* 带有属性 attr 且值以 Test 为结尾的元素 */
[attr$=Test]{ background: #bb8082; }
/* 带有属性 attr 且值包含 test3 的元素 */
[attr*=test3]{ background: #6e7582; }
/* 带有属性 attr 且值为 attr1 或以 attr1- 开头 */
[attr|=attr1]{ background: #046582; }
/* 属性选择器默认区分大小写,在属性选择器的右方括号前添加一个用空格隔开的字母 i(或 I),可忽略大小写 */
[attr*=test5 i]{ background: #865858; }
</style>
<body>
<div title='helloWorld'>[title]</div>
<div class="div1">[class=div1]</div>
<div attr='attr-test1 attr-test2'>[attr~=attr-test2]</div>
<div attr='test'>[attr^=te]</div>
<div attr='attrTest'>[attr$=Test]</div>
<div attr='attr-test3'>[attr*=test3]</div>
<div attr='attr1-test4'>[attr|=attr1]</div>
<div attr='attr-Test5'>attr*=test5 i</div>
</body>

伪类选择器

动态伪类(多用于超链接的样式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style>
/* 未访问的链接 */
a:link { color: #11698e; }
/* 已访问过的链接 注:只要是当前浏览器有访问记录的都算作已访问的状态 */
a:visited {color: #9fb8ad; }
/* 当鼠标悬浮在元素上方时 */
/* 注: 为了使点击过后的链接仍然受设置的伪类样式影响,在CSS 定义时 :hover 需设置在 :link 和 :visited 之后 */
a:hover{ color: #383e56; }
/* 被激活的元素 (鼠标点下且为松开时) */
/* 注: 为了使点击过后的链接仍然受设置的伪类样式影响,在CSS 定义时 :active 需设置在 :hover 之后 */
a:active{ color: #fb743e; }
</style>
<body>
<a target="_blank" href='https://juejin.cn/user/3456520257288974'>超链接</a>
</body>

伪类的active在IOS下存在兼容问题。

目标伪类、否定伪类、语言伪类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<style>
html { font-size: 14px; }
/* 目标伪类 :target: 代表一个唯一的页面元素(目标元素),其 id 与当前URL片段匹配 */
div:target { color: #f05454; }
/* 否定伪类 :not 注: 仅 Chrome、Firefox 和 Safari 高版本浏览器适用*/
p:not(#p1){ color: #e27802; }
/* 语言伪类 :lang */
div:lang(zh) { color: #ffc1b6; }
</style>
<body>
<h3>目标伪类 :target</h3>
<div id="div1">目标伪类: 这是 div1</div>
<div id="div2">目标伪类: 这是 div2</div>
<h3>否定伪类 :not</h3>
<p id="p1">否定伪类: 这是 p1</p>
<p id="p2">否定伪类: 这是 p2</p>
<h3>语言伪类 :lang</h3>
<div lang="en">语言伪类: 这是 en</div>
<div lang="zh">语言伪类: 这是 zh</div>
</body>

结构伪类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<style>
/* 父元素的第一个子元素且该子元素为 p 的元素 */
p:first-child { background: #046582; }
/* 父元素中第一个 span 元素 */
span:first-of-type { background: #6e7582; }
/* 父元素中第 2n 个子元素且为 p 的元素 */
p:nth-child(2n) { background: #bb8082; }
/* 父元素中第 2n 个 span 元素 */
span:nth-of-type(2n) { background: #f39189; }
/* 父元素有且仅有一个为 i 的元素 */
i:only-child { background: #865858; }
/* 父元素有且仅有一个为 strong 的元素 */
strong:only-of-type { background: #8e7f7f; }
/* 没有子元素的元素 */
p:empty { height: 16px; background: #bbb; }
/* 根元素 HTML 中相当于 <html> */
:root { background: #e2d5d5; color: #fff; }
</style>
<body>
<div>
<p class="p1">这是 p1</p>
<p class="p2">这是 p2</p>
<p class="p3"><i>这是 p3</i></p>
<p class="p4">这是 p4</p>
<span class="span1">这是 span1</span>
<span class="span2">这是 span2</span>
<p class="empty-p p5">这是 p5</p>
<strong>这是 strong</strong>
</div>
</body>

UI元素伪类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<style>
/* :enabled 可用状态 */
input[type="radio"]:enabled { box-shadow: 0 0 0 3px #7c9473; }
/* :disabled 禁用状态 */
input[type="radio"]:disabled { box-shadow: 0 0 0 3px #cfdac8; cursor: not-allowed; }
/* :checked radio 或 checkbox 表单被勾选状态 */
/* 注意书写顺序,选择元素相同时 :checked 应写在 :enabled/:disabled 后面 */
input[type="radio"]:checked { box-shadow: 0 0 0 3px #c0e218 ; }
/* :default 表示一组相关元素中的默认(选中)表单元素 此处 :default 应用于默认设置了 checked 的 radio 表单上 */
/* 该选择器可以在 <button>, <input type="checkbox">, <input type="radio">, 以及 <option> 上使用 */
input[type="radio"]:default { box-shadow: 0 0 0 3px #86aba1;}
/* :read-only 只读状态 */
input:read-write { background: #7c9473; }
/* :read-only 只读状态 */
input:read-only { background: #cfdac8; }
</style>
<body>
<div>
<input type="radio" name="my-radio" id="radio1" checked>
<label for="radio1">默认选中</label>
<input type="radio" name="my-radio" id="radio2">
<label for="radio2">未选中-可用</label>
<input type="radio" name="my-radio" id="radio1" disabled>
<label for="radio1">未选中-禁用</label>
</div>
<div>
<input type="input" name="my-input" id="input1" value="input1">
<input type="input" name="my-input" id="input2" value="input2" readonly>
</div>
</body>

伪元素选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<style>
div { margin-left: 50px;}
/* ::after 在选中元素的最后添加一个子元素,默认为行内元素 (替换元素上不生效) */
.div1::after { content: 'div1 的 after'; margin-left: 10px; color: #ef4f4f }
/* ::before 在选中元素的第一个位置添加一个子元素 (其他用法同 ::after) */
.div2::before { content: 'div2 的 before'; margin-right: 10px; color: #ee9595 }
/* ::first-letter 匹配选中块级元素的第一行的第一个字符 */
.div3::first-letter { color: #ff4646 }
/* ::first-line 匹配选中块级元素的第一行 */
.div4::first-line { color: #9dab86 }
/* ::marker 匹配选中有序或无序列表的序号或符号 */
.div5 ul li::marker { color: #fdb827 }
/* ::selection 匹配元素中被选中高亮的部分 */
.div6::selection { background: #9dab86; color: white }
</style>
<body>
<div class="div1">div1</div>
<div class="div2">div2</div>
<div class="div3">div3</div>
<div class="div4">div4第一行<br>div4第二行</div>
<div class="div5">div5
<ul>
<li>item1</li>
<li>item2</li>
<li>item3</li>
</ul>
</div>
<div class="div6">div6</div>
</body>

一个选择器中只能使用一个伪元素
CSS3 中伪元素应该用双冒号,以便区分伪元素和伪类。但是旧版的规范未做明确区分,所以大多数浏览器中支持部分伪元素使用单双冒号两种写法。

组合选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<style>
/* 后代选择器 空格隔开 匹配所有符合的后代元素*/
div span { margin-left: 10px; background: #ff8585 }
/* 直接子后代选择器 > 连接; 匹配符合的直接子元素; 不包括子元素的子元素 */
.div1>span { color: #6155a6 }
/* 群组选择器 逗号隔开 */
.div1, .div2 { color: #a7c5eb }
/* 相邻兄弟元素选择器 + 连接; 匹配某元素后紧邻的兄弟元素 */
.div3 + div { color: #fd3a69 }
/* 兄弟选择器 ~ 连接; 匹配某元素后所有同级的指定元素,强调的是所有的 */
.div5 ~ div { color: #008891 ; }
</style>
<body>
<div class="div1">div1
<span class="span1">span1
<span class="span1-1">span1-1</span>
</span>
</div>
<div class="div2">div2<span class="span2">span2</span></div>
<div class="div3">div3<span class="span3">span3</span></div>
<div class="div4">div4<span class="span4">span4</span></div>
<div class="div5">div5<span class="span5">span5</span></div>
<div class="div6">div6<span class="span6">span6</span></div>
<div class="div7">div7<span class="span7">span7</span></div>
<div class="div8">div8<span class="span8">span8</span></div>
</body>

CSS选择器优先级

基本选择器的优先级是:!important > 内联 > ID 选择器 > 类选择器 > 标签选择器 > 通用选择器。那么它如何计算的呢?有这样一个计算表格。

选择器 示例 权重值
!impotant color: #fff !important; 正无穷
内联选择器 样式作用元素 1 0 0 0
ID 选择器 #id 1 0 0
类选择器、属性选择器、伪类选择器 .class 1 0
标签选择器、伪元素选择器 div 1
通用选择器 * 0
继承属性 样式作用元素 -1

假设在一个拍卖会上,有以下几种筹码:

  • 一个内联样式相当于一千元
  • 一个ID 选择器相当于一百元
  • 一个类选择器相当于十元
  • 一个标签选择器相当于一元
  • 通用选择器为零元

每出现一个上述选择器,就增加对应筹码的钱数,最终采用出钱最多的样式。但是,这里的钱数计算方法和生活中的计算方法不一样,这里的”单位”不会因为值的累加而进行换算。例如十个一百只能是“十百“,仍然小于一千。

基本选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<style>
* { color: #31326f } /* 通用 */
div { background: #9ddfd3 } /* 标签 */
.div-class { background: #dbf6e9 } /* 类 */
.p1, .p2, .p3 { background: #ffdada; color: #060930 } /* 类 */
#p1-1 { background: #595b83 } /* id */
.p3 { color: #595b83 !important } /* !important */
</style>
<body>
<div class="div-class">
<h1>Hello word!</h1>
<p class="p1" id="p1-1">Hello word!</p>
<p class="p2" style="color: white">Hello word!</p> <!-- 内联样式 -->
<p class="p3" style="color: white">Hello word!</p> <!-- 内联样式 -->
</div>
</body>

其他选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<style>
div { color: #a20a0a }
div .p1 ~ p { background: #fceef5; color: #ffa36c } /* 总权重值: 12 */
div p ~ p { background: pink; } /* 总权重值: 3 */
.div-class .p1 ~ p { color: #799351 } /* 总权重值: 21 */
/* 以下两行样式若互换位置,则 .p1 的 ::after 文本颜色为 #333456 */
.div-class p::after { content:'p-after'; margin-left: 10px; color: #333456 } /* 总权重值: 0 0 1 2 */
div .p1::after { color: #42bfd8 } /* 总权重值: 12 */
/* 以下两行样式若互换位置,则 .p1 的文本颜色为 #92817a */
.p1 { color: #92817a }
[name="p1"] { color: #bedbbb }
/* 同一元素样式存在冲突且同时存在 !important */
div .span1 { color: #0e918c !important; }
.span1 { color: #6a097d !important; }
</style>
<body>
<div class="div-class">
<h1>Hello word!</h1>
<p class="p1" name="p1">p1</p>
<p class="p2">p2</p>
<p class="p3">p3</p>
<span class="span1">span1</span>
</div>
</body>

权重值相同时,写在后面的样式生效
!important 是 CSS 选择器中的一个 “流氓” 属性,不论选择器的权重或者优先级的高低,只要加上!important,那么这个样式的优先级就是最高的
如果针对同一元素样式存在冲突且同时存在!important ,那么选择器总权重值高者生效

1
2
3
4
5
6
7
8
9
10
<style>
div { max-width: 100px; }
div, p { background: #bedbbb }
.div1 { width: 200px !important; }
.p1 { width: 200px; }
</style>
<body>
<div class="div1"> 这是 div1</div>
<p class="p1"> 这是 p1</p>
</body>

对于一些互斥的样式,例如 max-width 与 width,选择器的权重值再高也是无能为力的。

总结

如你所见,CSS 选择器也是暗藏玄机的哦。不过到目前为止,暂时没有能够通过元素选择其父元素或其父元素相关元素的选择器,这就让人很是头疼。

语法

CSS的核心功能是将CSS属性设置为特定的值。一个属性与值的键值对称为声明。

1
color: red;

将声明用{}包起来后,就组成了声明块。

1
2
3
4
{
color: red;
text-align: center;
}

选择器和声明块组成了CSS规则集,常常简称CSS规则。

1
2
3
4
span {
color: red;
text-align: center;
}

CSS注释:

1
2
3
4
5
/* 单⾏注释 */
/*
    多⾏
    注释
*/

@规则

CSS包含的@规则:

@namespace告诉CSS引擎必须考虑XML命名空间

@media如果满足媒体查询条件则条件规则组里的规则生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* At the top level of your code */
@media screen and (min-width: 900px) {
article {
padding: 1rem 3rem;
}
}

/* Nested within another conditional at-rule */
@supports (display: flex) {
@media screen and (min-width: 900px) {
article {
display: flex;
}
}
}

@page描述打印文档时布局的变化

1
2
3
4
5
6
7
@page {
margin: 1cm;
}

@page :first {
margin: 2cm;
}

@font-face描述将下载的外部字体

@keyframes描述CSS动画的关键帧

1
2
3
4
5
6
7
8
9
@keyframes slidein {
from {
transform: translateX(0%);
}

to {
transform: translateX(100%);
}
}

@document文档样式表满足给定条件则条件规则组里的样式生效

@charset用于定义样式表使用的字符集,它必须是样式表的第一个元素。如果有多个@charset被声明,只有第一个被使用。

1
@charset "UTF-8";

样式表用什么编码,浏览器的识别程序:

  1. 文件开头的 Byte order mark 字符值,不过⼀般编辑器并不能看到文件头里的 BOM 值;

  2. HTTP 响应头里的 content-type 字段包含的 charset 所指定的值,比如:

    1
    Content-Type: text/css; charset=utf-8
  3. CSS文件头里定义的@charset规则里指定的字符编码

  4. 标签的charset属性,已在html5废除
  5. 默认是UTF-8

@import告诉CSS引擎引入一个外部样式表

link和@import都能导入样式文件,有什么区别:

  1. link 是 HTML 标签,除了能导入CSS 外,还能导入别的资源,比如图片、脚本和字体等;而@import 是 CSS 的语法,只能用来导入CSS;

  2. link 导入的样式会在页面加载时同时加载,@import 导入的样式需等页面加载完成后再加载;

  3. link没有兼容问题,@import不兼容ie5以及以下;

  4. link 可以通过 JS 操作 DOM 动态引入样式表改变样式,而@import不可以。

@support用于查询特定的CSS是否生效,可以结合not、and和or操作符决定后续的操作。

1
2
3
4
5
6
7
8
9
10
11
@supports (display: grid) {
div {
display: grid;
}
}

@supports not (display: grid) {
div {
float: right;
}
}

层叠性

越往下优先级越高:

  1. 用户代理样式表中的声明(浏览器的默认样式,没有设置其他样式时使用)

  2. 作者样式表中的常规声明(开发人员设置的样式)

  3. 作者样式表中的!important声明

针对同一个选择器,定义在后面的声明会覆盖前面的;作者定义的样式会比默认继承的样式优先级更高。

选择器

基础选择器

标签选择器:h1

类选择器:.checked

ID选择器:#picker

通配选择器:*

属性选择器

[attr] :指定属性的元素;
[attr=val] :属性等于指定值的元素;
[attr*=val] :属性包含指定值的元素;
[attr^=val] :属性以指定值开头的元素;
[attr$=val] :属性以指定值结尾的元素;
[attr~=val] :属性包含指定值(完整单词)的元素(不推荐使用);
[attr|=val] :属性以指定值(完整单词)开头的元素(不推荐使用);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 存在 title 属性的<a> 元素 */
a[title] {
color: purple;
}

/* 存在 href 属性并且属性值匹配"https://example.org"的<a> 元素 */
a[href="https://example.org"] {
color: green;
}

/* 存在 href 属性并且属性值包含"example"的<a> 元素 */
a[href*="example"] {
font-size: 2em;
}

/* 存在 href 属性并且属性值结尾是".org"的<a> 元素 */
a[href$=".org"] {
font-style: italic;
}

/* 存在 class 属性并且属性值包含以空格分隔的"logo"的<a>元素 */
a[class~="logo"] {
padding: 2px;
}

组合选择器

相邻兄弟选择器: A + B
普通兄弟选择器: A ~ B
子选择器: A > B
后代选择器: A B

伪类

条件伪类

:lang() :基于元素语言来匹配页面元素;
:dir() :匹配特定文字书写方向的元素;
:has() :匹配包含指定元素的元素;
:is() :匹配指定选择器列表里的元素;
:not() :用来匹配不符合一组选择器的元素;

行为伪类

:active :鼠标激活的元素;
:hover : 鼠标悬浮的元素;
::selection :鼠标选中的元素;

状态伪类

:target :当前锚点的元素;
:link :未访问的链接元素;
:visited :已访问的链接元素;
:focus :输入聚焦的表单元素;
:required :输入必填的表单元素;
:valid :输入合法的表单元素;
:invalid :输入非法的表单元素;
:in-range :输入范围以内的表单元素;
:out-of-range :输入范围以外的表单元素;
:checked :选项选中的表单元素;
:optional :选项可选的表单元素;
:enabled :事件启用的表单元素;
:disabled :事件禁用的表单元素;
:read-only :只读的表单元素;
:read-write :可读可写的表单元素;
:blank :输入为空的表单元素;
:current() :浏览中的元素;
:past() :已浏览的元素;
:future() :未浏览的元素;

结构伪类

:root :文档的根元素;
:empty :无子元素的元素;
:first-letter :元素的首字母;
:first-line :元素的首行;
:nth-child(n) :元素中指定顺序索引的元素;
:nth-last-child(n) :元素中指定逆序索引的元素;;
:first-child :元素中为首的元素;
:last-child :元素中为尾的元素;
:only-child :父元素仅有该元素的元素;
:nth-of-type(n) :标签中指定顺序索引的标签;
:nth-last-of-type(n) :标签中指定逆序索引的标签;
:first-of-type :标签中为首的标签;
:last-of-type :标签中为尾标签;
:only-of-type :父元素仅有该标签的标签;

伪元素

::before :在元素前插入内容;
::after :在元素后插入内容;

优先级

权重分成如下几个等级,数值越大权重越高:

10000:!important;
01000:内联样式;
00100:ID 选择器;
00010:类选择器、伪类选择器、属性选择器;
00001:元素选择器、伪元素选择器;
00000:通配选择器、后代选择器、兄弟选择器;

继承性

在 CSS 中有一个很重要的特性就是子元素会继承父元素对应属性计算后的值。比如页面根元素html 的文本颜色默认是黑色的,页面中的所有其他元素都将继承这个颜色,当申明了如下样式后,H1 文本将变成橙色。

1
2
3
4
5
6
body {
color: orange;
}
h1 {
color: inherit;
}

存在默认继承的是那些不会影响到页面布局的属性,如下:

字体相关: font-family 、font-style 、font-size 、font-weight 等;
文本相关: text-align 、text-indent 、text-decoration 、text-shadow 、letterspacing、word-spacing 、white-space 、line-height 、color 等;
列表相关: list-style 、list-style-image 、list-style-type 、list-styleposition等;
其他属性: visibility 、cursor 等;

对于其他默认不继承的属性也可以通过以下几个属性值来控制继承行为:

inherit :继承父元素对应属性的计算值;
initial :应用该属性的默认值,比如 color 的默认值是 #000 ;
unset :如果属性是默认可以继承的,则取 inherit 的效果,否则同 initial ;
revert :效果等同于 unset ,兼容性差。

文档流

在 CSS 的世界中,会把内容按照从左到右、从上到下的顺序进行排列显示。正常情况下会把页面分割成一行一行的显示,而每行又可能由多列组成,所以从视觉上看起来就是从上到下从左到右,而这就是 CSS 中的流式布局,又叫文档流。文档流就像水⼀样,能够自适应所在的容器,一般它有如下几个特性:

  • 块级元素默认会占满整行,所以多个块级盒子之间是从上到下排列的;

  • 内联元素默认会在一行里一列一列的排布,当一行放不下的时候,会自动切换到下一行继续按照列排布;

如何脱离文档流

脱流文档流指节点脱流正常文档流后,在正常文档流中的其他节点将忽略该节点并填补其原先空间。文档⼀旦脱流,计算其父节点高度时不会将其高度纳入,脱流节点不占据空间。有两种方式可以让元素脱离文档流:浮动和定位。

  • 使用浮动(float)会将元素脱离文档流,移动到容器左/右侧边界或者是另一个浮动元素旁边,该浮动元素之前占用的空间将被别的元素填补,另外浮动之后所占用的区域不会和别的元素之间发生重叠;

  • 使用绝对定位( position: absolute; )或者固定定位( position: fixed; )也会使得元素脱离文档流,且空出来的位置将自动被后续节点填补。

盒模型

在 CSS 中任何元素都可以看成是⼀个盒子,而⼀个盒子是由 4 部分组成的:内容(content)、内边距(padding)、边框(border)和外边距(margin)。

盒模型有两种:标准盒模型和IE盒模型。

1
2
3
4
5
6
7
.box {
width: 200px;
height: 200px;
padding: 10px;
border: 1px solid #eee;
margin: 10px;
}

标准盒模型认为:盒子的实际尺寸 = 内容(设置的宽/高) + 内边距 + 边框

所以 .box 元素内容的宽度就为 200px ,而实际的宽度则是 width + padding-left + padding-right + border-left-width + border-right-width = 200 + 10 + 10 + 1 + 1 = 222。

IE 盒模型认为:盒子的实际尺寸 = 设置的宽/高 = 内容 + 内边距 + 边框

.box 元素所占用的实际宽度为 200px ,而内容的真实宽度则是 width - padding-left -padding-right - border-left-width - border-right-width = 200 - 10 - 10 - 1 - 1 = 178。

高版本的浏览器默认使用标准盒模型。

CSS3中新增了一个属性box-sizing,指定盒子使用什么标准:

content-box :标准盒模型;
border-box :IE 盒模型。

视觉格式化模型

视觉格式化模型(Visual formatting model)是用来处理和在视觉媒体上显示文档时使用的计算规则。CSS 中一切皆盒子,而视觉格式化模型简单来理解就是规定这些盒子应该怎么样放置到页面中去,这个模型在计算的时候会依赖到很多的因素,比如:盒子尺寸、盒子类型、定位方案(是浮动还是定位)、兄弟元素或者子元素以及一些别的因素。

盒子类型由 display 决定,同时给⼀个元素设置 display 后,将会决定这个盒子的 2 个显示类型(display type):
outer display type(对外显示):决定了该元素本身是如何布局的,即参与何种格式化上下文;
inner display type(对内显示):其实就相当于把该元素当成了容器,规定了其内部子元素是如何布局的,参与何种格式化上下文;

outer display type

对外显示方面,盒子类型可以分成 2 类:block-level box(块级盒子) 和 inline-level box(行内级盒子)。
依据上图可以列出都有哪些块级和行内级盒子:
块级盒子:display 为 block、list-item、table、flex、grid、flow-root 等;
行内级盒子:display 为 inline、inline-block、inline-table 等;

所有块级盒子都会参与 BFC,呈现垂直排列;而所有行内级盒子都参会 IFC,呈现水平排列。

block

占满一行,默认继承父元素的宽度;多个块元素将从上到下进行排列;
设置 width/height 将会生效;
设置 padding 和 margin 将会生效;

inline

不会占满一行,宽度随着内容而变化;多个 inline 元素将按照从左到右的顺序在一行里排列显示,如果一行显示不下,则自动换行;
设置 width/height 将不会生效;
设置竖直方向上的 padding 和 margin 将不会生效;

inline-block

是行内块元素,不单独占满一行,可以看成是能够在一行里进行左右排列的块元素;
设置 width/height 将会生效;
设置 padding 和 margin 将会生效;

inner display type

对内方面,其实就是把元素当成了容器,里面包裹着文本或者其他子元素。container box 的类型依据 display 的值不同,分为 4 种:

block container:建立BFC 或者 IFC;
flex container:建立FFC;
grid container:建立GFC;
ruby container:接触不多,不做介绍。

值得⼀提的是如果把 img 这种替换元素(replaced element)申明为 block 是不会产生container box 的,因为替换元素比如 img 设计的初衷就仅仅是通过 src 把内容替换成图片,完全没考虑过会把它当成容器。

格式化上下文

格式化上下文(Formatting Context)说的是页面中一块渲染区域,规定了渲染区域内部的子元素是如何排版以及相互作用的。

不同的盒子有不同的格式化上下文,大概有四类:

BFC (Block Formatting Context) 块级格式化上下文;
IFC (Inline Formatting Context) 行内格式化上下文;
FFC (Flex Formatting Context) 弹性格式化上下文;
GFC (Grid Formatting Context) 格栅格式化上下文;

BFC和IFC扮演着非常重要的角色,因为它们直接影响了网页布局。

BFC

块格式化上下文,是一个独立的渲染区域,只有块级盒子参与,规定了内部的块级盒子如何布局,并且与这个区域外部毫不相干。

BFC渲染规则

内部的盒子会在垂直方向,一个接一个地放置;
盒子垂直方向的距离由 margin 决定,属于同一个 BFC 的两个相邻盒子的 margin 会发生重叠;
每个元素的 margin 的左边,与包含块 border 的左边相接触(对于从左往右的格式化,否则相反),即使存在浮动也是如此;
BFC 的区域不会与 float 盒子重叠;
BFC 就是页面上的⼀个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
计算 BFC 的高度时,浮动元素也参与计算。

如何创建BFC

根元素:html
非溢出的可见元素:overflow 不为 visible
设置浮动:float 属性不为 none
设置定位:position 为 absolute 或 fixed
定义成块级的非块级元素:display: inline-block/table-cell/table-caption/flex/inlineflex/grid/inline-grid

BFC应用场景

  1. 自适应两栏布局

    应用原理:BFC 的区域不会和浮动区域重叠,所以就可以把侧边栏固定宽度且左浮动,而对右侧内容触发 BFC,使得它的宽度自适应该行剩余宽度。

    1
    2
    3
    4
    <div class="layout">
    <div class="aside">aside</div>
    <div class="main">main</div>
    </div>
    1
    2
    3
    4
    5
    6
    7
    8
    .aside {
    float: left;
    width: 100px;
    }
    .main {
    <!-- 触发 BFC -->
    overflow: auto;
    }
  2. 清除内部浮动

    浮动造成的问题就是父元素高度坍塌,所以清除浮动需要解决的问题就是让父元素的高度恢复正常。而用BFC 清除浮动的原理就是:计算 BFC 的高度时,浮动元素也参与计算。只要触发父元素的 BFC 即可。

    1
    2
    3
    .parent {
    overflow: hidden;
    }
  3. 防止垂直margin合并

    BFC 渲染原理之一:同一个 BFC 下的垂直 margin 会发生合并。所以如果让 2 个元素不在同一个BFC 中即可阻质止垂直 margin 合并。那如何让 2 个相邻的兄弟元素不在同⼀个 BFC 中呢?可以给其中一个元素外面包裹一层,然后触发其包裹层的 BFC,这样一来 2 个元素就不会在同一个 BFC中了。

    1
    2
    3
    4
    5
    6
    <div class="layout">
    <div class="a">a</div>
    <div class="contain-b">
    <div class="b">b</div>
    </div>
    </div>
    1
    2
    3
    4
    5
    6
    7
    8
    .demo3 .a,
    .demo3 .b {
    border: 1px solid #999;
    margin: 10px;
    }
    .contain-b {
    overflow: hidden;
    }

IFC

IFC 的形成条件非常简单,块级元素中仅包含内联级别元素,需要注意的是当IFC中有块级元素插入时,会产生两个匿名块将父元素分割开来,产生两个 IFC。

IFC渲染规则

子元素在水平方向上一个接一个排列,在垂直方向上将以容器顶部开始向下排列;
节点无法声明宽高,其中 margin 和 padding 在水平方向有效在垂直方向无效;
节点在垂直方向上以不同形式对齐;
能把在一行上的框都完全包含进去的⼀个矩形区域,被称为该行的线盒(line box)。线盒的宽度是由包含块(containing box)和与其中的浮动来决定;
IFC 中的 line box ⼀般左右边贴紧其包含块,但 float 元素会优先排列。
IFC 中的 line box 高度由 line-height 计算规则来确定,同个 IFC 下的多个 line box 高度可能会不同;
当内联级盒子的总宽度少于包含它们的 line box 时,其水平渲染规则由 text-align 属性值来决定;
当一个内联盒子超过父元素的宽度时,它会被分割成多盒子,这些盒子分布在多个 line box中。如果子元素未设置强制换行的情况下,inline box 将不可被分割,将会溢出父元素。

1
<p>It can get <strong>very complicated</storng> once you start looking into it.</p>

p 标签是一个 block container,对内将产生一个 IFC;
由于一行没办法显示完全,所以产生了 2 个线盒(line box);线盒的宽度就继承了 p 的宽度;高度是由里面的内联盒子的 line-height 决定;
It can get:匿名的内联盒子;
very complicated:strong 标签产生的内联盒子;
once you start:匿名的内联盒子;
looking into it.:匿名的内联盒子。

IFC应用场景

水平居中:当一个块要在环境中水平居中时,设置其为 inline-block 则会在外层产生IFC,通过 text-align 则可以使其水平居中。
垂直居中:创建一个 IFC,用其中一个元素撑开父元素的高度,然后设置其 vertical-align: middle,其他行内元素则可以在此父元素下垂直居中。

层叠上下文

屏幕上水平是X轴,垂直是Y轴,屏幕到眼睛的方向是Z轴。元素根据属性的优先级在Z轴上排开。

z-index 能够在层叠上下文中对元素的堆叠顺序其作用是必须配合定位才可以;
除了 z-index 之外,一个元素在 Z 轴上的显示顺序还受层叠等级和层叠顺序影响;

特定的 HTML 元素或者CSS 属性产生层叠上下文,符合以下任一条件的元素都会产生层
叠上下文:

html ⽂档根元素
声明 position: absolute/relative 且 z-index 值不为 auto 的元素;
声明 position: fixed/sticky 的元素;
flex 容器的子元素,且 z-index 值不为 auto;
grid 容器的子元素,且 z-index 值不为 auto;
opacity 属性值⼩于 1 的元素;
mix-blend-mode 属性值不为 normal 的元素;
以下任意属性值不为 none 的元素:
transform
filter
perspective
clip-path
mask / mask-image / mask-border
isolation 属性值为 isolate 的元素;
-webkit-overflow-scrolling 属性值为 touch 的元素;
will-change 值设定了任⼀属性而该属性在 non-initial 值时会创建层叠上下文的元素;
contain 属性值为 layout、paint 或包含它们其中之⼀的合成值(比如 contain: strict、
contain: content)的元素。

层叠等级

层叠等级指节点在三维空间Z轴上的上下顺序。

在同⼀个层叠上下文中,它描述定义的是该层叠上下文中的层叠上下文元素在 Z 轴上的上下顺序;
在其他普通元素中,它描述定义的是这些普通元素在 Z 轴上的上下顺序;

普通元素的层叠等级由其所在的层叠上下文决定,层叠等级的比较只有在当前层叠上下文中才有意义,脱离当前层叠上下文的比较就无意义了。

层叠顺序

同一个层叠上下文如果有多个元素,它们之前的层叠顺序。

层叠等级由低到高:

层叠上下文的 border 和 background
z-index < 0 的子节点
标准流内块级非定位的子节点
浮动非定位的子节点
标准流内行内非定位的子节点
z-index: auto/0 的子节点
z-index > 0的子节点

如何比较两个元素的层叠等级?

在同一个层叠上下文中,比较两个元素就是按照上图的介绍的层叠顺序进行比较。
如果不在同一个层叠上下文中的时候,那就需要比较两个元素分别所处的层叠上下文的等级。
如果两个元素都在同⼀个层叠上下文,且层叠顺序相同,则在 HTML 中定义越后面的层叠等级越高。

值和单位

数值:长度值 ,用于指定例如元素 width、border-width、font-size 等属性的值;
百分比:可以用于指定尺寸或长度,例如取决于父容器的 width、height 或默认的fontsize;
颜色:用于指定 background-color、color 等;
坐标位置:以屏幕的左上角为坐标原点定位元素的位置,比如常见的 background-position、top、right、bottom 和 left 等属性;
函数:用于指定资源路径或背景图片的渐变,比如 url()、linear-gradient() 等;

还有些值是需要带单位的,比如 width: 100px,这里的 px 就是表示长度的单位,长度单位除了px 外,比较常用的还有 em、rem、vw/vh 等。

px

屏幕分辨率是指在屏幕的横纵方向上的像素点数量,比如分辨率 1920×1080 意味着水平方向含有1920 个像素数,垂直方向含有 1080 个像素数。

屏幕尺寸一致的情况下,屏幕分辨率越高,显示效果就越细腻。

em

em 是 CSS 中的相对长度单位中的一个。

在 font-size 中使用是相对于父元素的 font-size 大小,比如父元素 font-size: 16px,当给子元素指定 font-size: 2em 的时候,经过计算后它的字体大小会是 32px;
在其他属性中使用是相对于自身的字体大小,如 width/height/padding/margin 等;

每个浏览器都会给 HTML 根元素 html 设置一个默认的 font-size,而这个值通常是16px。这也就是为什么 1em = 16px 的原因所在了。

rem

rem(root em) 和 em一样,也是一个相对长度单位,不过 rem 相对的是 HTML 的根元素html。

rem 由于是基于 html 的 font-size 来计算,所以通常用于自适应网站或者 H5 中。

vw/vh

vw 和 vh 分别是相对于屏幕视口宽度和高度而言的长度单位:

  • 1vw = 视口宽度均分成 100 份中 1 份的长度;

  • 1vh = 视口高度均分成 100 份中 1 份的长度;

相对视口的单位,除了 vw/vh 外,还有 vmin 和 vmax:

  • vmin:取 vw 和 vh 中值较小的;

  • vmax:取 vw 和 vh 中值较大的;

颜色体系

根据 CSS 颜色草案 中提到的颜色值类型,大概可以把它们分为这几类:

  • 颜色关键字

  • transparent 关键字

  • currentColor 关键字

  • RGB 颜色

  • HSL 颜色

颜色关键字

颜色关键字(color keywords)是不区分大小写的标识符,它表示一个具体的颜色,比如 white(白),黑(black)等;

transparent关键字

transparent 关键字表示⼀个完全透明的颜色,即该颜色看上去将是背景色。

应用场景:

实现三角形

等腰三角形:设置⼀条边有颜色,然后紧挨着的 2 边是透明,且宽度是有颜色边的⼀半;直角三角形:设置⼀条边有颜色,然后紧挨着的任何一边透明即可。

增大点击区域

常常在移动端的时候点击的按钮的区域特别小,但是由于现实效果又不太好把它做大,所以常用的⼀个手段就是通过透明的边框来增大按钮的点击区域

currentColor关键字

currentColor 会取当前元素继承父级元素的文本颜色值或声明的文本颜色值,即 computed 后的color 值。

RGB[A]颜色

RGB[A] 颜色是由 R(red)-G(green)-B(blue)-A(alpha) 组成的色彩空间。

十六进制符号

RGB 中的每种颜色的值范围是 00~ff,值越大表示颜色越深。所以⼀个颜色正常是 6 个十六进制字符加上 # 组成,比如红色就是 #ff0000。

如果 RGB 颜⾊需要加上不透明度,那就需要加上 alpha 通道的值,它的范围也是 00~ff,比如一个带不透明度为 67% 的红色可以这样写 #ff0000aa。

使用十六进制符号表示颜色的时候,都是用 2 个十六进制表示一个颜色,如果这 2 个字符相同,还可以缩减成只写 1 个,比如,红色 #f00;带 67% 不透明度的红色 #f00a。

函数符

当 RGB 用函数表示的时候,每个值的范围是 0255 或者 0%100%,所以红色是 rgb(255, 0,0), 或者 rgb(100%, 0, 0)。
如果需要使用函数来表示带不透明度的颜色值,值的范围是 01 及其之间的小数或者0%100%,比如带 67% 不透明度的红色是 rgba(255, 0, 0, 0.67) 或者 rgba(100%, 0%, 0%, 67%)

要么都用数字,要么都用百分比,同时用是不对的,但是透明度不需要保持一致。rgb(100%, 0%, 0%, 0.67)

带 67% 不透明度的红色可以这样写 rgba(255 0 0 / 0.67)

HSL[A]颜色

HSL[A] 颜色是由色相(hue)-饱和度(saturation)-亮度(lightness)-不透明度组成的颜色体系。
色相(H)是色彩的基本属性,值范围是 0360 或者 0deg360deg, 0 (或 360) 为红色, 120为绿色, 240 为蓝色;
饱和度(S)是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取 0100% 的数值;0% 为灰色, 100% 全色;
亮度(L),取 0
100%,0% 为暗,100% 为白;
不透明度(A),取 0100%,或者01及之间的小数;

在 Chrome DevTools 中可以按住 shift + 鼠标左键可以切换颜色的表示方式。

媒体查询

媒体查询是指针对不同的设备、特定的设备特征或者参数进行定制化的修改网站的样式。

加上 media 属性来指定该样式文件只能对什么设备生效,不指定的话默认是 all,即对所有设备都生效:

1
2
<link rel="stylesheet" src="styles.css" media="screen" />
<link rel="stylesheet" src="styles.css" media="print" />

all:适用于所有设备;
print:适用于在打印预览模式下在屏幕上查看的分页材料和文档;
screen:主要用于屏幕;
speech:主要用于语音合成器。

除了通过 让指定设备生效外,还可以通过 @media 让 CSS 规则在特定的条件下才能生效。响应式页面就是使用了 @media 才让一个页面能够同时适配 PC、Pad 和手机端。

1
@media (min-width: 1000px) {}

媒体查询支持逻辑操作符:
and:查询条件都满足的时候才生效;
not:查询条件取反;
only:整个查询匹配的时候才生效,常用于兼容旧浏览器,使用时候必须指定媒体类型;
逗号或者 or:查询条件满足一项即可匹配;

1
2
/* ⽤户设备的最⼩⾼度为680px或为纵向模式的屏幕设备 */
@media (min-height: 680px), screen and (orientation: portrait) {}

常见需求

自定义属性

现在 CSS 里也支持了变量的用法。通过自定义属性就可以在想要使用的地方引用它。

自定义属性也和普通属性一样具有级联性,申明在 :root 下的时候,在全文档范围内可用,而如果是在某个元素下申明自定义属性,则只能在它及它的子元素下才可以使用。

自定义属性必须通过 –x 的格式申明,比如:–theme-color: red; 使用自定义属性的时候,需要用 var 函数。比如:

1
2
3
4
5
6
7
8
<!-- 定义⾃定义属性 -->
:root {
--theme-color: red;
}
<!-- 使⽤变量 -->
h1 {
color: var(--theme-color);
}

1px边框实现方案

Retina 显示屏比普通的屏幕有着更高的分辨率,所以在移动端的 1px 边框就会看起比较粗,为了美观通常需要把这个线条细化处理。

只设置单条底部边框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.scale-1px-bottom {
position: relative;
border:none;
}
.scale-1px-bottom::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
background: #000;
width: 100%;
height: 1px;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
}

同时设置 4 条边框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.scale-1px {
position: relative;
margin-bottom: 20px;
border:none;
}
.scale-1px::after {
content: '';
position: absolute;
top: 0;
left: 0;
border: 1px solid #000;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 200%;
height: 200%;
-webkit-transform: scale(0.5);
transform: scale(0.5);
-webkit-transform-origin: left top;
transform-origin: left top;
}

清除浮动

浮动:浮动元素会脱离文档流并向左/向右浮动,直到碰到父元素或者另⼀个浮动元素。

为什么要清除浮动:

因为浮动元素会脱离正常的文档流,并不会占据文档流的位置,所以如果⼀个父元素下面都是浮动元素,那么这个父元素就无法被浮动元素所撑开,这样⼀来父元素就丢失了高度,这就是所谓的浮动造成的父元素高度坍塌问题。

父元素高度一旦坍塌将对后面的元素布局造成影响。

两种方法:通过 BFC 来清除、通过 clear 来清除。

BFC清除浮动

前面介绍 BFC 的时候提到过,计算 BFC 高度的时候浮动子元素的高度也将计算在内,利用这条规则就可以清除浮动。

假设⼀个父元素 parent 内部只有 2 个子元素 child,且它们都是左浮动的,这个时候 parent 如果没有设置高度的话,因为浮动造成了高度坍塌,所以 parent 的高度会是 0,此时只要给 parent创造⼀个 BFC,那它的高度就能恢复了。

而产生BFC 的方式很多,我们可以给父元素设置overflow: auto 来简单的实现 BFC 清除浮动,但是为了兼容 IE 最好用overflow: hidden。

1
2
3
.parent {
overflow: hidden;
}

通过 overflow: hidden 来清除浮动并不完美,当元素有阴影或存在下拉菜单的时候会被截断,所以该方法使用比较局限。

通过clear清除浮动

1
2
3
4
5
6
7
8
.clearfix {
zoom: 1;
}
.clearfix::after {
content: "";
display: block;
clear: both;
}

这种写法的核心原理就是通过 ::after 伪元素为在父元素的最后一子元素后面生成一个内容为空的块级元素,然后通过 clear 将这个伪元素移动到所有它之前的浮动元素的后面,画个图来理解一下。

上面这个 demo 或者图里为了展示需要所以给伪元素的内容设置为了 ::after,实际使用的时候需要设置为空字符串,让它的高度为 0,从而父元素的高度都是由实际的子元素撑开。

消除浏览器默认样式

针对同⼀个类型的 HTML 标签,不同的浏览器往往有不同的表现,所以在网站制作的时候,开发者通常都是需要将这些浏览器的默认样式清除,让网页在不同的浏览器上能够保持一致。

reset.css,Normalize.css

长文本处理

默认:字符太长溢出了容器

字符超出部分换行

字符超出位置使用连字符

单行文本超出省略

多行文本超出省略

水平垂直居中

让元素在父元素中呈现水平垂直居中的几种情况:

单行的文本、inline 或者 inline-block 元素;
固定宽高的块级盒子;
不固定宽高的块级盒子;

单行的文本、inline或inline-block元素

水平居中

此类元素需要水平居中,则父级元素必须是块级元素( block level ),且父级元素上需要这样设置样式:

1
2
3
.parent {
text-align: center;
}
垂直居中

方法一:通过设置上下内间距一致达到垂直居中的效果:

1
2
3
4
.single-line {
padding-top: 10px;
padding-bottom: 10px;
}

方法二:通过设置 height 和 line-height 一致达到垂直居中:

1
2
3
4
.single-line {
height: 100px;
line-height: 100px;
}

固定宽高的块级盒子

方法一:absolute + 负 margin

方法二:absolute + margin auto

方法三:absolute + calc

不固定宽高的块级盒子

方法一:absolute + transform

方法二:line-height + vertical-align

方法三:writing-mode

方法四:table-cell

方法五:flex

方法六:grid

常用布局

两栏布局(边栏定宽主栏自适应)

方法一:float + overflow(BFC 原理)

方法二:float + margin

方法三:flex

方法四:grid

三栏布局(两侧栏定宽主栏自适应)

方法一:圣杯布局

方法二:双飞翼布局

方法三:float + overflow(BFC 原理)

方法四:flex

方法五:grid

多列等高布局

方法一:padding + 负margin

方法二:设置父级背景图片

三行布局(头尾定高主栏自适应)

1
2
3
4
5
6
7
<div class="layout">
<header></header>
<main>
<div class="inner"></div>
</main>
<footer></footer>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
html,
body,
.layout {
height: 100%;
}
body {
margin: 0;
}
header,
footer {
height: 50px;
}
main {
overflow-y: auto;
}

方法一:calc

方法二:absolute

方法三:flex

方法四:grid

前言

每个tab页面都有自己的渲染进程,而每个渲染进程又由多个线程组成,每个渲染进程都有一个主线程,主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要⼀个系统来统筹调度这些任务。这个系统就是今天的主角 – 消息队列和事件循环系统。

JS为什么是单线程的?为什么需要异步?既然 JS 是单线程的,只能在⼀条线程上执⾏,⼜是如何实现的异步呢?

本文Event Loop 只针对浏览器,暂不为 node 展开讨论。

进程与线程

概念

JS是单线程执行的,指的是一个进程里有且只有一个主线程负责执行js代码。

进程是CPU资源分配的最小单位。线程是CPU调度的最小单位。

多线程与多进程

多进程是指同一个时间里,同一个计算机系统允许两个或者两个以上的进程处于运行状态。比如可以在听歌的同时写代码。

多线程是指程序中包含多个执行流,一个程序中可以同时运行多个不同的线程来执行不同的任务,允许单个程序创建多个并行执行的线程来完成各自的任务。⽐如打开 QQ 的这个进程,可能同时有接收消息线程、传输⽂件线程、检测安全线程…… 所以⼀个⽹⻚能够正常的运⾏并和⽤户交互,也需要很多个进程之间相互配合。

浏览器打开一个Tab页就是创建了一个进程,一个进程可以有多个线程,JS 引擎线程、HTTP 请求线程、事件触发线程、GUI 渲染线程等等。发起请求是就创建了一个线程,请求结束后线程可能被销毁。

由于单线程原因,主线程一次只能执行一个任务,每次任务执行完成会去消息队列取新的任务执行。

  1. 一个任务执行时间过长,导致主线程长期被霸占,如何优化?==> 引入异步编程,实现
    非阻塞调用
  2. 如何处理任务优先级? ===》引⼊任务队列,先进先出来管理任务执行顺序
  3. 紧急任务无法插队?===》引入宏任务、微任务处理不同任务队列的优先级

浏览器

既然 JS 是单线程的,那么诸如 onclick 回调,setTimeout,Ajax 这些都是怎么实现的呢?是因为浏览器或 node(宿主环境)是多线程的,即浏览器搞了⼏个其他线程去辅助 JS 线程的运⾏。

浏览器有很多线程,例如:

  1. GUI 渲染线程
  2. JS 引擎线程
  3. 定时器触发线程 (setTimeout)
  4. 浏览器事件线程 (onclick)
  5. http 异步线程
  6. EventLoop 轮询处理线程

其中,1、2、4 为常驻线程。

分类:
类别 A:GUI 渲染线程
类别 B:JS 引擎线程
类别 C:EventLoop 轮询处理线程
类别 D:其他线程,有 定时器触发线程 (setTimeout)、http 异步线程、浏览器事件线程 (onclick)等等。

JS 引擎线程

JS 引擎线程,我们把它称为主线程,即运⾏ JS 代码的那个线程(不包括异步的那些代码)

1
2
3
4
var a = 2;
setTimeout()
ajax()
console.log()

第 1、4 ⾏代码是同步代码,直接在主线程中运⾏;第 2、3 ⾏代码交给其他线程运⾏。
主线程运⾏ JS 代码时,会⽣成个执⾏栈,可以处理函数的嵌套。

消息队列(任务队列)

可以理解为⼀个静态的队列存储结构,⾮线程,只做存储,⾥⾯存的是⼀堆异步成功后的回调函数字符串,肯定是先成功的异步的回调函数在队列的前⾯,后成功的在后⾯。
注意:是异步成功后,才把其回调函数扔进队列中,⽽不是⼀开始就把所有异步的回调函数扔进队列。⽐如 setTimeout 3 秒后执⾏⼀个函数,那么这个函数是在 3 秒后才进队列的。

其他线程

定时器触发线程 (setTimeout)、http 异步线程、浏览器事件线程 (onclick)
主线程执⾏ JS 代码时,碰到异步代码,就把它丢给各⾃相对应的线程去执⾏,⽐如:

1
2
3
4
5
var a = 2;
setTimeout(fun A)
ajax(fun B)
console.log()
dom.onclick(func C)

主线程在运⾏这段代码时,碰到 2 setTimeout (fun A),把这⾏代码交给定时器触发线程去执⾏;碰到 3 ajax (fun B),把这⾏代码交给 http 异步线程去执⾏;碰到 5 dom.onclick (func C) ,把这⾏代码交给浏览器事件线程去执⾏。
注意: 这⼏个异步代码的回调函数 fun A,fun B,fun C,各⾃的线程都会保存着的,因为需要在未来的某个时候,将回调函数交给主线程去执⾏。

作用:

  1. 执⾏主线程扔过来的异步代码,并执⾏代码
  2. 保存回调函数,在未来的某个时刻,通知 EventLoop 轮询处理线程过来取相应的回调函数然后执⾏(下⾯会讲)

区别:

  • 对于 setTimeout 代码,定时器触发线程在接收到代码时就开始计时,时间到了将回调函数扔进队列。

  • 对于 ajax 代码,http 异步线程⽴即发起 http 请求,请求成功后将回调函数扔进队列。

  • 对于 dom.onclick,浏览器事件线程会先监听 dom,直到 dom 被点击了,才将回调函数扔进队列。

EventLoop 轮询处理线程

  1. 主线程,处理同步代码
  2. 类别 D 的⼏个异步线程,处理异步代码
  3. 消息队列,存储着异步成功后的回调函数,⼀个静态存储结构

消息队列作⽤就是存放着未来要执⾏的回调函数

1
2
3
4
5
6
setTimeout(() => {
console.log(1)
}, 2000)
setTimeout(() => {
console.log(2)
}, 3000)

在⼀开始,消息队列是空的,在 2 秒后,⼀个 () => { console.log (1) } 的函数进⼊队列,在 3 秒后,⼀个 () => { console.log (2) } 的函数进⼊队列,此时队列⾥有两个元素,主线程从队列头中挨个取出并执⾏。

这需要⼀个中介去专⻔去沟通它们 3 个,⽽这个中介,就是 EventLoop 轮询处理线程

Event Loop(事件循环)

JS是一种单线程语言,一次只能完成一个任务。如果有多个任务,就必须排队,前面一个任务完成再完成下面的任务。如果前面的任务过长,后面的就会一直等待,拖延整个程序。为了解决这个问题,引入Event Loop,将任务分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会交给相应的WebAPIs 线程处理,在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。

事件循环可以理解为由三部分组成:

  1. 主线程执行栈
  2. 异步任务等待触发:浏览器为异步任务单独开辟的几个辅佐线程 (事件触发线程、Http异步请求线程、GUI 渲染线程) 可以统⼀理解为 WebAPIs
  3. 异步任务队列:以队列的数据结构对事件任务进行管理,特点是先进先出,后进后出。

模型特点:

  1. 所有同步任务都会在主线程上执行,同时会形成一个执行栈(execution context
    stack),直至栈空,即任务结束。
  2. 主线程之外,还存在一个任务队列(task queue)。只要异步任务有了运行结果,就在
    任务队列之中放置⼀个事件。
  3. 一旦执行栈上的任务执行完毕,系统就会从任务队列读取新的任务,结束等待状态,进入执行栈,开始执行,循环往复。

从C语言角度理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
TaskQueue task_queue;
void ProcessTask();
void ProcessDelayTask();
bool keep_running = true;
void MainThread(){
while (task_queue.waitForTask()) {
Task task = task_queue.takeTask();// 取出消息队列中任务
ProcessTask(task);// 执⾏任务
ProcessDelayTask()// 执⾏延迟队列中的任务
if(!keep_running) //如果设置了退出标志,那么直接退出线程循环
break;
}
}

callback

A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.
回调是作为参数传递给另一个函数并在其父函数完成后执行的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 异步请求的回调函数
$.get('api/find',function(res){
console.log(res)
})
// 点击事件的回调函数
$('.btn').click(function(){
alert('this is a callback of click event')
})
// 数组中遍历每⼀项调⽤的回调函数
[1,2,3].forEach(function(item){
console.log(item)
})
// 同步回调
function getNodes(params,callback){
var list = JSON.parse(params)
if(typeof callback === 'function'){
callback(list)
}
}
getNodes('[1,2,3]',function(list){
// ....
})

既可以有同步回调,也可以有异步回调,还可以有事件处理回调和延迟函数回调。

同步vs异步

同步就是后⼀个任务等待前⼀个任务结束,然后再执⾏,程序的执⾏顺序与任务的排列顺序是⼀致的、同步的。

异步则完全不同,从程序⻆度来理解就是改变程序正常执⾏顺序的操作,每⼀个任务有⼀个或多个回调函数(callback),前⼀个任务结束后,不是执⾏后⼀个任务,⽽是执⾏回调函数,后⼀个任务则是不等前⼀个任务结束就执⾏,所以程序的执⾏顺序与任务的排列顺序是不⼀致的、异步的。

JS作为一种单线程的语言如何实现异步的?

JS 异步的实现靠的就是浏览器的多线程,当他遇到异步 API 时,就将这个任务交给对应的线程,当这个异步 API 满⾜回调条件时,对应的线程⼜通过事件触发线程将这个事件放⼊任务队列,然后主线程执⾏完主线任务后从任务队列取出任务事件继续执⾏。

总结:

同步 / 异步指的是各个任务之间执⾏顺序的确定性。同时, 任务≠回调函数 , 不管是同步任务,异步任务都可以通过回调函数去实现。

从同步异步角度理解JS的执行机制

1
2
3
4
5
console.log(1)
setTimeout(function(){// 200ms后,新任务task_1进⼊ 任务队列
console.log(2)
},200)
console.log(3)

输出结果是132,思路(暂不考虑宏任务微任务):

  1. 整体 script 作为第⼀个任务进⼊主线程,console 输出 1;
  2. 遇到异步 API setTimeout ,将异步回调函数交给 Web API 处理 (此处为定时器触发线程,200ms 之后,即满⾜触发条件后,将 task_1 推⼊任务队列 task queue )。
  3. 主线程继续往下执⾏,console 输出3,任务执⾏结束,调⽤栈为空
  4. 进⼊下⼀个循环,取出任务队列中的下个任务,此时任务队列为空,主线程进⼊等待状态。
  5. 直到 200ms 之后,发现新推⼊任务队列的 task_1 , 开始执⾏,console 输出 2

异步任务

javascript 是⼀⻔单线程的脚本语⾔,也就意味着同⼀个时间只能做⼀件事,但是单线程有⼀个问题:⼀旦这个线程被阻塞就⽆法继续⼯作了,这肯定是不⾏的。上⾯谈的EventLoop 模型通过异步编程实现⾮阻塞的调⽤效果⽅式解决了⼀个任务⻓时间霸占线程问题,但由于队列是⼀种数据结构,可以存放要执⾏的任务。它符合队列先进先出的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取,但这不能解决任务优先级问题 (紧急任务插队的需求)。为解决这个问题引⼊宏任务,微任务概念。

宏任务VS微任务

ES6 规范中,Microtask 称为 jobs,Macrotask 称为 task。即微任务是 ES 对异步的定义;⽽宏任务是浏览器对异步的定义。

宏任务与微任务都是独⽴于主执⾏栈之外的另外两个队列。
为了处理任务的优先级,权衡效率和实时性。浏览器端事件循环中的异步队列有两种:Macrotask(宏任务)队列和 Microtask(微任务)队列.

宏任务 (Macrotask) 微任务 (Microtask)
谁发起 浏览器、Node Javascript
具体事件 script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering process.nextTick, Promise 的 then 或 catch, Object.observer, MutationObserver

在事件循环这个机制当中,我们将进⾏⼀次循环操作称为 tick ,每⼀次 tick 的任务处理模型是⽐较复杂的,但关键步骤如下:

  1. 进⼊循环,⾸先选择最先进⼊宏任务队列的任务 (oldest task),如果有则执⾏ (⼀次)
  2. 检查是否存在 Microtasks,如果存在则不停地执⾏,直⾄清空微任务 (Microtasks Queue),此时执⾏栈也为清空了。
  3. GUI 线程更新 (render) 界⾯(与主线程互斥)
  4. 进⼊下⼀个 Tick 主线程重复执⾏上述步骤

注意:new Promise 执⾏本身时是属于同步代码,只有.then 才是微任务

Event Loop 的模型 (Macrotask + Microtask)

这个 Event Loop 模型运⾏机制如下:

  1. 选择当前要执⾏的宏任务队列,选择任务队列中最先进⼊的任务 ( oldest task ),如果
    宏任务队列为空即 null,则执⾏跳转到微任务(MicroTask)的执⾏步骤。
  2. 将事件循环中的任务设置为已选择任务。
  3. 执⾏任务。当执⾏栈中的函数调⽤到⼀些异步执⾏的 API(例如异步 Ajax,DOM 事件,
    setTimeout 等 API),则会开启对应的线程(Http 异步请求线程,事件触发线程和定时
    器触发线程)进⾏监控和控制,当异步任务的事件满⾜触发条件时,对应的线程则会把
    该事件的回调函数推进任务队列 (task queue) 中,等待主线程读取执⾏。
  4. 任务结束后,将事件循环中当前运⾏任务设置为 null,同时将已经运⾏完成的任务从任
    务队列中删除。
  5. microtasks 步骤:进⼊ microtask 检查点。⽤户代理会执⾏以下步骤:
    5.1 设置 microtask 检查点标志为 true。
    5.2 当事件循环 microtask 执⾏不为空时:选择⼀个最先进⼊的 microtask 队列的
    microtask,将事件循环的 microtask 设置为已选择的 microtask,运⾏ microtask,将已
    经执⾏完成的 microtask 置为 null,移出 microtask 中的 microtask。
    5.3 清理 IndexDB 事务
    5.4 设置进⼊ microtask 检查点的标志为 false。
  6. 更新界⾯渲染。
  7. 返回第⼀步。

流程图:

之前说的所有异步都放进⼀个任务消息队列⾥,现在也就是分为两个任务队列了,⽐较容易理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log('script start')
new Promise((resolve)=>{
console.log('resolve1')
resolve()
}).then(()=>{ // 创建微任务 micro_1
console.log('promise1')
})
setTimeout(()=>{ // 创建了⼀个setTimeout的宏任务 macro_1
console.log('timeout')
})
new Promise((resolve)=>{
console.log('resolve2')
resolve()
}).then(()=>{ // 创建微任务 micro_2
console.log('promise2')
})
console.log('script end')
  1. 整体 script 作为第⼀个宏任务进⼊主线程,console 输出 script start ,

  2. 遇到 new Promise, ⼊栈处理,发现是同步回调,直接执⾏,console 输出 resolve1 ;
    遇到 then,⼊栈处理,发现是异步回调函数(创建微任务 micro_1),出栈,移交给对应Web API 处理,将回调函数加⼊微任务队列尾部;

  3. 遇到 setTimeout ⼊栈处理,发现是异步回调函数(创建宏任务 macro_1),出栈,移交给 Web API(此处为定时器触发线程)处理 (0 秒等待后,将回调函数加到宏任务队列尾部);

  4. 遇到 new Promise, ⼊栈处理,发现是同步回调,直接执⾏,console 输出 resolve2;遇到 then,⼊栈处理,发现是异步回调(创建微任务 micro2),出栈,移交给 Web API 处理,将回调函数加⼊微任务队列尾部;

  5. 执⾏到 script 任务末尾,console 输出 script end , 此时执⾏栈已清空 (将当前任务从任
    务队列移除),进⼊ microtask 检查点,此时任务队列情况如下:

    任务队列 任务1 任务2
    宏任务队列1 macro_1
    微任务队列 micro_1 micro_2
  6. 取出第⼀个微任务,⼊栈处理,console 直接输出 promise1 , 出栈;

  7. 继续从微任务队列中取下⼀个,⼊栈处理,console 直接输出 promise2 ,出栈,

  8. 继续从微任务队列中取下⼀个,发现微任务队列已清空,

  9. 渲染界⾯,结束第⼀轮事件循环;

  10. 从宏任务队列中取出第⼀个宏任务,⼊栈处理,发现是 console 直接输出 timeout,未发现有微任务,再次渲染界⾯,结束本轮事件循环。

任务的优先级

Event Loop事件循环是通过任务队列的机制来协调⼯作的。⼀个 Event Loop 中,可以有⼀个或者多个任务队列 (task queue),⼀个任务队列便是⼀系列有序任务 (task) 的集合;每个任务都有⼀个任务源 (task source),源⾃同⼀个任务源的 task 必须放到同⼀个任务队列,从不同源来的则被添加到不同队列。

一个事件循环有一个或多个任务队列。 任务队列是一组任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<div id="outer">
<button id="inner">点击我试试</button>
</div>
<script type="text/javascript">
const $inner = document.querySelector('#inner')
const $outer = document.querySelector('#outer')
function handler() {
console.log('click') // 直接输出 (js同步代码)
Promise.resolve().then(_ => console.log('promise')) // 注册微任务
setTimeout(() => {
console.log('timeout')
Promise.resolve().then(_ => console.log('timeout>promise'))
}) // 注册宏任务
requestAnimationFrame(_ => {
console.log('animationFrame')
Promise.resolve().then(_ => console.log('animationFrame>promise'))
}) // 注册宏任务
$outer.setAttribute('data-random', Math.random()) // DOM属性修改,触发微任务
}
new MutationObserver(_ => {
console.log('observer')
}).observe($outer, {
attributes: true
})
$inner.addEventListener('click', handler)
$outer.addEventListener('click', handler)
// 左边是⾼优先级
// 宏任务: requestAnimationFrame=>setTimeout => setInterval
// => setImmediate(nodeJS) => I/O => UI Rendering
// 微任务: process.nextTick(nodeJS)
// => Promise(Promise.then)/mutationObserver
</script>

点击#outter 输出结果,可以看出:requestAnimationFrame 优先级⽐ setTimeout ⾼

1
2
3
4
5
6
7
8
9
10
11
// Tick1
click
resolve1
promise
observer
// Tick2
animationFrame
animationFrame>promise
// Tick3
timeout
timeout>promise

点击#inner 输出结果,可以看出:每个 macroTask 队列中的 macroTask 按顺序执⾏,在每
macroTask 之间渲染⻚⾯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Tick1
click
resolve1
promise
observer
click
resolve1
promise
animationFrame>promise1
observer
// Tick2
animationFrame
animationFrame>promise
// Tick3
animationFrame
animationFrame>promise
// Tick4
timeout
timeout>promise
// Tick5
timeout
timeout>promise

每个 macroTask 队列中的 macroTask 按顺序执⾏,在每个 macroTask 之间渲染⻚⾯
⼀个 macroTask 执⾏结束 (即 js 执⾏栈中为空),会⽴即处理 macroTask 执⾏过程中产⽣
的 microTask 并且按顺序执⾏。microTask 产⽣的 macroTask 会⾃动加⼊相应的宏任务
队列。
每次循环会把这次宏任务产⽣的所有微任务执⾏完,再进⾏下⼀次 loop。

最后

本⽂回答了渲染进程如何利⽤消息队列和事件循环机制完成⻚⾯协调各个线程⼯作的。

1. JS为什么是单线程的?

想象⼀下,假设浏览器中的 JS 是多线程的(⼀个进程中资源共享),如果现在有 2 个线程,thread1 thread2, 由于是多线程的 JS, 所以他们可以对同⼀个 dom, 同时进⾏操作thread1 删除了该 dom, ⽽ thread2 编辑了该 dom,2 个⽭盾的命令同时下达,浏览器究竟该如何执⾏呢?
虽然 JS 是单线程,但是浏览器总共开了四个线程参与了 JS 的执⾏,其他三个只是辅助,不参与解析与执⾏: 1. JS 引擎线程(主线程,只有这个线程负责解析和执⾏ JS 代码) 2. 事件触发线程 3. 定时器触发线程 4. HTTP 异步请求线程
永远只有 JS 引擎线程在执⾏ JS 脚本程序,其他三个线程只负责将满⾜触发条件的处理函数推进任务队列,等待 JS 引擎线程执⾏

2. 为什么需要异步?

如果 JS 中不存在异步,只能⾃上⽽下执⾏,如果上⼀⾏解析执⾏时间很⻓,那么下⾯的代码就会被阻塞。 对于⽤户⽽⾔,阻塞就意味着 “卡死”, 这样就导致了很差的⽤户体验。

3. 既然 JS 是单线程的,只能在一条线程上执行,又是如何实现的异步呢?

答案就是事件循环(Event loop)

例子

1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Promise((resolve)=>{
console.log('resolve1')
setTimeout(function(){
resolve()
},3000)
}).then(()=>{ // 创建微任务 micro_1
console.log('promise1')
})
new Promise((resolve)=>{
console.log('resolve2')
setTimeout(function(){
resolve()
},2000)
}).then(()=>{ // 创建微任务 micro_2
console.log('promise2')
})

2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a = 111;
setTimeout(function() {
console.log(222)
}, 2000)
fetch(url) // 假设该http请求花了3秒钟
.then(function() {
console.log(333)
})
dom.onclick = function() { // 假设⽤户在4秒钟时点击了dom
console.log(444)
}
console.log(555)
// 结果
555
222
333
444

步骤1:

主线程只执⾏了 var a = 111; 和 console.log (555) 两⾏代码,其他的代码分别交给了其他三个线程,因为其他线程需要 2、3、4 秒钟才成功并回调,所以在 2 秒之前,主线程⼀直在空闲,不断的探查队列是否不为空。
此时主线程⾥其实已经是空的了(因为执⾏完那两⾏代码了)

步骤2:

步骤3:

步骤4:

图⾥的队列⾥都只有⼀个回调函数,实际上有很多个回调函数,如果主线程⾥执⾏的代码复杂需要很⻓时间,这时队列⾥的函数们就排着,等着主线程啥时执⾏完,再来队列⾥取

所以从这⾥能看出来,对于 setTimeout,setInterval 的定时,不⼀定完全按照设想的时间的,因为主线程⾥的代码可能复杂到执⾏很久,所以会发⽣你定时 3 秒后执⾏,实际上是 3.5 秒后执⾏(主线程花费了 0.5 秒)

3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
// 执⾏结果为1,7,6,8,2,4,3,5,9,11,10,12

每次执⾏完⼀个宏任务后都要去检查微任务就可以了。

4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script>
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(() => {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
setTimeout(() => {
console.log('timer3')
Promise.resolve().then(() => {
console.log('promise3')
})
}, 0)
new Promise(function(resolve) {
console.log('promise4');
resolve();
}).then(function() {
console.log('promise5')
})
console.log('end')
</script>

执⾏结果:start > promise4 > end > promise5 > timer1 > promise1 > timer2 > promise2 > timer3 > promise3

5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log("script start");

setTimeout(function () {
console.log("setTimeout");
}, 0);

Promise.resolve()
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});

console.log("script end");
  1. 宏任务:执行整体代码(相当于