├── 0和1编码的背后.md
├── README.md
├── VLQ编码.md
└── 布局.md
/0和1编码的背后.md:
--------------------------------------------------------------------------------
1 | # 0 和 1 编码的背后
2 | ## 二进制的一些基础知识
3 | ### 机器数和真值
4 | 1. 机器数
5 | 一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器数是带符号的,最高位存放一个符号位,非负数为 0,负数为 1。比如,十进制中的 +3,计算机字长为 8 位,转换成二进制就是 00000011。如果是 -3,就是 10000011。
6 | 2. 真值
7 | 由于机器数的第一位是符号位,所以机器数的形式值不等于真正的数值。例如上面的有符号数 10000011,最高位是1,代表是负数,真正数值是 -3 而不是形式值 131。
8 |
9 | ### 原码,反码,补码
10 | 1. 原码
11 | 原码就是符号位加上真值的绝对值,比如如果是 8 位二进制:
12 | ``` JavaScript
13 | [+1]原 = 0000 0001
14 | [-1]原 = 1000 0001
15 | ```
16 | 第一位是符号位,所以 8 位二进制数的取值范围是:
17 | ``` JavaScript
18 | [1111 1111, 0111 1111] 即 [-127, 127]
19 | ```
20 | 2. 反码
21 | 正数的反码是其本身,负数的反码是在其原码的基础上,符号位不变,其余各个位取反:
22 | ``` JavaScript
23 | [+1] = [0000 0001]原 = [0000 0001]反
24 | [-1] = [1000 0001]原 = [1111 1110]反
25 | ```
26 |
27 | 3. 补码
28 | 正数的补码是其本身,负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后 +1
29 | ```JavaScript
30 | [+1] = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
31 | [-1] = [1000 0001]原 = [1111 1110]反 = [1111 1111]补
32 | ```
33 |
34 | ### 为何要使用原码,反码和补码
35 | 原码可以被人脑直接识别并用于计算表示方式,因为人脑计算时自动识别第一位的符号位,在计算时候根据符号位选择对真值区的加减。但是对于计算机,为了尽可能的设计简便,如果计算机“运算”还要额外考虑符号位是否参与运算,那基础的电路设计会变得更加复杂。根据运算法则,减去一个正数等于加上一个负数,即 1 + 1 = 1 + (-1) = 0,所以如果能符号位参与运算,那么四则运算里只用加法就能完成“加减”这两种运算了。
36 |
37 | 下面我们来看看 1 - 1 = 0,用这几种码符号位也参与运算的结果
38 |
39 | ``` JavaScript
40 | 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [1000 0010]原 = -2
41 |
42 | 1 + (-1) = [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0
43 |
44 | 1 + (-1) = [0000 0001]补 + [1111 1111]补 = [0000 0000]补 = [0000 0000]原 = 0
45 | ```
46 |
47 | 所有的运算结果都是正确的,只是原码的符号位参与运算似乎不符合我们的数值运算结果,反码的符号位参与运算产生了一个没有“意义”的 -0,补码的符号位参与运算不仅修复了 0 的符号问题,而且得到正确的结果,这背后不是偶然的随意反转数字得到的结果。
48 |
49 | 在补码运算结果仲,-1 - 127 结果应该是 -128, 用补码运算:
50 |
51 | ```JavaScript
52 | -1 + (-127) = [1111 1111]补 + [1000 0001]补 = [1000 0000]补
53 | ```
54 |
55 | 在用补码运算的结果中,[1000 0000]补 就是 -128。但是注意因为实际上是实用以前的 -0 的补码来表示 -128,所以 -128并没有原码和反码表示。这就是为什么 8 位二进制,实用原码或反码表示的范围是 [-127, 127],而实用补码表示的范围是 [-128, 127],各位可以依次类推出 16 位、32 位、64 位的补码所能表示的范围。[1]
56 |
57 | ## 二进制位运算
58 | 1. 按位与运算符(&)
59 |
60 | 运算规则:两位同时为“1”,结果才为“1”,否则为“0”。
61 | ```JavaScript
62 | 0 & 0 = 0; 0 & 1 = 0; 1 & 0 = 0; 1 & 1 = 1;
63 | ```
64 |
65 | 2. 按位或运算符(|)
66 |
67 | 运算规则:两位中有一位为“1”,结果为“1”,否则为“0”。
68 | ```JavaScript
69 | 0 | 0 = 0; 0 | 1 = 1; 1 | 0 = 1; 1 | 1 = 1;
70 | ```
71 |
72 | 3. 按位异或运算符(^)
73 |
74 | 运算规则:两位相同结果为“0”,不同结果为“1”。
75 | ```JavaScript
76 | 0 ^ 0 = 0; 0 ^ 1 = 1; 1 ^ 0 = 1; 1 ^ 1 = 0;
77 | ```
78 | 与 0 异或运算保留原值,与自身异或结果为 0。
79 |
80 | 4. 按位非运算符(~),也称作取反运算符
81 |
82 | 运算规则:一元运算符,自身的每一位取反。
83 | ```JavaScript
84 | ~1 = 0; ~0 = 1;
85 | ```
86 |
87 | 5. 左移运算符(<<)
88 |
89 | 运算规则:将运算对象的各个二进制位全部左移若干位,左边二进制位丢弃,右边补 0。
90 | ```JavaScript
91 | 在 8 位二进制,数值 5 的二进制码是:0000 0101
92 | 5 << 2 结果是 10
93 | 0000 0101 << 2 结果是 0000 1010
94 | ```
95 |
96 | 6. 右移运算符(>>)
97 |
98 | 运算规则:将运算对象的各个二进制位全部右移若干位,正数左补 0, 负数左补 1,右边丢弃。
99 | ```JavaScript
100 | 在 8 位二进制,数值 5 的二进制原码是:0000 0101
101 | 5 >> 2 结果是 1
102 | 0000 0101 >> 2 结果是 0000 0001
103 | ```
104 |
105 | 7. 无符号右移运算符(>>>)
106 |
107 | 运算规则:将运算对象的各个二进制位全部右移若干位,右移后左边空出的位置补 0,移出右边的位丢弃。
108 | ```JavaScript
109 | 在 8 位二进制,数值 -5 的二进制原码是:1000 0101
110 | -5 >>> 2 结果是 33
111 | 1000 0101 >>> 2 结果是 0010 0001
112 | ```
113 |
114 | 8. 不同长度的数据进行位运算
115 |
116 | 运算规则:如果两个不同长度的数据进行位运算,系统会将二者按右端对齐,然后进行位运算。对齐后左边不足的位依照下面三种情况补足
117 | - 如果整型数据为正数,左边补 0。
118 | - 如果整型数据为负数,左边补 1。
119 | - 如果整型数据为无符号数,左边补 0。
120 |
121 | ## 逻辑运算符,又称为布尔运算符
122 | 1. 逻辑运算符,具有短路特性,进行逻辑与(&&)运算时会从左往右返回首先出现的 falsy 的值;逻辑或(||)则相反,会返回首先出现的 tury 的值。如果都能运算到最右边的值,结果则为最右的值。
123 | ``` JavaScript
124 | 例如:
125 | let a = 1 && 2 && 3 && 0 && 4 && 5 //a = 0
126 | let b = false || 0 || '' || 1 || null || undefined //b = 1
127 | ```
128 |
129 | 2. 逻辑非运算符(!),进行取反运算。通常可对值进行 (!!)运算得到一个值的原始布尔值。
130 |
131 |
132 | > [[1]计算机为何能巧妙把符号位参与运算,将减法变成加法?](https://www.zhihu.com/question/30395946)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## note
2 | - [0和1编码的背后](https://github.com/D-kylin/note/blob/master/0%E5%92%8C1%E7%BC%96%E7%A0%81%E7%9A%84%E8%83%8C%E5%90%8E.md)
3 |
4 | - [VQL编码](https://github.com/D-kylin/note/blob/master/VLQ%E7%BC%96%E7%A0%81.md)
5 |
6 | - [布局](https://github.com/D-kylin/note/blob/master/%E5%B8%83%E5%B1%80.md)
7 |
8 | #### Tips
9 | 如果您在github 网站打开 markdown 文档时发现无法显示,建议将浏览器的 markdown 插件关闭后刷新页面。
10 |
--------------------------------------------------------------------------------
/VLQ编码.md:
--------------------------------------------------------------------------------
1 | # Base64 VLQ
2 | 这个概念是 source map 的核心,分为 Base64 编码和 VLQ 编码两种编码技术,本文主要围绕 VLQ 编码展开,如果您对 Base64 编码不熟悉,建议阅读 [Base64 维基百科](https://zh.wikipedia.org/wiki/Base64)。
3 |
4 | ## source map
5 | ### source map 的作用
6 | 在生产环境中,代码一般是以编译、压缩后的形态存在,对生产友好,但是调试时候定位的错误只能定位到编译压缩后的代码位置,此时的代码对人类的阅读很不友好,可能变量名被缩短失去语义,甚至是经过编译的,生产代码与开发代码已经没法一一对应。为了解决代码之间对应关系,人们设计了 source map 这种数据格式来。source map 就像一个索引表,将生产代码和开发代码联系起来,这样开发调试时就可以清晰的定位到开发代码中了。
7 | #### 前端的编译处理过程包括但不限于
8 | - 转译器/Transpilers(Babel)
9 | - 编译器/Compilers(TypeScript,CoffeeScript,Webassembly)
10 | - 压缩/Minifiers(UglifyJS)
11 |
12 | 这些都是可生成 source map 的操作。有了 source map,使得我们调试线上产品时,能直接看到开发环境的代码(需要浏览器提供支持)。
13 | ### source map 的格式
14 | 打开 source map,它大概是这个样子
15 | ``` JavaScript
16 | {
17 | version: 3,
18 | file: "outName.js",
19 | sourceRoot: "",
20 | sources: ["inputName1.js", "inputName2.js"],
21 | names: ["src", "maps", "are", "fun"],
22 | mappings: "XXXXX, XXXX, XXXXXX"
23 | }
24 | ```
25 |
26 | 属性对应的内容是
27 |
28 | ``` JavaScript
29 | - version: source map 的版本号。
30 | - file: 转换后的文件名。
31 | - sourcesRoot:转换前文件所在目录。如果输入和输出文件在同一个目录下,该项为空。
32 | - sources:转换前的文件。该项是一个数组,可能存在多个文件合并成一个文件。
33 | - names:转换前的所有变量名和属性名。
34 | - mappings:记录位置信息的字符串。
35 | ```
36 |
37 | ### mappings 的编码设计
38 | 每个位置使用五位,表示五个字段,从左边算起
39 | - 第一位,表示这个位置在转换后的代码的第几列。
40 | - 第二位,表示这个位置属于 sources 属性中的哪一个文件。
41 | - 第三位,表示这个位置属于转换前代码的第几行。
42 | - 第四位,表示这个位置属于转换前代码的第几列。
43 | - 第五位,表示这个位置属于 names 属性中的哪一个变量。
44 |
45 | #### 编码设计
46 | 最直观的想法是,将生成的文件中每个字符位置对应的原位置保存起来。
47 |
48 | ```
49 | “feel the force” ⇒ Yoda ⇒ “the force feel”
50 | ```
51 |
52 | 一个简单的文本转换输出,其中 Yoda 可以理解为一个转换器。将上面的输入与输出列成表格可以得出转换后输入与输出的对应关系。
53 |
54 | |输出位置|输入|在输入中的位置|字符|
55 | |-|-|-|-|
56 | |行 1, 列 0| Yoda_input.txt| 行 1, 列 5| t|
57 | |行 1, 列 1 |Yoda_input.txt |行 1, 列 6| h|
58 | |行 1, 列 2 |Yoda_input.txt |行 1, 列 7| e|
59 | |行 1, 列 4 |Yoda_input.txt |行 1, 列 9| f|
60 | |行 1, 列 5 |Yoda_input.txt |行 1, 列 10|o|
61 | |行 1, 列 6 |Yoda_input.txt |行 1, 列 11 |r|
62 | |行 1, 列 7 |Yoda_input.txt |行 1, 列 12 |c|
63 | |行 1, 列 8 |Yoda_input.txt |行 1, 列 13 |e|
64 | |行 1, 列 10 |Yoda_input.txt |行 1, 列 0| f|
65 | |行 1, 列 11 |Yoda_input.txt |行 1, 列 1| e|
66 | |行 1, 列 12 |Yoda_input.txt |行 1, 列 2| e|
67 | |行 1, 列 13 |Yoda_input.txt |行 1, 列 3| l|
68 |
69 | 将上面的表格整理记录成一个映射编码,看起来会是这样的:
70 | ```
71 | mappings(283 字符):1|0|Yoda_input.txt|1|5, 1|1|Yoda_input.txt|1|6, 1|2|Yoda_input.txt|1|7, 1|4|Yoda_input.txt|1|9, 1|5|Yoda_input.txt|1|10, 1|6|Yoda_input.txt|1|11, 1|7|Yoda_input.txt|1|12, 1|8|Yoda_input.txt|1|13, 1|10|Yoda_input.txt|1|0, 1|11|Yoda_input.txt|1|1, 1|12|Yoda_input.txt|1|2, 1|13|Yoda_input.txt|1|3
72 | ```
73 |
74 | 这样确实能实现映射效果,但这里源文件 `feel the force` 才 12 个有效字符,加上空格的长度也才 14,为了记录编译前后的映射文件就已经达到了 283 个字符。随着源文件的字符数增加,这个映射文件将会变得巨大。
75 |
76 | ##### 优化方案
77 | 1. 省去输出文件中的行号,改用 `;` 来标识换行
78 |
79 | ```
80 | mappings (245 字符): 0|Yoda_input.txt|1|5, 1|Yoda_input.txt|1|6, 2|Yoda_input.txt|1|7, 4|Yoda_input.txt|1|9, 5|Yoda_input.txt|1|10, 6|Yoda_input.txt|1|11, 7|Yoda_input.txt|1|12, 8|Yoda_input.txt|1|13, 10|Yoda_input.txt|1|0, 11|Yoda_input.txt|1|1, 12|Yoda_input.txt|1|2, 13|Yoda_input.txt|1|3;
81 | ```
82 | 2. 可符号化字符的提取
83 |
84 | |序号|符号|
85 | |-|-|
86 | |0|the|
87 | |1|force|
88 | |2|feel|
89 |
90 | 搭配一个包含所有符号的数组:
91 | ```
92 | name: ['the', 'force', 'feel']
93 | ```
94 |
95 | 在记录时,只需要记录一个索引,还原时通过读取 `names` 数组即可还原。
96 | 所以 `the` 的映射有原来的
97 | ```
98 | 0|Yoda_input.txt|1|5, 1|Yoda_input.txt|1|6, 2|Yoda_input.txt|1|7
99 | ```
100 | 简化为:
101 | ```
102 | 0|Yoda_input.txt|1|5|0
103 | ```
104 | 考虑到合并多文件打包的情况,输入文件也许不止一个,加入一个输入文件的记录索引
105 | ```
106 | sources: ['Yoda_input.txt']
107 | ```
108 | 进一步将 `the` 的标示简化为:
109 | ```
110 | 0|0|1|5|0
111 | ```
112 |
113 | 到这一步,我们的 source map 文件大致的内容为:
114 | ```
115 | {
116 | version: 3,
117 | file: "Yoda_ouput.txt",
118 | sourceRoot: "",
119 | sources: ["Yoda_input.txt"],
120 | names: ["the", "force", "feel"],
121 | mappings(31 字符): 0|0|1|5|0, 4|0|1|9|1, 10|0|1|0|2;
122 | }
123 | ```
124 |
125 | 3. 记录相对位置
126 | 当文件内容巨大时,上面精简后的代码也有可能某些数字会随着增加而变得很长,如果一行的位置记录了某个位置,那么根据这一位置进行相对定位是可以到达一行内的任意位置。
127 |
128 | 具体到本例中,看看最初的表格中,记录的输出文件的位置:
129 |
130 | |输出位置|输出位置|
131 | |-|-|
132 | |行 1, 列 0| 行 1, 列 0|
133 | |行 1, 列 4| 行 1, 列 (上一值 + 4 = 4)|
134 | |行 1, 列 10| 行 1, 列 (上一值 + 6 = 10)|
135 |
136 | 对应到整个表格则是:
137 |
138 | |输出位置| 输入文件的索引| 输入的位置| 符号索引|
139 | |-|-|-|-|
140 | |行 1, 列 0| 0 |行 1, 列 5| 0|
141 | |行 1, 列 +4| +0 |行 1, 列 +4| +1|
142 | |行 1, 列 +6| +0 |行 1, 列 -9| +1|
143 |
144 | 到这一步,我们的 source map 文件大致的内容为:
145 | ```
146 | {
147 | version: 3,
148 | file: "Yoda_ouput.txt",
149 | sourceRoot: "",
150 | sources: ["Yoda_input.txt"],
151 | names: ["the", "force", "feel"],
152 | mappings(31 字符): 0|0|1|5|0, 4|0|1|4|1, 6|0|1|-9|1;
153 | }
154 | ```
155 |
156 | 观察编译后文件的内容,可以看出,通常是被压缩至几行代码内,这样通过 `;` 就可以标识行号。上面介绍 source map 格式的时候,就已经提及了第二种优化,source map 文件中使用 names 记录源文件中的变量名和属性名,只需要读取这个数组的下标就可以映射出源文件和目标文件的变量。源文件中的代码都是连续的,只要记录了起始位置,通过相对位置的记号即可到达文件中的任意位置。通过前三种优化已经能有效减少 mappings 的字符数量,如果文件体积不是特别巨大,到这一步已经能够满足应用了。对于内存控制精确到字节的工程师,只有 mappings 中的分隔符,以及如何记录大数字这两个优化方向了。
157 |
158 | #### VLQ 编码
159 | 1. VLQ 以数字的方式呈现
160 |
161 | 如果我们提前知晓要记录的数字每一位的个数,就可以省略间隔符。比如:
162 | ```
163 | 假如要记录的数字是
164 | 1|2|3|4
165 | 那么直接记录成如下也能被正确识别
166 | 1234
167 | ```
168 | 如果我们使用下划线来标识一个数字后是否跟有其他数字:
169 | 1234567
170 | 解读规则为:
171 | - 1没有下划线,那解析出来的第一个数字是1
172 | - 2有下划线,继续解析,遇到3,3没有下划线,第二个数字解析结束,是23
173 | - 4有下划线,继续解析,5有下划线,继续解析,6没有下划线,第三个数字解析结束,是456
174 | - 7没有下划线,第四个数字是7
175 |
176 |
177 | 2. VLQ 以二进制的方式呈现
178 |
179 | 在二进制系统中,我们使用 6 个字节来记录一个数字,用其中一个字节来标识它是否结束(下方 C),再用一位标识正负(下方 S),剩下还有四位用来表示数值。用这样6个字节来表示我们需要的数字。
180 |
181 |
182 |
183 | B5 |
184 | B4 |
185 | B3 |
186 | B2 |
187 | B1 |
188 | B0 |
189 |
190 |
191 | C |
192 | Value |
193 | S |
194 |
195 |
196 |
197 | 任意数字中,第一组的第一个字节就已经明确标明该数字的正负,所以后续字节不需要再标识,也就是说第一组有 4 个字节来表示数值,后续每一组都有 5 个字节来表示数值(每组的第一个字节标识是否结束)
198 |
199 | 现在我们用二进制规则来编码之前的这个数字序列 `1|23|456|7`。
200 |
201 | 将数字转换对应的真值二进制
202 |
203 |
204 | 数值 |
205 | 二进制 |
206 |
207 |
208 | 1 |
209 | 1 |
210 |
211 |
212 | 23 |
213 | 10111 |
214 |
215 |
216 | 456 |
217 | 1111001000 |
218 |
219 |
220 | 7 |
221 | 111 |
222 |
223 |
224 |
225 | - 对 1 进行编码
226 | 1 需要一位来表示,首个字节组有四位来表示值
227 |
228 |
229 | B5(C) |
230 | B4 |
231 | B3 |
232 | B2 |
233 | B1 |
234 | B0(S) |
235 |
236 |
237 | 0 |
238 | 0 |
239 | 0 |
240 | 0 |
241 | 1 |
242 | 0 |
243 |
244 |
245 |
246 | - 对 23 进行编码
247 | 23 的二进制位 10111 一共需要5位,只能拆分成两组,第一组截取后四位,剩下一位放入第二组中。
248 |
249 |
250 | B5(C) |
251 | B4 |
252 | B3 |
253 | B2 |
254 | B1 |
255 | B0(S) |
256 | |
257 | B5(C) |
258 | B4 |
259 | B3 |
260 | B2 |
261 | B1 |
262 | B0(S) |
263 |
264 |
265 | 1 |
266 | 0 |
267 | 1 |
268 | 1 |
269 | 1 |
270 | 0 |
271 | |
272 | 0 |
273 | 0 |
274 | 0 |
275 | 0 |
276 | 0 |
277 | 1 |
278 |
279 |
280 |
281 | - 对 456 进行编码
282 | 456 的二进制 111001000 需要 9 个字节,同样第一组截取后四位,剩下的每五位截取放入到跟随的字节组中即可。
283 |
284 |
285 | B5(C) |
286 | B4 |
287 | B3 |
288 | B2 |
289 | B1 |
290 | B0(S) |
291 | |
292 | B5(C) |
293 | B4 |
294 | B3 |
295 | B2 |
296 | B1 |
297 | B0(S) |
298 |
299 |
300 | 1 |
301 | 1 |
302 | 0 |
303 | 0 |
304 | 0 |
305 | 0 |
306 | |
307 | 0 |
308 | 1 |
309 | 1 |
310 | 1 |
311 | 0 |
312 | 0 |
313 |
314 |
315 |
316 | - 对 7 进行编码
317 | 7 的二进制为 111,只需要一个字节组
318 |
319 |
320 | B5(C) |
321 | B4 |
322 | B3 |
323 | B2 |
324 | B1 |
325 | B0(S) |
326 |
327 |
328 | 0 |
329 | 0 |
330 | 1 |
331 | 1 |
332 | 1 |
333 | 0 |
334 |
335 |
336 |
337 | 将上面编码合并得到最终的编码:
338 | ```
339 | 000010 101110 000001 110000 011100 001110
340 | ```
341 |
342 | 之所以用 6 位来作为一组字节组的长度,是因为 6 位的长度可以进行 base64 编码,将上面得到的最终编码对照 base64 编码表,转换的结果就是
343 | ```
344 | CuBwcO
345 | ```
346 |
347 | #### 利用 Base64 VLQ 编码生成最终的 source map
348 | 还记得我们上面讨论中的示例,优化过的结果是
349 | ```
350 | {
351 | version: 3,
352 | file: "Yoda_ouput.txt",
353 | sourceRoot: "",
354 | sources: ["Yoda_input.txt"],
355 | names: ["the", "force", "feel"],
356 | mappings(31 字符): 0|0|1|5|0, 4|0|1|4|1, 6|0|1|-9|1;
357 | }
358 | ```
359 | 现在进行 Base VlQ 编码,先转成二进制,再转 VLQ 表示。
360 | ```
361 | 0 -> 0 -> 000000
362 | 0 -> 0 -> 000000
363 | 1 -> 1 -> 000010
364 | 5 -> 101 -> 001010
365 | 0 -> 0 -> 000000
366 | ```
367 | 合并后的编码为:
368 | ```
369 | 000000 000000 000010 001010 000000
370 | ```
371 | 转 Base64 后得到:
372 | ```
373 | AACKA
374 | ```
375 | 经过 Base64 VLQ 编码后的 source map 结果是:
376 | ```
377 | {
378 | version: 3,
379 | file: "Yoda_ouput.txt",
380 | sourceRoot: "",
381 | sources: ["Yoda_input.txt"],
382 | names: ["the", "force", "feel"],
383 | mappings(31 字符): AACKA, IACIC, MACTC
384 | }
385 | ```
386 | 上面的结果就是 source map 的核心原理,通过数据设计和编码转换,达到用最简便的方式记录信息,而且考虑到生成的文件体积大小问题,可以说是设计思想是相当巧妙。
387 |
388 | ## 代码实现
389 | 以下是 Base64 VQL 源码实现,是GitHub 上[MattiasBuelens](https://github.com/MattiasBuelens)实现的,用的 TypeScript,这里我将他实现转换成 JavaScript,并且标注上我自己的理解。其中位运算可以参考这篇[文章](https://github.com/D-kylin/note/blob/master/0%E5%92%8C1%E7%BC%96%E7%A0%81%E7%9A%84%E8%83%8C%E5%90%8E.md),如果 Base64 编码不熟悉的建议先查看[Base64维基百科](https://zh.wikipedia.org/wiki/Base64)。
390 | ```JavaScript
391 | let charToInteger = {};
392 | let integerToChar = {};
393 |
394 | // Base64 和数字互转
395 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split( '' ).forEach( function ( char, i ) {
396 | charToInteger[ char ] = i;
397 | integerToChar[ i ] = char;
398 | });
399 |
400 | function decode(string) { //Base64 转数字
401 | let result = [];
402 | let shift = 0;
403 | let value = 0;
404 |
405 | for (let i = 0; i < string.length; i++) {
406 | let integer = charToInteger[string[i]];
407 |
408 | if (integer === undefined) {
409 | throw new Error(`Invalid character ${string[i]}`)
410 | }
411 |
412 | const hasContinuationBit = integer & 32; //小于 32 的数跟 32 通过与位运算都是 0
413 |
414 | integer &= 31; // 31 的二进制编码是 11111,相当于取这个数值二进制编码的右五位。
415 | value += integer << shift; //无符号左移运算,每一位向左移动 n 位,然后在移动前位置补 n 个 0,这个 shift 的值是根据上一个字符决定是否左移,即如果上一个符号的 hasContinuationBit 大于 0,表示这个数值还没有读取完毕,需要继续读取下一个符号的值加起来才是完整值。<< 的优先级比 += 要高,先 << 后 +=。
416 |
417 | if (hasContinuationBit) {
418 | shift += 5; //左移的位数加 5,因为如果需要分割的时候,后面的字节组都是有 1 个字节表示是否结束,5 个字节表示值,所以左移值是五位。
419 | } else {
420 | const shouldNegate = value & 1; //如果读取完毕,判断这个值的二进制的末位是否 1,如果是 1 表明数值是负数,如果是 0 表明数值是正数
421 | value >>= 1; //因为最后一位是正负数的记号标识,所以这个值需要右移一位。
422 |
423 | result.push(shouldNegate ? -value : value);
424 |
425 | //reset
426 | value = shift = 0;
427 | }
428 | }
429 |
430 | return result;
431 | }
432 |
433 | function encode(value) { //数字转 Base64
434 | let result;
435 |
436 | if (typeof value === 'number') {
437 | result = encodeInteger(value);
438 | } else {
439 | result = '';
440 | for(let i = 0; i < value.length; i++) {
441 | result += encodeInteger(value[i])
442 | }
443 | }
444 |
445 | return result;
446 | }
447 |
448 | function encodeInteger(num) { //数字转 Base64 核心代码
449 | let result = '';
450 |
451 | if (num < 0) {
452 | num = (-num << 1) | 1; //如果要转的数字小于 0,则左移一位,并且将末位变为 1,来标识负数
453 | } else {
454 | num <<= 1; //如果要转的数字大于 0,则左移一位,末位自动补 0
455 | }
456 |
457 | do {
458 | let clamped = num & 31; //取二进制编码的后五位
459 | num >>= 5; //然后将数值左移五位
460 |
461 | if (num > 0) { //判断左移后的值是否大于 0,如果还有大于 0 的值,表面该数值还没转完,如果等于 0,表面该数值已完全转为 Base64 字母
462 | clamped |= 32; //如果进入这个条件语句,表示该数值后还会有值,这组字节组的首个字节一定是 1,所以需要读取的值是六位。通过 |= 跟 32 位运算可以将值变为 6 位二进制值。
463 | }
464 |
465 | result += integerToChar[clamped];
466 | } while (num > 0); //左移五位后判断 num 是否大于 0,如果是表明值还没读取完毕,如果不是,结束循环。
467 |
468 | return result;
469 | }
470 | ```
471 |
472 | ## 结语
473 | 以上代码便是该作者的实现思路,主要是通过位运算。里面用的数值 5,31,32 这些值看数值反而不是很直观,如果将他们转换成“真值”二进制码,更容易看出实现思路。这个代码实现结合 VQL 编码的设计思想我认为能更好的理解这种编码格式。现在各种算法都有框架或者底层来实现封装,大多数时候都是直接拿来就用,理解其实现原理大多数情况下也是帮助不大。不过我认为在学习一个技术的时候,了解其当初的设计思想和实现原理对编程思想的提高很有帮助。这篇文章如果有错误的地方欢迎指教,感谢您的阅读。
474 |
475 | #### 参考资料,感谢以下文章的作者。
476 | > [source map 的原理探究](https://github.com/wayou/wayou.github.io/issues/9)
477 |
478 | > [Source Maps under the hood – VLQ, Base64 and Yoda](https://blogs.msdn.microsoft.com/davidni/2016/03/14/source-maps-under-the-hood-vlq-base64-and-yoda/#comment-626)
479 |
480 | > [JavaScript Source Map 详解](http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html)
--------------------------------------------------------------------------------
/布局.md:
--------------------------------------------------------------------------------
1 | # Layout/布局
2 |
3 | ## 盒模型
4 | 当对一个文档进行布局(laying out)的时候,浏览器渲染引擎根据 CSS-Box 模型,将所有元素表示为一个矩形盒子。使用标准盒模型描述这些矩形盒子,盒子有四个边:外边距,边框,内填充,内容。
5 |
6 | content:元素的真实内容区域,通常由width,height,max-width,max-height,min-width,min-height控制内容大小。
7 |
8 | padding:延伸到包围padding的边框。如果 content 设置了背景,颜色或者图片,会延伸到 padding 上。此属性不能设置为负值,设置为负数时,在浏览器渲染时会被当成数值 0。
9 |
10 | border: 包含边框的区域,扩展了内边距区域。
11 |
12 | margin:用空白区域扩展边框区域,以分开相邻的元素。此属性可设置为负数。
13 |
14 | 对一个元素设置宽高时,如果没有声明 box-sizing 属性,则该盒子尺寸模型默认为 content-box,此时该元素在文档中的占据尺寸为:content(width/height) + border + padding + margin。声明为 border-box 时,设置的宽理解为 width = content + padding + border。
15 |
16 | ## position
17 | static:指定元素使用正常的布局行为,即元素在文档常规流中的布局位置。此时 top,right,bottom,left,z-index 属性无效。
18 |
19 | relative:元素先防止在未添加定位时的位置,再在不改变页面布局的前提下调整元素位置。
20 |
21 | absolute:指定元素脱离文档流,通过指定元素相对于最近的非 static 定位祖先元素的偏移。默认定位位置是指定元素的左上角对齐最近非 static 祖先元素的 content 左上角。还有一个要注意的是,absolute 定位下,指定 top 的元素的上边距离最近非 static 祖先元素的 content 上边距离。bottom 是元素的下边距离最近非 static 祖先元素的 content 下边距离。左右同理。此属性可以用来将 absolute 定位的元素,通过指定 top、bottom 的值,来达到拉伸元素 height,实现高度自适应。或者指定高度的情况下,实现垂直居中效果。
22 |
23 | fixed:指定元素脱离文档流,将指定元素相对于屏幕视口(viewport)的位置来指定元素位置。如果元素祖先的 transform 属性非 none 时,容器由视口改为该元素祖先。
24 |
25 | sticky:盒位置根据正常流计算(这称为正常流动中的位置),然后相对于该元素在流中的 flow root(BFC)和 containing block(最近的块级祖先元素)定位。在所有情况下(即便被定位元素为 table 时),该元素定位均不对后续元素造成影响。当元素 B 被粘性定位时,后续元素的位置仍按照 B 未定位时的位置来确定。position: sticky 对 table 元素的效果与 position: relative 相同。可以看作,在设定阈值外,表现为 position: relative。在设定阈值内,表现为 position: fixed。sticky 要生效,必须满足以下条件
26 | - 指定 top,right,bottom,left 四个阈值其中之一。否则其行为与相对定位相同,并且 top 和 bottom 同时设置时,top 生效优先级高,left 和 right 同时设置时,left 生效优先级高。
27 | - 设定为 sticky 元素的任意父节点的overflow 属性必须是 visible。
28 | - 达到设定阈值。
29 |
30 | ## float
31 | 当一个元素浮动后,会被移出正常的文档流,然后向左或者向右平移,一直平移知道碰到所处的容器边框,或者碰到另一个浮动的元素。能设置的值有:left,right,none。
32 |
33 | 消除浮动元素影响的方法有两种,一种是在同一块级格式化上下文中没有其他元素时,设置 clear 属性为 both 可以清除元素受到 float 元素的影响。另一种是将浮动元素的容器元素设置 overflow 属性值为 hidden 或者 auto,这样可以让容器元素伸展到能包含浮动元素,而不是让他们超出块元素的底部。
34 |
35 | ## display
36 | none:此元素不会被显示。
37 |
38 | block:此元素显示为块级元素,默认横向充满其父元素的内容区域,独占一行。
39 |
40 | inline:此元素显示为行内元素,一般情况下无法设置高度,行高,顶和底边距不可改变。内联元素只能容纳文本或者其他内联元素。
41 | - vertical-align: baseline,top,middle,bottom,text-top,text-bottom
42 | - line-height: normal,百分比、em和数值。
43 |
44 | inline-block:行内块元素。
45 |
46 | list-item:此元素会作为列表显示。
47 |
48 | table:此元素会作为块级表格来显示,表格前后带有换行符。
49 |
50 | inherit:规定从父元素继承 display 属性。
51 |
52 | flex:弹性布局,属性值有 flex 定义元素为块级的 flex 容器,inline-flex 定义元素为内联的 flex 容器。
53 |
54 | grid:网格布局。
55 |
56 | ## flex布局
57 | - display: flex | inline-flex;
58 | - 容器
59 | - flex-flow: <'flex-direction'> || <'flex-wrap'>
60 | - flex-direction: row | row-reverse | column | column-reverse;
61 | - flex-wrap: nowrap | wrap | wrap-reverse;
62 | - justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly;
63 | - align-content: stretch | flex-start | flex-end | center | space-between | space-around | space-evenly;
64 | - align-items: stretch | flex-start | flex-end | center | baseline;
65 |
66 | - 子项
67 | - order: ; /* 整数值,默认值是 0 */
68 | - flex: none | auto | [ <'flex-grow'> <'flex-shrink'>?] || <'flex-basis'> ]; /* 默认值为 0 1 auto */
69 | - 默认值,none,auto的区别
70 | - flex 默认值等同于 flex: 0 1 auto;
71 | - flex: none; 等同于 flex: 0 0 auto;
72 | - flex: auto; 等同于 flex: 1 1 auto;
73 | - flex-grow: ; /* 数值,可以是小数,默认值是 0,不支持负数 */
74 | - flex-shrink: ; /* 数值,默认值是 1,不支持负数 */
75 | - flex-basis: | auto; /* 默认值是 auto */
76 | - align-self: auto | flex-start | flex-end | center | baseline | stretch;
77 |
78 | - Flex布局注意事项
79 | - 在Flex布局中,flex子元素的设置float,clear以及vertical-align属性都是没有用的。
80 | - 关于 flex-grow 和 flex-shrink 的计算方式。
81 | - flex-grow
82 |
83 | 如果所有子项的 flex-grow 数值总和 < 1,则将剩余的空白乘以总和数值再按每个子项设置值占总和比例分配给每个子项。
84 | 如果所有子项的 flex-grow 数值总和 >1,则将剩余的空白按每个子项设置值占总和比例分配给每个子项。
85 | - flex-shrink
86 |
87 | 如果只有一个子项设置了不为 0 的值,当值 <1,收缩的比例是不足的空间乘以设置值,再将需要缩小的值体现到子项上,收缩尺寸不完全,会有一部分内容溢出 flex 容器,如果 >1,则完全收缩,正好填满 flex 容器。如果有多个子项设置了 flex-shrink 的值,则需要算设置的总和,总和 >1 则是完全收缩,直到刚好占满容器;总和 <1 则是不完全收缩,仍然有部分溢出。
88 |
89 | ## grid布局
90 | 暂无,后续补充。
--------------------------------------------------------------------------------