svg sprite 简介

前言

  首页的高速加载和渲染一直是前端开发者们津津乐道的事情,因此各种技术也应运而生。在 HTTP1.1 时代,为了减少请求的发送,加快首页加载,压缩和合并成了必不可少的技术,其中包括了 JavaScript 文件的压缩、混淆和合并,还有 CSS 文件的压缩和合并,最后还有一个是针对小图片的请求优化,也就是 CSS Sprite,也叫 雪碧图CSS 精灵。其大概的思想就是将多个小图片按照一定的尺寸和位置排列好,然后合成一张图片,最后用户访问页面时,只要请求这一张合成图,而开发者利用 background-position 等属性控制显示合成图某个位置的图片,即可达到一张图片多个图标的效果,同时也将请求数量压缩为一个。当然,这次我们要说的并不是这个技术,而是与之运用思想类似的 SVG Sprite

SVG Sprite

  SVG Sprite 使用 <symbol> 标签来定义一个图形模板对象,好处在于其可以重复利用,我们可以看一下 MDN 中对 <symbol> 的定义:

symbol元素用来定义一个图形模板对象,它可以用一个元素实例化。symbol元素对图形的作用是在同一文档中多次使用,添加结构和语义。结构丰富的文档可以更生动地呈现出来,类似讲演稿或盲文,从而提升了可访问性。注意,一个symbol元素本身是不呈现的。只有symbol元素的实例(亦即,一个引用了symbol的 元素)才能呈现。

  可以看到,<symbol> 定义的图形并不会第一时间显示出来,只有使用了 <use> 标签进行实例化以后才会显现。MDN 中对其做出了如下定义:

use元素在SVG文档内取得目标节点,并在别的地方复制它们。它的效果等同于这些节点被深克隆到一个不可见的DOM中,然后将其粘贴到use元素的位置,很像HTML5中的克隆模板元素。

  而要使用 <use> 来实例化一个 svg图形模板对象,则要使用其中的 xlink:href 属性,在我们处理好的 <symbol> 上都会带有一个 id,如下所示(伪代码):

1
2
3
4
5
6
7
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0;visibility:hidden">
<defs>
<symbol id="icon1">...</symbol>
<symbol id="icon2">...</symbol>
<symbol id="icon3">...</symbol>
</defs>
</svg>

  根据每一个 <symbol>id,我们可以使用 <use> 根据这些 id 来使用 svg,如下所示:

1
2
3
4
5
<div class="icons">
<svg><use xlink:href="#icon1"/></svg>
<svg><use xlink:href="#icon2"/></svg>
<svg><use xlink:href="#icon3"/></svg>
</div>

  SVG Sprite 的基本原理就是运用这些元素,相比较 CSS SpriteSVG Sprite 显得更为友好,不用多余的例如 background-position 属性来控制位置。

兼容性

  运用一个技术的前提都是其兼容性满足项目的最低要求,或者在不兼容的情况下有相对应的替代方案。在饿了么 Web 的兼容性要求为 PC 端 IE9+,安卓移动端 4.4+,IOS7+,具体可以看 ElemeFE 的 style-guide。下面是在 caniuse 上的结果:
SVG-兼容性

  可以看到兼容性在 PC 端上是完全没有问题的,而在移动端上也能支持,所以可以安心地用起来了。

结合 Webpack 使用 SVG Sprite

  我们使用 Webpack 来对多个分离的 SVG 文件进行自动化处理为合并好的多个 <symbol>,并插入到 <body> 顶部。首先我们要对 webpack.config.js 进行配置。

首先看一下基本的文件目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
├── LICENSE
├── README.md
├── css
│   └── index.css
├── dist
│   ├── app.css
│   ├── app.js
│   └── index.html
├── js
│   └── index.js
├── package.json
├── static
│   ├── analytics.svg
│   ├── archives.svg
│   ├── businessman.svg
│   ├── businessmen.svg
│   ├── certificate.svg
│   ├── chat.svg
│   └── contract.svg
├── template
│   └── index.html
└── webpack.config.js

webpack.config.js

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
var plugins = [
new HtmlWebpackPlugin({
filename: './index.html',
template: './template/index.html'
})
];
plugins.push(
new ExtractTextPlugin('[name].css')
);
module.exports = {
entry: {
app: './js/index.js',
},
output: {
path: './dist',
publicPath: './',
// filename: '[name].[chunkhash:6].js'
filename: '[name].js'
},
resolve: {
extensions: [ '', '.js' ]
},
module: {
loaders: [
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' },
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css') },
{ test: /\.svg$/, loaders: [ 'svg-sprite-loader', 'svgo-loader?useConfig=svgoConfig' ] },
{ test: /\.(gif|png|jpg|ttf|woff2|woff|eot)$/, loader: 'url?limit=1000&name=[path][name].[hash:6].[ext]' }
]
},
svgoConfig: {
plugins: [
{ removeTitle: true },
{ convertColors: { shorthex: true } },
{ convertPathData: true },
{ cleanupAttrs: true },
{ removeComments: true },
{ removeDesc: true },
{ removeUselessDefs: true },
{ removeEmptyAttrs: true },
{ removeHiddenElems: true },
{ removeEmptyText: true }
]
},
plugins: plugins
};

  可以看到,配置文件中使用了 svg-sprite-loadersvgo-loadersvg 文件进行处理,svg-sprite-loader 的作用就是将多个 svg 文件合并为一个 <svg> 元素。至于 svgo-loader,作用是将 <svg> 中一些无用的信息过滤去除,精简结构,详细配置可以自行查阅对应的文档说明,可以根据实际需求进行过滤。接下来将列出 index.css, index.js, index.html 的内容:

index.css

1
2
3
4
5
6
7
8
* {
box-sizing: border-box;
}
.icons svg {
width: 100px;
height: 100px;
}

index.js

1
2
3
4
5
6
7
8
9
10
import indexStyle from '../css/index.css';
import analytics from '../static/analytics.svg'
import archives from '../static/archives.svg'
import businessman from '../static/businessman.svg'
import businessmen from '../static/businessmen.svg'
import certificate from '../static/certificate.svg'
import chat from '../static/chat.svg'
import contract from '../static/contract.svg'
console.log('demo complete');

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>svg-sprites-demo</title>
</head>
<body>
<div class="icons">
<svg><use xlink:href="#analytics"/></svg>
<svg><use xlink:href="#archives"/></svg>
<svg><use xlink:href="#businessman"/></svg>
<svg><use xlink:href="#businessmen"/></svg>
<svg><use xlink:href="#certificate"/></svg>
<svg><use xlink:href="#chat"/></svg>
<svg><use xlink:href="#contract"/></svg>
</div>
</body>
</html>

  以上就是主要的一些配置和内容,如果需要完整的项目,可以到我的 github 下 clone 项目到你的本地进行构建。下面是构建后的网页效果和结构:

效果

优点 & 缺点

优点:

  • 将多个请求压缩为无请求
  • svg 对比 image 其屏幕适应性更好,任何分辨率都能达到高清效果
  • svg 体积更小
  • 每一个 <symbol> 都可以重复利用

缺点:

  • svg 不利于变动性大的图片,例如需要经常修改颜色
  • 兼容性对于需要兼容 IE8- 的网站不好,需要对低版本浏览器有替代方案

参考资料:

https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg
https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/symbol
https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/defs
https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/use

CSS Selectors Level 4 中的新东西

以下所有示例在 Safari Technology Preview 使用最佳,Chrome Canary 有些都还没实现,可能是我使用的姿势不对……

Negation pseudo-class——:not()

:not() 用于将符合规则的元素剔除,将样式规则应用于其他元素上。在 CSS3 中已经有 :not(),不过在 CSS3 中只能使用简单的匹配规则,例如 :not(p) 用来选择不是 <p></p> 的元素。而在 CSS4 中,可以应用更复杂的匹配规则,但是同样地不允许嵌套使用,例如 :not(:not(...))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.negation {
color: black;
}
.negation .default:not([data-red="no"]) {
color: red;
}
.negation .default a {
color: green;
}
.negation .default a:not([rel="green"], [rel="default"]) {
color: blue;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="negation">
<div class="default" data-red="no">
<a href="http://www.baidu.com" rel="green">这里是绿色</a>
<a href="http://www.ele.me" rel="default">这里也是绿色</a>
<a href="http://www.sina.com" rel="blue">这里是蓝色</a>
</div>
<div class="default" data-red="no">
这里是黑色
</div>
<div class="default" data-red="yes">
这里是红色
</div>
</div>

:not() tips

我们可以利用 :not() 来对 CSS 样式进行一个优先级提升,例如 div:not(span) {…}div {…} 是同个概念,但是明显地前者的优先级更高。

想解锁更多 :not() 的使用姿势就去看 The Negation Pseudo-class 草案

Matches-any Pseudo-class——:matches 伪类

:matches() 用于匹配所述规则的元素,并应用相应的样式规则,同样不允许嵌套使用,-webkit-any()-moz-any() 是它的两个兼容性写法。

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
36
37
38
39
40
41
42
43
44
45
.matches {
color: black;
}
.matches :matches(span, div) :matches(span, div) {
color: green;
}
/*
等同于
.matches span div,
.matches span span,
.matches div span,
.matches div div {
color: green;
}
*/
.matches :-webkit-any(span, div) :-webkit-any(span, div) {
color: green;
}
.matches :-moz-any(span, div) :-moz-any(span, div) {
color: green;
}
.matches :matches(.a, .b) :matches(.a, .b) {
color: red;
}
/*
等同于
.matches .a .a,
.matches .a .b,
.matches .b .a,
.matches .b .b {
color: red;
}
*/
.matches :-webkit-any(.a, .b) :-webkit-any(.a, .b) {
color: red;
}
.matches :-moz-any(.a, .b) :-moz-any(.a, .b) {
color: red;
}
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
<div class="matches">
<span>
<div>绿色</div>
</span>
<span>
<span>绿色</span>
</span>
<div>
<span>绿色</span>
</div>
<div>
<div>绿色</div>
</div>
<div class="a">
<div class="b">红色</div>
</div>
<div class="b">
<div class="a">红色</div>
</div>
<div class="a">
<div class="a">红色</div>
</div>
<div class="b">
<div class="b">红色</div>
</div>
</div>

想解锁更多姿势就去看 The Matches-any Pseudo-class 草案

Case-Sensitivity——不区分大小写匹配标识

Case-Sensitivity 用于声明某个匹配规则中,对字符串或者某个 value 的匹配不区分大小写。该标志声明于 ] 即右中括号之前,例如 [data-value="case" i],其中的 i 就是 Case-Sensitivity 标识。

1
2
3
.case-sensitivity :matches([data-value="case" i]) {
color: yellow;
}
1
2
3
4
<div class="case-sensitivity">
<p data-value='Case'>Case</p>
<p data-value="case">case</p>
</div>

以上的例子,data-value 虽然既有大写也有小写,但是由于我们声明了 Case-Sensitivity,所以无论大小写都会被匹配。像例子中 caseCaseCASE 等都会被匹配。

想解锁更多姿势就去看 Case-sensitivity 草案

The Directionality Pseudo-class——:dir()

:dir() 伪类用于匹配符合某个方向性的元素,例如 :dir(ltr)dir(rtl)。顾名思义,ltr 表示 left to right,即方向从左到右,rtl 表示 right-to-left,即方向从右到左。值得注意的是,使用 :dir() 匹配元素和使用 [dir=...] 在某个程度上是一样的效果,但是一个区别是 [dir=...] 无法匹配到没有显示声明 dir 的元素,但是 :dir() 却可以匹配到由浏览器计算得到或者继承来的 dir 属性的元素,详情可以看一下草案

1
2
3
4
5
6
.dir :dir(ltr) {
color: blue;
}
.dir :dir(rtl) {
color: green;
}
1
2
3
4
<div class="dir">
<p dir="ltr">从左到右</p>
<p dir="rtl">从右到左</p>
</div>

想解锁更多姿势就去看 The Directionality Pseudo-class 草案

The Language Pseudo-class——:lang()

:lang() 用于匹配声明了 lang=value 的元素,并且可以使用通配符匹配,例如 p:lang(*-CH) 将可以匹配 de-CHp 元素。

1
2
3
4
5
6
.lang p:lang(de-DE) {
color: green;
}
.lang p:lang(*-CH) {
color: blue;
}
1
2
3
4
<div class="lang">
<p lang="de-DE-1996">de-DE-1996</p>
<p lang="de-CH">de-CH</p>
</div>

想解锁更多姿势就去看 The language pseudo-class 草案

The Hyperlink Pseudo-class——:any-link 伪类

:any-link 用于匹配带有 href 属性的超链接元素,例如 <a><area><link> 等带有 href 属性的元素。:-webkit-any-link:-moz-any-link 是它的兼容性写法。

1
2
3
4
5
6
7
8
9
.link a:any-link {
color: red;
}
.link a:-webkit-any-link {
color: red;
}
.link a:-moz-any-link {
color: red;
}
1
2
3
<div class="link">
<a href="#">我是带有颜色的超链接</a>
</div>

想解锁更多姿势就去看 The Hyperlink Pseudo-class 草案

The contextual reference element pseudo-class——:scope

:scope 用于匹配当前作用域下的顶级元素。但是目前 <style scoped> 已经被移除——issue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="scope">
<p>This paragraph is outside the scope.</p>
<div>
<style scoped>
:scope {
background-color: red;
}
p {
color: blue;
}
</style>
<p>This paragraph is inside the scope.</p>
</div>
</div>

以上代码,第二个 div 将会有红色背景,并且他的所有 <p> 子元素都将拥有蓝色文字。

想解锁更多姿势就去看 The contextual reference element pseudo-class 草案

Time-dimensional Pseudo-classes——:current(), :past(), :future()

我个人用 时间轴伪类 统一称呼 :current(), :past(), :future() 这三个伪类。:current() 匹配时间轴当前的元素,:past() 匹配 :current()元素之前的元素,:future() 则匹配当前时间轴后的所有元素。这里说的时间轴指的是例如 WebVTT。值得注意的是,规范中写道如果使用的时间轴并不是文档语言所规定的,那么 :past():future() 有可能分别匹配 :current() 元素的前面的兄弟元素和后面的兄弟元素。由于在 Chrome Canary 和 Safari TP 上都不支持这几个伪类,所以无法实验正确性。下面使用的例子是从这个网址摘过来的。

1
2
3
4
5
6
7
8
:current(p, span) {
background-color: yellow;
}
:past(p, span),
:future(p, span) {
background-color: gray;
}
1
2
3
4
5
6
7
<video controls preload="metadata">
<source src="http://html5demos.com/assets/dizzy.mp4" type="video/mp4" />
<source src="http://html5demos.com/assets/dizzy.webm" type="video/webm" />
<source src="http://html5demos.com/assets/dizzy.ogv" type="video/ogv" />
<track label="English" kind="subtitles" srclang="en" src="http://www.iandevlin.com/html5test/webvtt/upc-video-subtitles-en.vtt" default>
</video>

想解锁更多姿势就去看 Time-dimensional Pseudo-classes 草案

The Indeterminate-value Pseudo-class——:indeterminate

radiocheckbox 元素上一般有两种状态——选中未选中,但是有的时候的状态会是不确定状态,而 :indeterminate 就是匹配这种不确定状态的 radiocheckbox

1
2
3
:indeterminate + label {
background-color: gray;
}
1
2
<input type="radio" name="name" id="test">
<label for="test">未确定状态</label>

上面例子的 <label><input> 处于 indeterminate state 的时候,文字将会变为灰色。

想解锁更多姿势就去看 The Indeterminate-value Pseudo-class 草案

The default-option pseudo-class——:default

:default 匹配一组相似元素集合中的默认元素,例如 <form> 中有多个 <input>,其中有一个是 <input type="submit">,那么该元素将会被匹配。此外还有 <option> 也有默认元素。

1
2
3
.default .default-form :default {
background-color: gray;
}
1
2
3
4
5
6
<div class="default">
<form class="default-form" action="#" method="get">
<input type="submit" name="name" value="submit">
<input type="reset" name="name" value="reset">
</form>
</div>

想解锁更多姿势就去看 The default-option pseudo-class 草案

The validity pseudo-classes——:valid, :invalid

<input type="email"> 中,如果我们输入了 abc123,那么此时 :invalid 将会匹配该元素,假如我们输入 abc123@163.com,那么此时 :valid 将会匹配该元素。这里要注意假如我们没有为 <input> 作约束,例如 <input type="text">,那么它的任意输入将使元素既不会被 :valid 匹配,也不会被 :invalid 匹配。

1
2
3
4
5
6
7
.valid input:valid {
color: green;
}
.valid input:invalid {
color: red;
}
1
2
3
4
<div class="valid">
<input type="email" name="eamil_valid" value="abc@abc.com">
<input type="email" name="email_invalid" value="abc">
</div>

想解锁更多姿势就去看 The validity pseudo-classes 草案

The range pseudo-classes——:in-range, :out-of-range

:in-range:out-of-range 只对有被条件约束的元素起作用,例如 <input type="number" min="1" value="1">,如果输入数字小于 1,那么将会被 :out-of-range 匹配,反之则是被 :in-range 匹配。

1
2
3
4
5
6
7
.range input:in-range {
color: green;
}
.range input:out-of-range {
color: red;
}
1
2
3
<div class="range">
<input type="number" name="range" value="1" min="1" max="10">
</div>

想解锁更多姿势就去看 The range pseudo-classes 草案

The optionality pseudo-classes——:required, :optional

:required:optional 分别匹配带有 required 标识的元素和不带 required 标识的元素。

1
2
3
4
5
6
7
.optionality input:required {
color: green;
}
.optionality input:optional {
color: red;
}
1
2
3
4
<div class="optionality">
<input type="text" name="required" value="required" required>
<input type="text" name="optional" value="optional">
</div>

想解锁更多姿势就去看 The optionality pseudo-classes 草案

The user-interaction pseudo-class——:user-error

:user-error 会匹配 :invalid, :out-of-range 和没有任何值的 :required 元素,但是假如是初始化时就触发这三种错误,user-error 将不会匹配该元素,只有当用户和元素进行交互或者提交了该表单并且触发了这三种错误,:user-error 才会被触发。Chrome 和 Safari 可能尚未支持(也可能是我使用姿势不对,希望指正),所以无法验证正确性。

1
2
3
4
5
6
7
.user-error input:user-error {
color: red;
}
.user-error input:valid {
color: green;
}
1
2
3
4
<div class="user-error">
<input type="email" name="eamil_valid" value="abc@abc.com">
<input type="email" name="email_invalid" value="abc">
</div>

想解锁更多姿势就去看 The user-interaction pseudo-class 草案

The mutability pseudo-classes——:read-only, :read-write

:read-only 匹配不可被编辑的元素,:read-write 则匹配可被编辑的元素,例如 <input> 或者 contenteditable="true" 的元素。:-moz-read-only:-moz-read-write 分别是他们的兼容性写法。

1
2
3
4
5
6
7
.mutability :read-only {
color: red;
}
.mutability :read-write {
color: green;
}
1
2
3
4
5
<div class="mutability">
<input type="text" name="read-write-input" value="read-write">
<p contenteditable="true">read-write-paragraph</p>
<p>read-only-paragraph</p>
</div>

想解锁更多姿势就去看 The mutability pseudo-classes 草案

The placeholder-shown pseudo-class——:placeholder-shown

:placeholder-shown 匹配 placeholder 文字显示时的 <input> 元素。::-webkit-input-placeholder, ::-moz-placeholder, :-ms-input-placeholder 分别是它在不同浏览器的兼容性写法。

1
2
3
4
5
6
7
8
9
10
11
12
.placeholder input:placeholder-shown {
color: green;
}
.placeholder input::-webkit-input-placeholder {
color: green;
}
.placeholder input::-moz-placeholder {
color: green;
}
.placeholder input:-ms-input-placeholder {
color: green;
}
1
2
3
<div class="placeholder">
<input type="text" name="placeholder" placeholder="placeholder is green">
</div>

想解锁更多姿势就去看 The placeholder-shown pseudo-class 草案

Grid-Structural Selectors

该特性将对例如 <table> 的栅格布局起作用。它包含 :column(selector), :nth-column(n):nth-last-column(n)。目前浏览器都还未支持,无法实验正确性。

:column(selector)

:column(selector) 将匹配例如 <table> 中 带有 selector 类名的那一列的所有元素。

1
2
3
:column(.selected) {
color: green;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<table>
<col class="selected" />
<col class="blur" />
<col class="blur" />
<tr>
<td>A</td>
<td>B</td>
<td>C</td>
</tr>
<tr>
<td>D</td>
<td>E</td>
<td>F</td>
</tr>
<tr>
<td>G</td>
<td>H</td>
<td>I</td>
</tr>
</table>

在上面的例子中,A、D、G 都将是绿色的。

:nth-column(n) 和 :nth-last-column(n)

:nth-column(n) 匹配括号内 n 的计算值的某一列的元素,计算方式是从头开始计算,而 :nth-last-column(n) 则是从后开始计算。

1
2
3
4
5
6
7
:nth-column(2n) {
color: red;
}
:nth-last-column(3n) {
color: green;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<table>
<col class="selected" />
<col class="blur" />
<col class="blur" />
<tr>
<td>A</td>
<td>B</td>
<td>C</td>
</tr>
<tr>
<td>D</td>
<td>E</td>
<td>F</td>
</tr>
<tr>
<td>G</td>
<td>H</td>
<td>I</td>
</tr>
</table>

想解锁更多姿势就去看 Grid-Structural Selectors 草案

Tree-Structural pseudo-classes——:blank

Tree-Structural pseudo-classes 是 CSS3 中的规范,但在 CSS Selectors Level 4 中加入了 :blank,它和 :empty 类似,区别在于 :empty 只能匹配没有任何内容的元素,而 :blank 可以匹配带有 spaces(空格), tabs(缩进符) and segment breaks(段落过段) 内容的元素。

想解锁更多姿势就去看 :blank pseudo-class 草案

Combinators——>>

A >> B 匹配祖先元素为 A 的 B元素,其用法与 A B 一样,与 >, +, ~ 用意一样,不过意义不同。

想解锁更多姿势就去看 Combinators 草案


上面的特性都已经存在 Working Draft 中,还有一些 Editor’s Draft 的特性,也顺带一提。

The Relational Pseudo-class——:has()

:has(selector) 匹配含有 某些规则 的元素。

1
2
3
4
5
6
7
8
9
10
11
/* 将匹配含有 img 子元素的 a 元素 */
a:has(> img)
/* 将匹配拥有 dt 兄弟元素的 dt 元素 */
dt:has(+ dt)
/* 将匹配不含有 h1、h2、h3、h4、h5、h6 元素的 section 元素 */
section:not(:has(h1, h2, h3, h4, h5, h6))
/* 和上面例子不同,下面交换了两个伪类的嵌套,表示匹配含有的不是 h1、h2、h3、h4、h5、h6 子元素的元素,区别在于这种写法要求必须含有一个子元素,而上面的写法可以不含有子元素也会被匹配 */
section:has(:not(h1, h2, h3, h4, h5, h6))

想解锁更多姿势就去看 The Relational Pseudo-class 草案

The Drag-and-Drop Pseudo-class——:drop, :drop()

:drop:drop() 匹配可被放置拖动元素的目标元素,两者区别在于 :drop() 可以匹配一些规则,包括 active, valid, invalidactive 会匹配可被放置的目标元素,valid 匹配放置的元素为合法元素的目标元素,invalid 反之。如果 :drop() 括号里没有任何过滤,那么将和 :drop 没有区别。

想解锁更多姿势就去看 The Drag-and-Drop Pseudo-class 草案

最后

其实这就是一篇科普文, 趁着国庆的闲余时间将 CSS Selectors Level 4 的一些新的特性总结了一下,可能有所错漏或理解错误,我十分的希望各位大牛帮我指出错误之处或不足之处!另外,祝大家国庆快乐。作为一只 single dog 还是写代码为好!

参考资料:

https://www.w3.org/TR/2013/WD-selectors4-20130502/
https://drafts.csswg.org/selectors-4/
http://css4.rocks/selectors-level-4/
http://css4-selectors.com/selectors/

SameSite-Cookie——防御 CSRF & XSSI

SameSite——防御 CSRF & XSSI 新机制

SameSite-cookies 是 Google 开发的用于防御 CSRF 和 XSSI(Cross Site Script Inclusion,跨域脚本包含)的新安全机制,只需在 Set-Cookie 中加入一个新的字段属性,浏览器会根据设置的安全级别进行对应的安全 cookie 发送拦截,而目前在 Chrome-dev(51.0.2704.4)中可用。

XSSI——Cross Site Script Inclusion

XSSI 属于 XSS 攻击的一种攻击方式,一般来说,浏览器允许网页加载其他域的脚本或图片等,假设我们在安全的网站上 a.com 包含一个脚本文件 getData.js 用于读取用户的私人信息,第一次用户需要在 a.com 登录,然后就可以根据验证返回用户私人信息并设置 cookie 以便下次使用,此时我们只做一个恶意网站 c.com,并包含了 getData.js 这个脚本文件,当用户点击 c.com 时,他的信息就泄露了。

当然,这种方法可以用 token 令牌等来解决,另外一种方法就是 SameSite-Cookies,通过验证是否是从 a.com 访问的 getData.js 的方式来阻止 cookie 的发送。

SameSite 使用方式

需要在 Set-Cookie 中加入 SameSite 关键字,例如

Set-Cookie: key=value; HttpOnly; SameSite=Strict

SameSite 的两个属性

no_restriction

当没有添加 SameSite 关键字的时候,默认是空的

Lax

  • 允许发送安全 HTTP 方法(GET, HEAD, OPTIONS, TRACE)第三方链接的 cookies
  • 必须是 TOP-LEVEL 即可引起地址栏变化的跳转方式,例如 <a>, <link rel="prerender">, GET 方式的 form 表单,此外,XMLHttpRequest, <img> 等方式进行 GET 方式的访问将不会发送 cookies
  • 但是禁止发送不安全 HTTP 方法(POST, PUT, DELETE)第三方链接的cookies

Strict

禁止发送所有第三方链接的 cookies,默认情况下,如果添加了 SameSite 关键字,但是没有指定 value(Lax or Stict),那么默认为 Strict

当我们通过其他网站来访问一个有 SameSite-Cookies 机制的网站时,例如从 a.com 点击链接进入 b.com,如果 b.com 设置了 Set-Cookie:foo=bar;SameSite=Strict,那么,foo=bar 这一 cookie 是不会随着 request 发送的,如下图:

outerLink

可以看到 foo=bar 并没有发送,假如此时刷新一下页面或者直接从地址栏输入 b.com 的地址,那么 cookie 将会正常发送,不过假设用户点击了外链进入了 b.com,由于 SameSite-Cookies 的保护,他的 cookie 第一次受到了保护,但是假设用户此时手动刷新了一下页面,那么 cookie 将会被发送出去,所以可能需要其他一些验证手段来对 cookie 未随 request 发送的情况来做相对应的处理,不过该种情况仅限于 GET 方法下,如下图:

innerLink

此外,为了更清楚地了解 cookie 中哪些字段是 SameSite 控制并且级别为 Lax 或 Strict 或 none,我们可以在 Chrome DevTools 上清楚地看到,如下图:

devTools

点击查看 DEMO

参考资料:

reflow 和 repaint 简易分析

福利

浏览器渲染

当我们打开一个网页的时候,浏览器是如何将 HTML 代码转换为用户可见的视图的?浏览器又在何时进行 repaintreflow 的操作?首先我们要先知道用于该操作的 渲染树 的由来。

DOM Tree 的生成

浏览器首先会解析 HTML 代码,生成一颗 DOM Tree,DOM(文档对象模型)

简单地说,DOM Tree 的生成一般经历了四个阶段

  • 转换——浏览器将从供应方(例如本地磁盘或服务器)获取到的 HTML 字节,根据 HTML 的文件编码格式转换为字符
  • 符号化——浏览对转换好的字符串进行解析,将 <> 识别为对应的符号
  • 词法分析——将符号化的字符串转换为 对象,一般来说是节点(Node)
  • DOM 构建——对象生成完毕后,将根据对象之间的关系(父子、兄弟)生成 DOM Tree,在 DOM Tree 中可以确认 Node 节点间的关系

CSSOM 的生成

在生成 DOM Tree 以后,将会生成 CSSOM(CSS 对象模型)的树形结构

与构建 DOM 的过程类似是,CSSOM 的构建过程也是读取 CSS 字节,进行转换解析,并生成对应的 CSSOM Tree,不同的是,CSSOM 为 CSS 样式服务,而 DOM 为节点服务

CSSOM 能干什么?

CSSOM 通过复杂而具体的规则计算 CSS 样式,并将其映射到对应的需要样式的节点上,其遵循 向下层叠 的计算规则,例如下图

向下层叠

可以看到,body 处使用了 font-size: 16px,根据 向下层叠 的规则,body 的子节点如果没有其自己的 font-size 规则,那么 body 的 font-size 规则将会层叠给该节点

CSSOM 注意点

  • CSSOM 的构建会阻塞页面的渲染——假设页面的呈现没有等待 CSSOM 的构建和计算,那么用户看到的将会是一堆没有样式的页面,等到 CSSOM 构建以后,页面又突然间变成有样式的页面,所以等待 CSSOM 的构建完成再进行渲染并呈现页面是必须的,不过如果 CSSOM 的构建的效率很低,那么将会出现常见的 白屏 现象。
  • 只要重新加载页面,那么 CSSOM 也会重新构建——不管 CSS 样式文件是否被浏览器进行了缓存,CSSOM 是永远不会被缓存的,它会伴随页面的每一次重新加载而加载。
  • JS 的运行所阻塞会被 CSSOM 的构建阻塞——在构建 DOM 时,遇见 <script> 标签时,浏览器会发出 HTTP 请求资源,并将控制权移交给 JavaScript 引擎,等待 JS 执行完毕归还控制权继续 DOM 的构建,然而,如果此时 CSSOM 未下载并构建完成,JS 的执行时机将被延迟

Render Tree——渲染树

渲染树DOM TreeCSSOM Tree 融合构成,渲染树与 DOM TreeCSSOM Tree 不同,渲染树只包含需要渲染的节点信息,例如 display: none 的节点是不存在于 渲染树 内的

合成渲染树

可以发现,span 由于拥有 display: none 并未包含在 渲染树 里。

渲染树构建和渲染步骤

在这里我们简单说一下 渲染树 的构建和渲染步骤:

  • DOM Tree 根节点开始遍历所有可见节点
    • 不可见节点(如 <script><meta> 等)将不会包含在内,会被忽略
    • 通过 CSS 样式设置不可见的节点也不被包括,例如 display: none,不包括 visibility: hidden
  • CSSOM Tree 找到对应节点的规则,进行匹配
  • 发射可见节点,连带其内容及计算的样式
  • 根据生成的渲染树计算每个节点在屏幕中的绝对像素位置
  • 根据计算结果开始渲染,这一步通常称为 绘制 或者 栅格化

重绘(repaint)和重排(reflow)

上面我们知道了 渲染树DOM TreeCSSOM Tree 融合构建而成,但是页面并不是进行一次渲染就可以适应各种节点的几何属性改变等变化的,页面会在某些时机进行 重绘(repaint)重排(reflow)

我们首先要知道其二者的区别:

  • 重绘(repaint)——页面部分样式属性改变了(背景颜色,字体颜色等),但是几何属性没有改变,页面需要重绘该部分的内容,这就叫 重绘(repaint)
  • 重排(reflow)——页面节点的几何属性改变,这时候需要重新计算元素的几何属性,重新构建 渲染树,这就叫 重排(reflow)

同时,我们要记得一下这句话:

重绘不一定导致重排,但是重排一定会导致重绘

repaint 和 reflow 所带来的性能问题

通过前面的分析,可以预见的是,repaintreflow 所需的性能消耗代价必然巨大,下面通过一个例子来说明:

HTML:

1
2
3
4
5
6
<body>
<div id="elem-a"></div>
<div id="elem-b"></div>
<div id="elem-c"></div>
<div id="elem-d"></div>
</body>

JavaScript:

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
36
37
38
39
40
41
42
// example 1
(function() {
console.time("elem-a render time");
for(let idx=0;idx<10000;idx++) {
document.getElementById("elem-a").innerHTML += idx;
}
console.timeEnd("elem-a render time");
})();
// example 2
(function() {
console.time("elem-b render time");
let elemB = document.getElementById("elem-b");
for(let idx=0;idx<10000;idx++) {
elemB.innerHTML += idx;
}
console.timeEnd("elem-b render time");
})();
// example 3
(function() {
console.time("elem-c render time");
let str = "";
for(let idx=0;idx<10000;idx++) {
let elemC = document.getElementById("elem-c");
str += idx;
}
document.getElementById("elem-c").innerHTML = str;
console.timeEnd("elem-c render time");
})();
// example 4
(function() {
console.time("elem-d render time");
let elemD = document.getElementById("elem-d");
let str = "";
for(let idx=0;idx<10000;idx++) {
str += idx;
}
elemD.innerHTML += str;
console.timeEnd("elem-d render time");
})();

上面进行了 4 次试验,每次试验的内容不一样,下面进行分析:

  • example 1——进行 10000 次的 “DOM 索引 + 重绘 + 重排”
  • example 2——进行 10000 次的 “重绘 + 重排”,进行 1 次的 “DOM 索引”
  • example 3——进行 10000 次的 “DOM 索引”,进行 1 次的 “重绘 + 重排”
  • example 4——进行 10000 次的 “字符串拼接”,进行一次的 “DOM 索引 + 重绘 + 重排”

控制台打印结果如下:

  • elem-a render time: 6020.826ms
  • elem-b render time: 5797.140ms
  • elem-c render time: 14.061ms
  • elem-d render time: 3.905ms

由结果分析可知:

  • repaintreflow 消耗的性能是无比巨大的
  • DOM 索引也消耗一定的性能,但是比起 repaintreflow 简直是小巫见大巫
  • 优化的重点在于减少 DOM 重复索引和循环引起的 repaintreflow,尽量压缩为一次

如何减少 repaintreflow 的发生?

要知道如何减少 repaintreflow 的发生,我们就得先知道引起它们的原因, repaint 无疑就是改变 DOM 的背景颜色等导致,重点在于 reflow 的原因:

  • 页面初始化必须进行一次的 reflow
  • 缩放窗口
  • 改变字体
  • 添加或删除样式
  • 添加或删除元素
  • 内容改变,例如用户在输入框中输入文本
  • 激活了伪类样式,例如:hover
  • 脚本操作 DOM 并改变了其样式
  • 计算 offsetWidth 和 offsetHeight
  • 设置样式属性(width,height等)

以上种种都有可能引起页面的 reflow,而且不止这些,但是我们无法完全避免 reflow,所以我们必须想方设法去减少 reflow,例如:

  • 减少单一操作样式属性,使用 class 一次性替换
  • 对有动画的元素,使其 positionfixedabsolute,这样会减少元素间的影响
  • 使用平滑的过渡动画,例如尽量少用 1 个像素的移动动画,可以改为 3 个像素,具体原因
  • 避免使用 table 布局,具体原因
  • 减少在 CSS 样式中使用 JS 表达式
  • 将元素 display: none 后再修改样式
  • 创建一个新的节点元素,进行样式操作后替代原先的元素,不过可能会出现页面闪烁
  • 创建 DocumentFragment 来进行更新

这里提一下浏览器自身对减少 reflow 的优化,下面例子:

1
2
3
4
5
let elemA = document.getElementById("elem-a");
elemA.style.width = "100px";
elemA.style.height = "100px";
elemA.style.backgroundColor = "yellow";

以上例子浏览器只会一次性进行 reflow 而非 3 次

1
2
3
4
5
6
let elemA = document.getElementById("elem-a");
elemA.style.width = "100px";
elemA.style.height = "100px";
elemA.getComputedStyle();
elemA.style.backgroundColor = "yellow";

以上例子浏览器会进行 2 次 reflow,因为中间需要获取当前的样式信息,浏览器必须先进行 渲染树 的重新计算,只要是获取以下样式信息的,都会引起浏览器立即重新渲染(如果必须则会 reflow):

  • offsetTop
  • offsetLeft
  • offsetWidth
  • offsetHeight
  • scrollTop
  • scrollLeft
  • scrollWidth
  • scrollHeight
  • clientTop
  • clientLeft
  • clientWidth
  • clientHeight
  • getComputedStyle()
  • currentStyle(just for IE)

参考

angular 注意点(长期更新)

这篇 blog 主要存放我在学习和使用 angular 踩到的坑和需要注意的点

ng-repeat

ng-repeat 用于标识某个 elem 需要重复输出,同时重复输出的内容需为唯一

1
2
3
<div ng-app="app" ng-controller="control">
<h3 ng-repeat="content in repeatContent">ng-repeat: {{ content }}</h3>
</div>
1
2
3
4
5
6
7
let app = angular.module("app", []);
app.controller("control", ($scope) => {
// 输出李滨泓
$scope.repeatContent = ["李", "滨", "泓"];
// 下面存在两个“泓”,会报错
// $scope.repeatContent = ["李", "滨", "泓", "泓"];
})

provider, service, factory 之间的关系

factory

factory 很像 service,不同之处在于,service 在 Angular 中是一个单例对象,即当需要使用 service 时,使用 new 关键字来创建一个(也仅此一个)service。而 factory 则是一个普通的函数,当需要用时,他也仅仅是一个普通函数的调用方式,它可以返回各种形式的数据,例如通过返回一个功能函数的集合对象来将供与使用。

定义:

1
2
3
4
5
6
7
8
9
10
11
let app = angular.module("app", []);
// 这里可以注入 $http 等 Provider
app.factory("Today", () => {
let date = new Date();
return {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate()
};
});

使用注入:

1
2
3
4
5
app.controller("control", (Today) => {
console.log(Today.year);
console.log(Today.month);
console.log(Today.day);
});

service

service 在使用时是一个单例对象,同时也是一个 constructor,它的特点让它可以不返回任何东西,因为它使用 new 关键字新建,同时它可以用在 controller 之间的通讯与数据交互,因为 controller 在无用时其作用域链会被销毁(例如使用路由跳转到另一个页面,同时使用了另一个 controller)

定义:

1
2
3
4
5
6
7
8
9
10
11
let app = angular.module("app", []);
// 这里可以注入 $http 等 Provider
// 注意这里不可以使用 arrow function
// arrow function 不能作为 constructor
app.service("Today", function() {
let date = new Date();
this.year = date.getFullYear();
this.month = date.getMonth() + 1;
this.day = date.getDate();
});

使用注入:

1
2
3
4
5
app.controller("control", (Today) => {
console.log(Today.year);
console.log(Today.month);
console.log(Today.day);
});

provider

providerservice 的底层创建方式,可以理解 provider 是一个可配置版的 service,我们可以在正式注入 provider 前对 provider 进行一些参数的配置。

定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let app = angular.module("app", []);
// 这里可以注入 $http 等 Provider
// 注意这里不可以使用 arrow function
// arrow function 不能作为 constructor
app.provider("Today", function() {
this.date = new Date();
let self = this;
this.setDate = (year, month, day) => {
this.date = new Date(year, month - 1, day);
}
this.$get = () => {
return {
year: this.date.getFullYear(),
month: this.date.getMonth() + 1,
day: this.date.getDate()
};
};
});

使用注入:

1
2
3
4
5
6
7
8
9
10
11
// 这里重新配置了今天的日期是 2015年2月15日
// 注意这里注入的是 TodayProvider,使用驼峰命名来注入正确的需要配置的 provider
app.config((TodayProvider) => {
TodayProvider.setDate(2015, 2, 15);
});
app.controller("control", (Today) => {
console.log(Today.year);
console.log(Today.month);
console.log(Today.day);
});

handlebars 与 angular 符号解析冲突

场景:

当我使用 node.js 作为服务端,而其中使用了 handlebars 作为模板引擎,当 node.js 对某 URL 进行相应并 render,由于其模板使用 { {} } 作为变量解析符号。同样地,angular 也使用 { {} } 作为变量解析符号,所以当 node.js 进行 render 页面后,如果 { {} } 内的变量不存在,则该个区域会被清空,而我的原意是这个作为 angular 的解析所用,而不是 handlebars 使用,同时我也想继续使用 handlebars,那么此时就需要将 angular 默认的 { {} } 解析符号重新定义。即使用依赖注入 $interpolateProvider 进行定义,如下示例:

1
2
3
4
app.config($interpolateProvider => {
$interpolateProvider.startSymbol('{[{');
$interpolateProvider.endSymbol('}]}');
});

ng-annotate-loader

ng-annotate-loader 应用于 webpack + angular 的开发场景,是用于解决 angular 在进行 JS 压缩后导致依赖注入失效并出现错误的解决方法

安装

1
$ npm install ng-annotate-loader --save-dev

配置

1
2
3
4
5
6
// webpack.config.js
{
test: /\.js?$/,
exclude: /(node_modules|bower_components)/,
loader: 'ng-annotate!babel?presets=es2015'
},

双向数据绑定

当我们使用非 Angular 自带的事件时,$scope 里的数据改变并不会引起 $digestdirty-checking 循环,这将导致当 model 改变时,view 不会同步更新,这时我们需要自己主动触发更新

HTML

1
2
<div>{{ foo }}</div>
<button id="addBtn">go</button>

JavaScript

1
2
3
4
5
6
app.controller("control", ($scope) => {
$scope.foo = 0;
document.getElementById("addBtn").addEventListener("click", () => {
$scope.foo++;
}, false);
})

很明显,示例的意图是当点击 button 时,foo 自增长并更新 View,但是实际上,$scope.foo 是改变了,但是 View 并不会刷新,这是因为 foo 并没有一个 $watch 检测变化后 $apply,最终引起 $digest,所以我们需要自己触发 $apply 或者创建一个 $watch 来触发或检测数据变化

JavaScript(使用 $apply)

1
2
3
4
5
6
7
8
9
10
app.controller("control", ($scope) => {
$scope.foo = 0;
document.getElementById("addBtn").addEventListener("click", () => {
$scope.$apply(function() {
$scope.foo++;
});
}, false);
})

JavaScript(使用 $watch & $digest)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.controller("control", ($scope) => {
$scope.foo = 0;
$scope.flag = 0;
$scope.$watch("flag", (newValue, oldValue) => {
// 当 $digest 循环检测 flag 时,如果新旧值不一致将调用该函数
$scope.foo = $scope.flag;
});
document.getElementById("addBtn").addEventListener("click", () => {
$scope.flag++;
// 主动触发 $digest 循环
$scope.$digest();
}, false);
})

$watch(watchExpression, listener, [objectEquality])

注册一个 listener 回调函数,在每次 watchExpression 的值发生改变时调用

  • watchExpression 在每次 $digest 执行时被调用,并返回要被检测的值(当多次输入同样的值时,watchExpression 不应该改变其自身的值,否则可能会引起多次的 $digest 循环,watchExpression 应该幂等)
  • listener 将在当前 watchExpression 返回值和上次的 watchExpression 返回值不一致时被调用(使用 !== 来严格地判断不一致性,而不是使用 == 来判断,不过 objectEquality == true 除外)
  • objectEqualityboolean 值,当为 true 时,将使用 angular.equals 来判断一致性,并使用 angular.copy 来保存此次的 Object 拷贝副本供给下一次的比较,这意味着复杂的对象检测将会有性能和内存上的问题

$apply([exp])

$apply$scope 的一个函数,用于触发 $digest 循环

$apply 伪代码

1
2
3
4
5
6
7
8
9
function $apply(expr) {
try {
return $eval(expr);
} catch (e) {
$exceptionHandler(e);
} finally {
$root.$digest();
}
}

  • 使用 $eval(expr) 执行 expr 表达式
  • 如果在执行过程中跑出 exception,那么执行 $exceptionHandler(e)
  • 最后无论结果,都会执行一次 $digest 循环

webpack + Babel 使用 ES6 新特性

webpack 是一个模块加载器兼打包工具,Babel 是一款转码编译器,可以很方便地将 ES6、ES7 等当前浏览器不兼容的 JavaScript 新特性转码为 ES5 等当前浏览器普遍兼容的代码。将两者结合起来可以很方便地在项目中一边使用 ES6 编写代码,一边自动生成 ES5 代码

webpack & Babel

安装 webpack

1
npm install webpack -g

或者

1
2
npm init
npm install webpack --save-dev

安装 Babel 相关组件

1
2
3
4
5
6
# 安装加载器 babel-loader 和 Babel 的 API 代码 babel-core
npm install --save-dev babel-loader babel-core
# 安装 ES2015(ES6)的代码,用于转码
npm install babel-preset-es2015 --save-dev
# 用于转换一些 ES6 的新 API,如 Generator,Promise 等
npm install --save babel-polyfill

你可以在以下页面查看 JavaScript 的所需的转换代码模块进行按需安装
http://babeljs.io/docs/plugins/preset-es2015/

配置

类似于 Grunt 和 Gulp,webpack 也有其特定地配置文件 webpack.config.js

如下配置使用babel:

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
module.exports = {
entry: [
"babel-polyfill",
"./index.js"
],
output: {
path: __dirname + '/output/',
publicPath: "/output/",
filename: 'index.js'
},
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader', // 'babel-loader' is also a legal name to reference
query: {
presets: ['es2015']
}
}
]
}
};

  • entry——用于设置 webpack 执行打包文件的入口,是一个数组
  • output——用于指定生成文件的路径以及文件名等
    • path——指定生成文件路径
    • publicPath——指定域名公共路径
    • filename——指定生成文件的名称
  • module——主要用于配置 loaders
    • loaders——用于配置对应后缀的文件使用何种加载器进行处理
      • test——使用正则表达式来指定某种特定的文件类型
      • exclude——排除某个文件夹下的文件进行处理
      • loader——指定相应的加载器,多个加载器使用 ! 进行连接,每个 loader 都可以省略其后缀,如 babel-loader 可以写成 babel
      • query——指定加载器的配置信息,也可以使用 ? 直接连接在 loader 后面

以上只是涉及到我目前用到的一些配置信息的说明,更多的配置信息可以查阅官方文档,地址如下:
https://webpack.github.io/docs/configuration.html

开始

在有 webpack.config.js 文件的目录下使用一下命令行:

  • webpack——直接启动 webpack,默认配置文件为 webpack.config.js
  • webpack -w——监测启动 webpack,实时打包更新文件
  • webpack -p——对打包后的文件进行压缩

更多的命令可以查看官方文档下的说明,地址如下:
http://webpack.github.io/docs/cli.html

ES6 学习笔记(七)

Generator

Generator 函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同

声明

1
2
3
4
5
6
7
8
9
10
11
12
13
// 这里的 * 只要在 function 和函数名之间使用就可以
function* iterator() {
yield "x";
yield "y";
return "z";
}
let iter = iterator();
iter.next(); // {value: "x", done: false}
iter.next(); // {value: "y", done: false}
iter.next(); // {value: "z", done: true}
iter.next(); // {value: undefined, done: true}

调用 Generator 函数并不会如同普通函数一样返回函数的返回值,而是返回一个迭代器对象。yield 用于惰性执行 Generator 函数,当调用迭代器对象的 next() 时,就会执行函数内的代码知道遇到一个 yield 声明,并且其后面的值作为 value 的值返回,如果程序执行完毕,那么 donetrue,反之是 false

next()

next() 是 Generator 函数的步骤执行的调用者,同时可以利用它来为函数内部注入值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
// 一开始 x 为 5,所以 yield 惰性求值返回为 6
var b = foo(5);
b.next() // { value:6, done:false }
// 这里相当于声明上一个 yield 的返回值是 12,所以 y 等于 2*12
b.next(12) // { value:8, done:false }
// 这里相当于声明上一个 yield 的返回值是 13,所以 z 等于 13
b.next(13) // { value:42, done:true }

使用 for…of

由于 Generator 函数调用返回一个迭代器对象 ,那么他就可以被所有支持迭代器遍历的特性使用,例如 for…of

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
// 由于当迭代器返回的 done 是 true 就停止遍历,所以 6 并不会被输出

除了 for…of,... 运算符和 Array.from() 同样支持 Generator

Generator.prototype.throw()

Generator函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
// 内部捕获后不再捕获此后的错误
i.throw('a');
// 内部不捕获,传递到外部
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

Generator.prototype.return()

Generator函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历Generator函数

1
2
3
4
5
6
7
8
9
10
11
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }

yield*

yield* 用于在 Generator 函数内调用另一个 Generator 函数

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
// 另外一个例子
function* inner() {
yield 'hello!';
}
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一个遍历器对象
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"

Generator 函数的丰富超乎想象,这里写的笔记只是其冰山一角的特性,更多还是需要阅读相关书籍和资料
以上实例代码大部分来自阮一峰老师的 《ECMAScript 6 入门》 一书
书籍在线阅读地址: http://es6.ruanyifeng.com/#README

ES6 学习笔记(六)

Iterator

Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of消费

部署 Iterator 接口

数组、Map、Set、类数组都有部署 Iterator 的接口,但是对象并没有,下面展示如何为对象部署 Iterator 接口

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// 第一种
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
// Symbol.Iterator 即部署接口
[Symbol.iterator]() { return this; }
// next() 代表迭代器的指针运动逻辑
// value 默认是当前值
// done 表示当前值是否为末尾,布尔值
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
} else {
return {done: true, value: undefined};
}
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (var value of range(0, 3)) {
console.log(value);
}
// 第二种
function Obj(value){
this.value = value;
this.next = null;
}
Obj.prototype[Symbol.iterator] = function(){
var iterator = {
next: next
};
var current = this;
function next(){
if (current){
var value = current.value;
var done = current === null;
current = current.next;
return {
done: done,
value: value
}
} else {
return {
done: true
}
}
}
return iterator;
}
var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);
one.next = two;
two.next = three;
for (var i of one){
console.log(i)
}
// 1
// 2
// 3
// 第三种
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};

上面的三个例子都有一个共同点,就是部署 Iterator 接口都是在对象的 Symbol.iterator 属性中返回一个对象,而该对象包含一个 next() 函数指明指针的运动逻辑,并返回 valuedone 两个值。还有另外一个 return() 方法用于在中断遍历时的操作,必须返回一个对象。此外,也可以直接使用数组的 Iterator 作为对象的迭代器

1
2
3
4
5
6
7
8
9
10
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // 'a', 'b', 'c'
}

注意,如果 Symbol.iterator 属性返回并不是一个迭代器的标准对象,那么会报错

1
2
3
4
5
var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj] // TypeError: [] is not a function

Iterator 的使用场景

解构赋值

对数组的解构赋值默认会调用 Symbol.iterator 方法

1
2
3
4
5
6
7
let set = new Set().add('a').add('b').add('c');
let [x,y] = set;
// x='a'; y='b'
let [first, ...rest] = set;
// first='a'; rest=['b','c'];

扩展运算符

1
2
3
4
5
6
7
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']

yield*

这个还没学到,不予评论,后面学到了再填坑

其他场景

  • for…of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([[‘a’,1],[‘b’,2]]))
  • Promise.all()
  • Promise.race()
  • 字符串也部署了 Iterator 接口

for…of

  • for…of 遍历数组,key 值保持为数字,而 for…in 会把 key 值变为字符串
  • for…of 可以 break 或 return,而 forEach() 不可以
  • for…of 可以正确识别 32 位 UTF-16 字符

以上实例代码大部分来自阮一峰老师的 《ECMAScript 6 入门》 一书
书籍在线阅读地址: http://es6.ruanyifeng.com/#README

ES6 学习笔记(五)

Set

Set 是一个类数组的数据结构,但是它的值都是独一无二的,并且它是一个构造函数

Set 有以下几个特点

  • 值是不重复的
  • NaN 等于自身
  • 添加值的时候不会进行类型转换,5 和 ‘5’ 是不一样的
  • 可以接受一组数组进行初始化,会自动过滤重复的数
  • 两个空对象不互相相等

Set 的方法

  • Set.prototype.constructor:构造函数,默认就是Set函数
  • Set.prototype.size:返回Set实例的成员总数
  • add(value):添加某个值,返回Set结构本身,由于返回值的特点,可以链式调用

    1
    s.add(1).add(2).add(2);
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功

  • has(value):返回一个布尔值,表示该值是否为Set的成员
  • clear():清除所有成员,没有返回值
  • keys():返回一个键名的遍历器
  • values():返回一个键值的遍历器
  • entries():返回一个键值对的遍历器
  • forEach():使用回调函数遍历每个成员

WeakSet

WeakSet 有以下几个特点

  • 加入成员只能是对象
  • 成员都是弱引用,即垃圾回收机制不会因其引用对象而保持对象的存在
  • 无法引用成员对象
  • 无法变量成员对象

WeakSet 的方法

  • WeakSet.prototype.add(value):向WeakSet实例添加一个新成员
  • WeakSet.prototype.delete(value):清除WeakSet实例的指定成员
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在WeakSet实例之中

Map

与对象的 key-value 结构类似的一种集合数据结构,不用的是 Map 可以使用任意一种类型的值作为 key,不会发生自动转换为字符串的情况

  • size:返回Map结构的成员总数
  • set(key, value):设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键
  • get(key):读取key对应的键值,如果找不到key,返回undefined
  • has(key):返回一个布尔值,表示某个键是否在Map数据结构中
  • delete(key):删除某个键,返回true。如果删除失败,返回false
  • clear():清除所有成员,没有返回值

WeakMap

与 WeakSet 的特点类似,不过这个相对于 Map 的数据结构的,只能使用 get()set()has()delete()

以上实例代码大部分来自阮一峰老师的 《ECMAScript 6 入门》 一书
书籍在线阅读地址: http://es6.ruanyifeng.com/#README

ES6 学习笔记(四)

二进制数组

二进制数组不是数组,只是类数组,二进制数组用于直接操作内存

ArrayBuffer(long)

ArrayBuffer 用于创建一段连续的内存地址,long 表示内存的长度(字节),但是不可以直接读写,只能通过 视图 读写,例如 TypeArrayArrayBuffer 每个内存地址默认值都为 0。不同 TypeArray 操作同一组内存会互相影响

1
2
3
4
5
6
7
8
9
10
11
12
13
var buf = new ArrayBuffer(32);
var dataView = new DataView(buf);
dataView.getUint8(0) // 0
// 操作同一组内存
var buffer = new ArrayBuffer(12);
var x1 = new Int32Array(buffer);
x1[0] = 1;
var x2 = new Uint8Array(buffer);
x2[0] = 2;
x1[0] // 2

Array.prototype.byteLength

获取分配的内存字节长度

1
2
3
4
5
6
7
8
9
10
var buffer = new ArrayBuffer(32);
buffer.byteLength
// 32
if(buffer.byteLength) {
// success
}else {
// error
// 分配的内存空间太大,可能存在没有那么大的内存空间而失败的情况
}

Array.prototype.slice(start, end)

生成一段新的内存,拷贝 start 到 end-1 位置的内容到新的内存

1
2
var buffer = new ArrayBuffer(8);
var newBuffer = buffer.slice(0, 3);

ArrayBuffer.isView(buffer)

判断 buffer 是否是视图的实例化对象,该方法为 ArrayBuffer 的静态方法,直接调用

1
2
3
4
5
var buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false
var v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true

TypeArray

TypeArray 不是一个构造函数,而是一组构造函数,它包含 9 个不同的视图构造函数,都是类数组,都能用下标访问,都有 length 属性

  • Int8Array:8位有符号整数,长度1个字节。
  • Uint8Array:8位无符号整数,长度1个字节。
  • Uint8ClampedArray:8位无符号整数,长度1个字节,溢出处理不同。
  • Int16Array:16位有符号整数,长度2个字节。
  • Uint16Array:16位无符号整数,长度2个字节。
  • Int32Array:32位有符号整数,长度4个字节。
  • Uint32Array:32位无符号整数,长度4个字节。
  • Float32Array:32位浮点数,长度4个字节。
  • Float64Array:64位浮点数,长度8个字节。

TypedArray(buffer [, byteOffset [, length]])

TypeArray 的构造函数可以传入三个参数

  • 第一个参数,ArrayBuffer 的实例对象,必选
  • 第二个参数,从 ArrayBuffer 的第 byteOffset 个字节开始读取,默认从 0 开始,注意,如果是 16 位的视图,需要是 2 的倍数,如果是 32 位 的视图,需要是 4 的倍数,以此类推,因为例如是 32 位的视图,那么该视图中 4 个字节为一个下标,为了完整读取内存,必须是每一个下标字节大小的整数倍,可选
  • 第三个参数,读取 ArrayBuffer 的 length 个字节,默认读取到末尾,可选
1
2
3
4
5
6
7
8
9
10
11
// 创建一个8字节的ArrayBuffer
var b = new ArrayBuffer(8);
// 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾
var v1 = new Int32Array(b);
// 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾
var v2 = new Uint8Array(b, 2);
// 创建一个指向b的Int16视图,开始于字节2,长度为2
var v3 = new Int16Array(b, 2, 2);

TypeArray(length)

TypeArray 的构造函数还能直接通过分配内存创建视图

1
2
3
4
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

TypeArray(typedArray)

TypeArray 的构造函数能接受另外一个 TypeArray 的实例作为构造参数,新的实例是开辟了新的内存地址同时复制了相同的值,不会互相影响

1
2
3
4
5
6
7
var x = new Int8Array([1, 1]);
var y = new Int8Array(x);
x[0] // 1
y[0] // 1
x[0] = 2;
y[0] // 1

TypedArray(arrayLikeObject)

TypeArray 的构造函数能接受一个类数组的参数,会开辟一段新的内存实例化视图

1
2
3
4
5
6
7
var x = new Int8Array([1, 1]);
var y = new Int8Array(x.buffer);
x[0] // 1
y[0] // 1
x[0] = 2;
y[0] // 2

数组方法

普通数组的方法对 TypeArray 也完全适用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 缺少 contact 方法
TypedArray.prototype.copyWithin(target, start[, end = this.length])
TypedArray.prototype.entries()
TypedArray.prototype.every(callbackfn, thisArg?)
TypedArray.prototype.fill(value, start=0, end=this.length)
TypedArray.prototype.filter(callbackfn, thisArg?)
TypedArray.prototype.find(predicate, thisArg?)
TypedArray.prototype.findIndex(predicate, thisArg?)
TypedArray.prototype.forEach(callbackfn, thisArg?)
TypedArray.prototype.indexOf(searchElement, fromIndex=0)
TypedArray.prototype.join(separator)
TypedArray.prototype.keys()
TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
TypedArray.prototype.map(callbackfn, thisArg?)
TypedArray.prototype.reduce(callbackfn, initialValue?)
TypedArray.prototype.reduceRight(callbackfn, initialValue?)
TypedArray.prototype.reverse()
TypedArray.prototype.slice(start=0, end=this.length)
TypedArray.prototype.some(callbackfn, thisArg?)
TypedArray.prototype.sort(comparefn)
TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
TypedArray.prototype.toString()
TypedArray.prototype.values()

小端字节序

如果使用一个 32 位的视图读取并修改一段内存后,再使用一个 16 位的视图读取该段内存缓冲时,其长度应该是 32 位视图的两倍,这时候 16 位视图中的数据存放就会出现一个现象,跟字节排放的规则相关,看示例

1
2
3
4
5
6
7
8
9
10
11
12
var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
for (var i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}
console.log(int32View); // [0, 2, 4, 6]
var int16View = new Int16Array(buffer);
console.log(int16View); // [0, 0, 2, 0, 4, 0, 6, 0]

由于 32 位视图是 4 个字节存放一个数据,16 位视图是 2 个字节存放一个数据,那么 16 位视图明显要使用 2 个数据位来存放一个 32 位视图的数据,而之所以 16 位视图都是两个数据位的第一个数据位显示为正确数据,第二个显示为 0,这就涉及现代计算机都采用了小端字节序的规则在内存中存放数据的原因,而 TypeArray 也是采用该种策略。小端字节序简略地说就是从后往前一个字节一个字节地存放数据到内存的每个地址中,更详细的解释可以参考:http://blog.csdn.net/ce123/article/details/6971544 小端字节序也导致了 TypeArray 无法正确读取大端字节序的内容

TypeArray.prototype.BYTES_PER_ELEMENT

用于输出每一个 TypeArray 类型视图的每一位所占的字节数

1
2
3
4
5
6
7
8
Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Int16Array.BYTES_PER_ELEMENT // 2
Uint16Array.BYTES_PER_ELEMENT // 2
Int32Array.BYTES_PER_ELEMENT // 4
Uint32Array.BYTES_PER_ELEMENT // 4
Float32Array.BYTES_PER_ELEMENT // 4
Float64Array.BYTES_PER_ELEMENT // 8

溢出

TypeArray 对于溢出的处理就是直接抛弃溢出的位

  • 正向溢出(overflow):当输入值大于当前数据类型的最大值,结果等于当前数据类型的最小值加上余值,再减去1
  • 负向溢出(underflow):当输入值小于当前数据类型的最小值,结果等于当前数据类型的最大值减去余值,再加上1
1
2
3
4
5
6
7
var int8 = new Int8Array(1);
int8[0] = 128;
int8[0] // -128
int8[0] = -129;
int8[0] // 127

其他方法

  • TypedArray.prototype.buffer,返回视图整段内存引用
  • TypedArray.prototype.byteLength,返回视图的内存字节长度
  • TypedArray.prototype.byteOffset,返回视图从内存第几个字节开始引用
  • TypedArray.prototype.length,返回视图的成员长度,与其每个成员占用字节数相关
  • TypedArray.prototype.set(typeArray[, startIndex]),将一段内存完全复制到另一端内存中
  • TypedArray.prototype.subarray(startIndex, endIndex),从一个视图上截取一部分创建为一个新的视图
  • TypedArray.prototype.slice(index),从一个视图上截取一段创建一个新的视图,index 可以为负数,表示倒数
  • TypedArray.of(),将参数创建为一个新的视图
  • TypedArray.from(arrayLike[, callback]),将 arrayLike 转化为一个相应的可遍历的视图类型,callback 的功能类似于数组的 map 功能

DataView

DataView 可以自行设置大端字节序或者小端字节序,同时有 8 个获取数据的方法和 8 个设置数据的方法

  • getInt8:读取1个字节,返回一个8位整数。
  • getUint8:读取1个字节,返回一个无符号的8位整数。
  • getInt16:读取2个字节,返回一个16位整数。
  • getUint16:读取2个字节,返回一个无符号的16位整数。
  • getInt32:读取4个字节,返回一个32位整数。
  • getUint32:读取4个字节,返回一个无符号的32位整数。
  • getFloat32:读取4个字节,返回一个32位浮点数。
  • getFloat64:读取8个字节,返回一个64位浮点数。

这 8 个方法都传入一个正整数表示从第几个字节开始读取,第二个参数为一个布尔值,true 代表使用小端字节序,false 代表使用大端字节序,默认为大端字节序

  • setInt8:写入1个字节的8位整数。
  • setUint8:写入1个字节的8位无符号整数。
  • setInt16:写入2个字节的16位整数。
  • setUint16:写入2个字节的16位无符号整数。
  • setInt32:写入4个字节的32位整数。
  • setUint32:写入4个字节的32位无符号整数。
  • setFloat32:写入4个字节的32位浮点数。
  • setFloat64:写入8个字节的64位浮点数。

这 8 个方法都可以传入三个参数,第一个表示从第几个字节写入数据,第二个表示写入数据的内容,第三个指定大小端字节序

其他一些新特性

AJAX

AJAX 可以设置 responseTypearraybuffer 明确指定响应数据位二进制数据,如果不明确,可以设置为 blod

Canvas

Canvas 使用 Uint8ClampedArray 读取二进制像素数据,该 TypeArray 可以自动过滤高位溢出,取值范围始终为 0-255

WebSocket

WebSocket 可以发送二进制数据

Fetch API

Fetch API取回的数据,就是 ArrayBuffer 对象

File API

File API 可以将文件数据作为二进制的 ArrayBuffer 类型读取处理

以上实例代码大部分来自阮一峰老师的 《ECMAScript 6 入门》 一书
书籍在线阅读地址: http://es6.ruanyifeng.com/#README

Stay folish<br><br>Stay hungry