0%

管理

用户

mysql 库中的 user 表包含了当前所有用户, db 表中包含了用户对于数据库的权限.

1
2
3
select * from mysql.user;

select * from mysql.db;

我们可以使用 insert 向其中插入数据来添加新用户, 然后对 db 添加权限. 最后别忘了调用 flush privileges; 使改动生效.

show variables

mysql 的系统变量分为两种, 全局变量会话变量.
全局变量进程生命周期的, mysql 重启后就丢失了. 同时, 全局变量设置后, 只有从此以后开始的新连接才生效. 因此, 已经连接的会话不会有效果, 即便是当前执行语句所在会话都一样没有效果. 全局变量使用 set @@global.xxx=...; 设置, 通过 select @@global.xxx; 读取.

会话变量生命周期为当前连接, 断开重连就失效了. 会话使用 set @@session.xxx=...; 设置, 通过 select @@session.xxx; 读取. 其中 session. 是可选的. session 也可以使用 local 替换, 两者等价.

show variables 命令用于查看 mysql 服务的各项配置信息. 其本质是读取了全局或会话变量, 并以 table 的形式展现出来. 例如 show variables where Variable_name='time_zone'; 等价于 select @@time_zone.

在 variables 中, 大部分变量都是可读可写的, 不过也有部分是只读的.

数据库存储路径
1
2
3
4
5
# mysql 运行目录
select @@basedir;

# 数据存储目录
select @@datadir;
时区
1
2
3
4
5
# 代表 mysql 启动时的操作系统时区. 只读
select @@system_time_zone;

# 显示当前时区
select @@time_zone;

当我们查询 UTC 时区的表时, 可以临时修改时区(set @@time_zone='...';), 这样显示的时间就是当地时区时间了.

查看用户的授权信息
1
2
3
show grants;

show grants for 'xxx';

时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 当前时区时间
select now(); // '2019-12-26 12:53:21'

# datetime => timestamp
select unix_timestamp('2019-12-26 12:59:35'); // 1577365175

# timestamp => datetime
select from_unixtime(1577365175); // '2019-12-26 12:59:35'

# UTC => +8:00 时区转换
select convert_tz('2019-12-26 12:59:35', 'UTC', '+8:00'); // '2019-12-26 20:59:35'

# 时间 => 秒
select time_to_sec('08:00:00'); // 28800

# 时间的差值. 同样还有 datediff, 但是只比较日期的差值.
select timediff('2019-12-26 20:59:35', '2019-12-25 12:59:35'); // '32:00:00'
select timediff('20:59:35', '12:59:35'); // '08:00:00'

# 格式化. 完整 specifier 列表: https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-format
select date_format(now(), '%Y-%m-%d %H:%i:%s'); // '2019-12-26 13:23:45'

时区

1
2
3
4
5
6
7
# 命令行 --default-time-zone="+8:00"

# 配置文件 /etc/my.cnf
# default-time-zone="+8:00"

# 连接时区
set @@time_zone='+8:00';

Shell

命令 结果 说明
"" 与 ''
-
"" 中可以解析变量, 例如 "$USER" => walfud. 而 '' 则代表原始字符串, 例如 '$USER' => $USER
---
cat /etc/hostname > out
-/pre>
标准输出流重定向到 out 文件
cat not-exists > err
-
标准错误流重定向到 err 文件
cat not-exists >& all
-
标准输出和错误流都重定向到 all 文件
cat not-exists 2>&1
-
标准输出流重定向到标准输出. 这样错误信息也能够被管道使用
---
echo $USER
walfud
echo 可以解析 $ 变量
echo "\n"
\n
echo 不能解析控制字符. 如果想打印 \n 这种控制字符, 考虑使用 `printf '%s\n%d' foo 123`
---
dirname /home/walfud/.profile
/home/walfud
解析路径
basename /home/walfud/.profile
.profile
解析文件名

系统信息

命令 结果 说明
lsb_release -a
No LSB modules are available.
Distributor ID:    Ubuntu
Description:    Ubuntu 19.10
Release:    19.10
Codename:    eoan
    
所有发行版专有信息
lsb_release -is
Ubuntu
发行版名称. `-s` 只返回 value
lsb_release -sr
19.10
版本
lsb_release -sc
eoan
代号
---
ps axu
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0 168424  7864 ?        Ss   11月14   1:39 /sbin/init splash
root         2  0.0  0.0      0     0 ?        S    11月14   0:00 [kthreadd]
gdm       1521  0.0  0.0 274428  9072 tty1     Sl+  11月14   0:00 /usr/lib/gnome-session/gnome-session-binary --systemd --autostart /usr/share/gdm/greeter/autostart
walfud    1783  0.0  0.0  18964  6140 ?        Ss   11月14   0:01 /lib/systemd/systemd --user
walfud    1784  0.0  0.0 168496   212 ?        S    11月14   0:00 (sd-pam)
walfud    1796  0.0  0.0 3851460 13728 ?       S
列出所有进程的常用信息. 默认情况下 ps 输出当前用户有 tty 的进程信息, `a` 表示 "包含所有用户", `x` 表示 "包含非 tty 进程", `u` 表示 "包含用户常用的信息, 例如 cpu/内存 等". 注意参数前不要加 `-`
kill -9 123456
-
杀掉 pid 为 123456 的进程. -9 表示发送 SIGKILL(9), 由内核杀进程而不会经过进程同意. 默认的 SIGTERM(15) 则是发送给进程告知其应该立刻结束, 但是进程可以不响应或者来不及响应从而杀不死进程
killall -9 java
-
杀掉所有名称为 java 的进程
---
lsof -c java
COMMAND   PID   USER   FD      TYPE             DEVICE SIZE/OFF     NODE NAME
java    12159 walfud  cwd       DIR              259,2     4096  2229681 /home/walfud/Projects/akulaku/midend-test/sms-testutils/backend
java    12159 walfud  rtd       DIR              259,4     4096        2 /
java    12159 walfud  txt       REG              259,3     8464  3933441 /opt/jdk/bin/java
java    12159 walfud  mem       REG              259,3   283368  3933758 /opt/jdk/jre/lib/amd64/libsunec.so
java    12159 walfud    3r      REG              259,3 66463215  3933500 /opt/jdk/jre/lib/rt.jar
java    12159 walfud   90u     IPv6             186997      0t0      TCP *:7211 (LISTEN)
java    12159 walfud   96u  a_inode               0,14        0    11326 [eventpoll]
...
    
查看进程打开的所有文件. 这样会遍历所有同名进程. 可以使用 -p 指定 pid
lsof /home/walfud
-
反向查看哪些进程打开了 /home/walfud 目录. 在不知道全路径的情况下, 可以通过 `lsof | grep XXX` 来查找(比较慢)

CPU

命令 结果 说明
lscpu
Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
Address sizes:                   39 bits physical, 48 bits virtual
CPU(s):                          8
On-line CPU(s) list:             0-7
Thread(s) per core:              2
Core(s) per socket:              4
Socket(s):                       1
NUMA node(s):                    1
Vendor ID:                       GenuineIntel
CPU family:                      6
Model:                           142
Model name:                      Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
Stepping:                        12
CPU MHz:                         800.008
CPU max MHz:                     4900.0000
CPU min MHz:                     400.0000
BogoMIPS:                        4599.93
Virtualization:                  VT-x
L1d cache:                       128 KiB
L1i cache:                       128 KiB
L2 cache:                        1 MiB
L3 cache:                        8 MiB
NUMA node0 CPU(s):               0-7
Vulnerability L1tf:              Not affected
Vulnerability Mds:               Not affected
Vulnerability Meltdown:          Not affected
Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl and seccomp
Vulnerability Spectre v1:        Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:        Mitigation; Enhanced IBRS, IBPB conditional, RSB filling
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp md_clear flush_l1d arch_capabilities
    
根据 /proc/cpuinfo 获取信息

网络

命令 结果 说明
ss -a
State                   Recv-Q              Send-Q                            Local Address:Port                                  Peer Address:Port                                                         
LISTEN                  0                   100                                   127.0.0.1:18083                                      0.0.0.0:*                                                            
LISTEN                  0                   10                                    10.1.2.94:35301                                      0.0.0.0:*                                                            
ESTAB                   0                   0                                     127.0.0.1:5037                                     127.0.0.1:48126                                                        
CLOSE-WAIT              32                  0                                     10.1.2.94:51300                              180.163.255.156:https               timer:(keepalive,13sec,2)                
......
    
列出所有网络连接. 默认情况下只显示已连接的套接字, -a 可以包括 LISTENING 状态的所有套接字
ss -au
-
列出所有 udp 连接. -u 指定 udp 连接, 还可以 -t 指定 tcp 连接
ss -au4
-
列出所有基于 ipv4 的 udp 连接. -4 指定基于 ipv4 的连接, 还可以 -6 指定 ipv6
ss -aup
State               Recv-Q              Send-Q                                 Local Address:Port                              Peer Address:Port                                                            
UNCONN              0                   0                                        224.0.0.251:mdns                                   0.0.0.0:*                 users:(("chrome",pid=3624,fd=46))             
UNCONN              0                   0                                            0.0.0.0:mdns                                   0.0.0.0:*                                                               
UNCONN              0                   0                                          127.0.0.1:46643                                  0.0.0.0:*                                                               
UNCONN              0                   0                                          10.1.2.94:48101                                  0.0.0.0:*                                                          
.....
    
列出所有 udp 连接. -p 显示进程名称
ss -aun
State                     Recv-Q                     Send-Q                                               Local Address:Port                                           Peer Address:Port                    
UNCONN                    0                          0                                                      224.0.0.251:5353                                                0.0.0.0:*                       
UNCONN                    0                          0                                                          0.0.0.0:5353                                                0.0.0.0:*                       
UNCONN                    0                          0                                                        127.0.0.1:46643                                               0.0.0.0:*                       
UNCONN                    0                          0                                                        10.1.2.94:48101                                               0.0.0.0:*      
.....
    
列出所有 udp 连接. -n 制定以数字而非名称方式显示端口号
---
curl http://walfud.com?foo=123&bar=abc
-
发送 GET 请求
curl http://walfud.com -d '{ "foo": 123, "bar": "abc" }'
-
发送 POST 请求. -d 会自动识别为 POST 请求
curl http://walfud.com -H 'Content-Type: application/json' -H 'token: fooooo'
-
-H 设置 header
curl http://walfud.com -v
*   Trying 123.56.81.162:80...
* TCP_NODELAY set
* Connected to walfud.com (123.56.81.162) port 80 (#0)
> GET / HTTP/1.1
> Host: walfud.com
> User-Agent: curl/7.65.3
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.17.4
< Date: Wed, 27 Nov 2019 04:55:56 GMT
< Content-Type: text/html
< Content-Length: 612
< Last-Modified: Tue, 24 Sep 2019 14:49:10 GMT
< Connection: keep-alive
< ETag: "5d8a2ce6-264"
< Accept-Ranges: bytes
< 
<!DOCTYPE html>
<html>
.....
</html>
* Connection #0 to host walfud.com left intact
    
-v 显示请求和响应的详细信息
---
原理请看这里
iptables -L
-
按 chain 查询规则, 也可以指定 chain 查询 `iptables -L <PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING>`.
-t 可以限制 table, 例如 `iptables -L -t filter` 则查询所有 filter 表中的规则.
-n 表示不解析域名.

其他

命令 结果 说明
grep "some thing" <文件>
-
显示 <文件> 中匹配 "some thing" 的行, 可以使用正则, 但需要转义字符. -i 可以不区分大小写, -C 可以打印匹配行的前后 n/2 行, -r 可以递归遍历目录下的所有文件, -v 表示显示部匹配的行
egrep "^fo.+$" <文件>
-
等价于 `grep -E`, 使用正则表达式无需转义字符
---
echo '1 2 3 4\na b c d' | awk '{print $1}'
1
a
    
打印每行的第 1 列. `$0` 代表整个行, `$1` 代表第一列
echo '1 2 3 4\na b c d' | awk 'BEGIN{ print "<<<<<" 1 } { print end{ ">>>>>" }'
<<<<<
1 2 3 4
>>>>>
    
`BEGIN`/`END` 是命令开始解析和结束解析时执行的动作.
`/1/` 指定了只有匹配该正则的行才进行处理. 默认是处理所有行.
echo '1,2,3,4.a,b,c,d' | awk 'BEGIN{RS="."; FS=","} {print $1}'
1
a
    
默认情况下以 "\n" 为一行, 而每一行以空白符分割列, 可以通过 RS 设置换行符, FS 指定列分隔符. 他们都支持正则, 例如 `FS=[\t ,.]`
---
date
2019年 12月 27日 星期五 15:42:29 CST
显示当前时区的系统时间. 可以使用 -u 显示 UTC 时间
date +"%Y-%m-%d %H:%M:%S"
2019-12-27 15:45:03
按指定格式显示时间. `date +"%s"` 可以获取 timestamp. 其他格式参考 `man date`
date -s "2019-12-27 15:45:03"
-
设置日期时间, 需要 root 权限

为何 node 底层同样使用多线程却依然比传统多线程快?

Ryan 说: Threads are expensive and should only be left to the experts of concurrent programming to be utilized.

node 底层确实使用多线程并发的执行我们代码中的异步函数, 但是它在底层会使用 epoll 等类似技术让多个 io 请求在一个线程上并发执行, 从而减少需要的线程数量. 线程少了, 内核调度开销也少了, 所以速度就快了, 也节省资源.

js 运行过程中, 每个 ‘对象’ 都有三种约束, 即 禁止扩展, 密封冻结

禁止扩展 (Object.preventExtensions)

禁止扩展的含义是 某个对象不能添加新的属性.例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const foo = {}

// 可以随意添加属性
foo.a = 1

// 对象被 *禁止扩展* 后, 则不能添加新属性
Object.preventExtensions(foo)
foo.b = 1 // 无法添加新属性. strict mode 下会抛异常

// 但是依然可以修改已有属性的值或者删除已有属性
foo.a = 2
delete foo.a

// 有一种假象: 我们可以修改对象的 __proto__ 属性, 从而间接 *扩展* 该对象
Object.getPrototypeOf(foo).b = 3
console.log(foo.b) // 假象. 貌似 b 被扩展, 而实际上是原型链被扩展
foo.b = 4 // 无效. 无法给 b 添加新属性

相关链接

密封 (Object.seal)

密封的含义是 某个对象禁止扩展, 同时该对象所有的属性描述符 configurable 变为 false(关于对象属性描述符, 参见 js 对象属性描述符详解). 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const bar = {
x: 1,
y: 2,
}

// 一旦 *密封*, 则无法修改属性描述符
Object.seal(bar)
Object.defineProperty(bar, 'x', { // TypeError: Cannot redefine property: x
enumerable: false,
})

// 当然, 也无法删除已有属性
delete bar.x // 失败, 返回 false

// 一个 *密封* 的对象, 必然是 *禁止扩展* 的
console.log(Object.isExtensible(bar)) // false

有一个例外, 属性的 writable 可以由 true 变为 false, 但是反向是不可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const bar = {
x: 1,
}

// 即使 *密封* 的对象, 其属性描述符 writable 也可以 true -> false
Object.seal(bar)
Object.defineProperty(bar, 'x', { // 成功. x 属性将变得不可写
writable: false,
})

// writable 由 false -> true 是不可以的
Object.defineProperty(bar, 'x', { // TypeError: Cannot redefine property: x
writable: true,
})

相关链接

冻结 (Object.freeze)

冻结的含义是 某个对象 密封, 同时该对象所有属性描述符 writable 变为 false. 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const baz = {
a: 1,
b: 2,
}

// 一旦 *冻结*, 无法修改属性描述符, 也无法修改属性的值
Object.freeze(baz)

Object.defineProperty(bar, 'a', { // TypeError: Cannot redefine property: a
enumerable: false,
})
delete baz.a // 失败, 返回 false
baz.a = 100 // 无效

// 一个 *冻结* 的对象, 必然是 *密封* 的
console.log(Object.isSeal(baz)) // true

相关链接

总结

禁止扩展 ⊆ 密封 ⊆ 冻结

增加新属性 属性描述符 configurable 属性描述符 writable 属性描述符 enumerable 属性的值 (value)
禁止扩展 (Object.preventExtensions) 不可以 - - - -
密封 (Object.seal) 不可以 false - - -
冻结 (Object.freeze) 不可以 false false - -

什么是 property, 什么是 descriptor

对象的没一个属性(property), 实际上都有一组描述符(descriptor) 对其进行约束. 比如我们日常写的 foo.a = 1, 实际上对应的底层实现是:

1
2
3
4
5
6
7
8
foo.a = 1 
// 等价于
Object.defineProperty(foo, 'a', {
value: 1,
writable: true,
configurable: true,
enumerable: true,
})

那么到底这些描述符都是干什么的, 又有多少个描述符呢? 我们往下看

一个 property 到底有多少个 descriptor

简言之, 一个 property 一共有 6 个 descriptor, 他们是:

  • value / writable
  • get / set
  • configurable
  • enumerable

这些 descriptor 被分为两类, data descriptor 和 access descriptor.

data descriptor

value/writable 称之为 data descriptor. 它可以组合 configurable / enumerable, 比如:

1
2
3
4
5
6
Object.defineProperty(foo, 'a', {
value: 1,
writable: true,
configurable: true,
enumerable: true,
})

access descriptor

get/set 称之为 access descriptor. 它也可以组合 configurable / enumerable, 比如:

1
2
3
4
5
6
Object.defineProperty(foo, 'a', {
get: function() { ... },
set: function() { ... },
configurable: true,
enumerable: true,
})

但是 data descriptor 和 access descriptor 是互斥的, 也就是说, 一个 property 要么有 value/writable, 要么有 get/set, 但是不能同时拥有两者.

descriptor 的作用

writable

writable 定义了对象的属性是否能够被重新赋值. 赋值操作符中默认是 true, Object.defineProperty() 函数中默认值是 false

如果 writable 为 false, 那么给该属性赋值时, 在严格模式下会抛出异常, 非严格模式下赋值操作无效.

1
2
3
4
5
6
7
8
'use strict'
const foo = {}
Object.defineProperty(foo, 'a', {
value: 1,
writable: false,
})

foo.x = 2 // TypeError: Cannot assign to read only property 'x' of object '#<Object>'

enumerable

enumerable 定义了对象的属性是否可以在 for…in 循环和 Object.keys() 中被枚举. 赋值操作符中默认是 true, Object.defineProperty() 函数中默认值是 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const foo = {}
Object.defineProperty(foo, 'a', {
value: 1,
enumerable: true,
})
Object.defineProperty(foo, 'b', {
value: 2,
enumerable: false,
})

for (let i in foo) {
console.log(i)
}

// 输出了 a

Object.keys(foo).forEach((i) => console.log(i))

// 输出了 a

与 enumerable 相关的 js 方法

Object.keys() 函数 Object.hasOwnProperty() 函数 for..in 表达式 in 操作符
自身属性, 可枚举 true true true true
自身属性, 不可枚举 false true false true
继承属性, 可枚举 false false true true
继承属性, 不可枚举 false false false true

对于枚举属性, 总结来说:

  • 首选 Object.keys(). 如果还要枚举 不可枚举 的属性, 则使用 Object.hasOwnProperty()
  • for..in 只会枚举 enumerable 的属性, 但是会遍历原型链
  • in 会枚举自身以及原型链上的所有属性, 无论该属性是否 enumerable

configurable

configurable 定义了对象的属性是否可以被修改或者删除. 赋值操作符中默认是 true, Object.defineProperty() 函数中默认值是 false

但是有一条例外, 无论 configurable 配置如何, 将 writable 由 true => false 的转变总是能够成功.

1
2
3
4
5
6
7
8
9
10
11
12
13
const foo = {}
Object.defineProperty(foo, 'a', {
value: 1,
configurable: false,
writable: true,
enumerable: true,
})

Object.defineProperty(foo, 'a', {
enumerable: false,
})

// TypeError: Cannot redefine property: a

与 configurable 相关的 js 方法

delete

  • 只能删除对象自身的属性, 不能删除原型链的属性
  • 不能删除静态属性

总结

data descriptor 和 access descriptor

描述符被分为两组:

  • data descriptor: value/writable 与 configurable/enumerable 的组合
  • access descriptor: get/set 与 configurable/enumerable 的组合

descriptor 作用和默认值

descriptor 作用 赋值操作时的默认值 Object.defineProperty 时的默认值
value 设置 property 的值 true false
writable property 的值可以被修改 true false
get 读该 property 时返回的值 undefined undefined
set 写 property 时的具体操作 undefined undefined
configurable 该 property 的 descriptor 是否能够被修改或者删除 true false
enumerable 该 property 是否能够被 for .. in 和 Object.keys 枚举到 true false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const foo = {};

foo.a = 1;
// 等同于 :
Object.defineProperty(o, "a", {
value : 1,
writable : true,
configurable : true,
enumerable : true
});


// 另一方面,
Object.defineProperty(foo, "a", { value : 1 });
// 等同于 :
Object.defineProperty(foo, "a", {
value : 1,
writable : false,
configurable : false,
enumerable : false
});

refs

Object.defineProperty()

MDN: 属性的可枚举性和所有权

关于为什么会有 React.Children.forEach 而不直接用 this.props.forEach 的问题, 我在 React.Children.xxx 的作用 已经说得很明白了. 本文我们要更进一步, 分析一下 React.Children 的实现, 从而更好的理解 react 背后的故事. Let’s go.

追根溯源

几经跳转, 我们能看到 React.Children 的所有方法, 如下:

1
2
3
4
5
6
7
8
9
10
var React = {
Children: {
map: ReactChildren_1.map,
forEach: ReactChildren_1.forEach,
count: ReactChildren_1.count,
toArray: ReactChildren_1.toArray,
only: onlyChild_1
},
...
}

这几个函数除了 only 以外, 都要对 children 进行遍历. 而它们的代码结构也非常类似, 我们只要看懂了一个, 其他的就都通了.

我们来重点分析 forEach 方法, 因为它是一个比较纯粹的遍历函数.

上面的代码中, ReactChildren_1 仅仅是 ReactChildren 的引用, 如下:

1
2
3
4
5
6
7
8
var ReactChildren = {
forEach: forEachChildren,
map: mapChildren,
count: countChildren,
toArray: toArray
};

var ReactChildren_1 = ReactChildren;

因此, 最终代码实际上调用的是 forEachChildren 方法:

1
2
3
4
5
6
7
8
function forEachChildren(children, forEachFunc, forEachContext) {
if (children == null) {
return children;
}
var traverseContext = getPooledTraverseContext(null, null, forEachFunc, forEachContext);
traverseAllChildren(children, forEachSingleChild, traverseContext);
releaseTraverseContext(traverseContext);
}

别问我为什么有什么多层赋值, 我也不知道啊…

框架分析

大部分 React.Children 函数都是这个套路,

  1. getPooledTraverseContext: 作用是将你的 forEachFunc 函数 forEachContext(相当于 this 指针) 封装在一起, 便于传入后面的 traverseAllChildren 方法
  2. traverseAllChildren 这里具体执行了遍历, 它内部会进行 children 的递归操作
  3. releaseTraverseContext 是将第一步 getPooledTraverseContext 的结果 “释放” 掉. 为什么需要释放? 是因为第一步相当于从一个 pool 中取了一个对象来用, 这里用完了, 要放回到 pool 中. 看完后面 ‘细节代码’ 你就明白了

这里可以总结一下框架思路:

  1. 用户 forEach 传入的处理函数传入的上下文 打包成一个 context 对象
  2. 递归遍历 children, 执行上述 context 中的 用户处理函数
  3. 释放 1 中的 context

细节源码

下面, 我们进入重点, 来看一看其中的实现原理

getPooledTraverseContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var POOL_SIZE = 10;
var traverseContextPool = [];
function getPooledTraverseContext(mapResult, keyPrefix, mapFunction, mapContext) {
if (traverseContextPool.length) {
var traverseContext = traverseContextPool.pop();
traverseContext.result = mapResult;
traverseContext.keyPrefix = keyPrefix;
traverseContext.func = mapFunction;
traverseContext.context = mapContext;
traverseContext.count = 0;
return traverseContext;
} else {
return {
result: mapResult,
keyPrefix: keyPrefix,
func: mapFunction,
context: mapContext,
count: 0
};
}
}

getPooledTraverseContext 实际上维护了一个 size 为 10 的缓冲池. 如果 pool 中有存货, 则 pop 出一个进行使用. 如果 pool 中空空如也, 则 return 一个新的对象.

当然, 这个函数的重点并不是缓冲池, 而是返回的对象本身. 要记住这两个字段:

  • func: 这就是用户传入的 forEach 处理函数
  • context: 这是个可选参数, 用户可以传入作为调用上述 func 时的上下文. 看到这里你就知道, 默认情况下, 你的 处理函数执行的时候, 是没有 context, 也就是处理函数中, this === undefined. 如果想在 处理函数中绑定 this, 只能通过这个参数指定. 这一点在后面分析 forEachSingleChild 会看到原理

traverseAllChildren

这是我们最重要的函数, 我们来回顾一下他的参数:

1
2
3
4
5
function forEachChildren(children, forEachFunc, forEachContext) {
...
traverseAllChildren(children, forEachSingleChild, traverseContext);
...
}

绑定上下文调用处理函数

其中, children 是要遍历的子节点对象, traverseContext 是上一步封装了 处理函数处理函数执行上下文 的一个对象. 而 forEachSingleChild 则是真正调用处理函数的方法:

1
2
3
4
5
6
function forEachSingleChild(bookKeeping, child, name) {
var func = bookKeeping.func,
context = bookKeeping.context;

func.call(context, child, bookKeeping.count++);
}

看, func.call(context, child, ...) 这就是绑定上下文调用处理函数的秘密.

递归遍历

traverseAllChildren 只是个入口, 真实的递归遍历是定义在 traverseAllChildrenImpl 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 外层入口
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}

return traverseAllChildrenImpl(children, '', callback, traverseContext);
}

// 真正递归遍历的实现
function traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext) {
...
}

我们来重点分析一下 traverseAllChildrenImpl.

处理单个 child

先来看上半部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext) {
var type = typeof children;

if (type === 'undefined' || type === 'boolean') {
// All of the above are perceived as null.
children = null;
}

if (children === null || type === 'string' || type === 'number' ||
// The following is inlined from ReactElement. This means we can optimize
// some checks. React Fiber also inlines this logic for similar purposes.
type === 'object' && children.$$typeof === REACT_ELEMENT_TYPE) {
callback(traverseContext, children,
// If it's the only child, treat the name as if it was wrapped in an array
// so that it's consistent if the number of children grows.
nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);
return 1;
}

... 下部分处理递归, 后面分析 ...
}

typeof 操作符一共能有几种返回值? 来看看:

类型 返回值 备注
Undefined “undefined”
Null “object” 实际上是被 traverseAllChildren 在入口被处理了
Boolean “boolean”
Number “number”
String “string”
Symbol “symbol”
函数对象 “function”
任何其他对象 “object”

: 红色部分是当前被处理的类型

这个分支处理了大部分类型. 这里的 children 实际上是单个对象, 并不是像它的名字一样是个复数. 接下来执行 callback(traverseContext, children, nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar) 并返回 1.

看到这里, 我们就知道, children 不仅仅可以是 Component, 还可以是 String/Boolean/Undefined 等等.

我们回忆一下 traverseAllChildren 就会知道, 这个 callback 实际上是 forEachSingleChild:

1
2
3
4
5
function forEachChildren(children, forEachFunc, forEachContext) {
...
traverseAllChildren(children, forEachSingleChild, traverseContext);
...
}

forEachSingleChild 则调用 func.call(context, child, ...) 实现了绑定上下文调用处理函数. 所以 callback(traverseContext, children, ...) 可以简单理解为以 children(实际上是单个对象, 并不是集合, 名字容易误导读者) 为参数, 调用了用户的处理函数.

处理 children (集合)

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
function traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext) {

... 上部分处理单个 child, 已经分析过了. 下面来分析递归调用处理 children ...

var child;
var nextName;
var subtreeCount = 0; // Count of children found in the current subtree.
var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

if (Array.isArray(children)) {

// !! 如果是 Array, 则深度递归 !!

for (var i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getComponentKey(child, i);
subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
}
} else {

// !! 如果不是 Array, 则看该对象是否可迭代 !!

var iteratorFn = ITERATOR_SYMBOL && children[ITERATOR_SYMBOL] || children[FAUX_ITERATOR_SYMBOL];
if (typeof iteratorFn === 'function') {

// !! 如果是 Map, 则警告用户不支持 !!

{
// Warn about using Maps as children
if (iteratorFn === children.entries) {
warning$2(didWarnAboutMaps, 'Using Maps as children is unsupported and will likely yield ' + 'unexpected results. Convert it to a sequence/iterable of keyed ' + 'ReactElements instead.%s', getStackAddendum());
didWarnAboutMaps = true;
}
}

// !! 其他可迭代对象, 则使用迭代方法, 深度遍历 !!

var iterator = iteratorFn.call(children);
var step;
var ii = 0;
while (!(step = iterator.next()).done) {
child = step.value;
nextName = nextNamePrefix + getComponentKey(child, ii++);
subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
}
} else if (type === 'object') {

// !! 如果该对象不可迭代, 则提示错误 !!

var addendum = '';
{
addendum = ' If you meant to render a collection of children, use an array ' + 'instead.' + getStackAddendum();
}
var childrenString = '' + children;
invariant_1(false, 'Objects are not valid as a React child (found: %s).%s', childrenString === '[object Object]' ? 'object with keys {' + Object.keys(children).join(', ') + '}' : childrenString, addendum);
}
}

return subtreeCount;
}

看代码中的注释, 应该很清楚的明白 React.Children.forEach 是不支持 Map 但却支持 Set 的.

releaseTraverseContext

1
2
3
4
5
6
7
8
9
10
function releaseTraverseContext(traverseContext) {
traverseContext.result = null;
traverseContext.keyPrefix = null;
traverseContext.func = null;
traverseContext.context = null;
traverseContext.count = 0;
if (traverseContextPool.length < POOL_SIZE) {
traverseContextPool.push(traverseContext);
}
}

这个方法简单的不能再简单, 核心目的就是 if 里面的那块代码, 如果池数量小于 POOL_SIZE(上文中得知这个数字是 10), 则把对象放回到池中, 以备后续使用.

回顾

至此, React.Children.forEach 就分析完了. 回过头来看看:

1
2
3
4
5
6
7
8
9
10
11
12
function forEachChildren(children, forEachFunc, forEachContext) {

// !! 将 *处理函数* 和 *上下文* 封装成一个对象(`traverseContext`) !!
var traverseContext = getPooledTraverseContext(null, null, forEachFunc, forEachContext);

// !! 深度遍历子元素, 并调用 *处理函数* !!
traverseAllChildren(children, forEachSingleChild, traverseContext);

// !! 释放封装了 *处理函数* 和 *上下文* 的对象 !!
releaseTraverseContext(traverseContext);

}

这三步, 是不是很简单?

总结

我们回顾一下 React.Children.forEach 能够处理的类型:

类型 返回值 备注
Undefined “undefined”
Null “object” traverseAllChildren 在入口被处理
Boolean “boolean”
Number “number”
String “string”
Symbol “symbol”
函数对象 “function”
可迭代对象 “object”
其他对象 “object”

总的来说, React.Children.forEach 是通过 typeof 操作符, 对 children 进行判断, 进行深度遍历后完成了任务.

Refs

React.Children.xxx 的作用

我们在编写 React 的时候, 经常会遇到 React.Children.map(...) 这种写法. 然而 js 中明明有对等的函数, 如下:

  • React.Children.map(this.props.children, ...) <=> this.props.children.map(...)
  • React.Children.forEach(this.props.children, ...) <=> this.props.children.forEach(...)
  • React.Children.count(this.props.children) <=> this.props.children.length
    那么为什么还会有 React.Children.xxx 呢?

要想知道这个问题, 首先要回答 this.props.children 到底是什么.

this.props.children 到底是什么

我们来列举几种 case
![](/images/React.Children.xxx 的作用/React.Children.xxx-typeof.png)
可见, children 可以是任何类型

this.props.children.count / map 有什么问题?

![](/images/React.Children.xxx 的作用/React.Children.xxx-result.png)
这就很明显了, 当 children 为 Component 数组 的时候, React.Children.xxxthis.props.children.xxx 都可以正常工作.

同样, Set 也属于可迭代对象, 所以 React.Children 系列也能够正确的计算出长度, 而 this.props 系列显然无能为力. 但是 React 对于 Map 作为 children 是不支持的, 会得到一个 warning ‘Using Maps as children is unsupported and…’

当 children 是 单一对象 时, React.Children.xxx 可以正常计算 count, 但是 this.props.children 由于不是数组, 所以 length 操作会失败.

但是无论 React.Children 系列还是 this.props 系列, 对于 function 而言都是有问题的. 这里并不是说不能够使用 function 作为 children, 而是说如果你要将 function 作为 children 传入, 希望你知道自己在做什么.

结论

  1. 除非使用 PropTypes 限定, 否则不要对 children 有任何假设, children 可以是任何类型
  2. React.Children 提供了一些方便的函数帮助我们遍历和统计 children, 不要自己重造轮子了

refs

A deep dive into children in React

React.Children.forEach 源码分析E5%88%86%E6%9E%90/)E5%88%86%E6%9E%90/)

起源

http 是不安全的, 源于:

  • 网络的通信线路是公用的. 你的数据同样会被传送到别人的网卡里
  • http 是明文传输

在网上传输 http 相当于裸奔!

此外, https 相对于 http, 还有速度上的优势. 具体 https 的优点, 可以看看 Google I/O 2016.

https 工作原理

简单来说, 在左侧原始 http 的基础上, 右侧的 https 引入了一层 SSL, 在数据发送和接收的时候, 自动的进行加密/解密. 这样的加密数据在网络上传输, 即便被窃取到也不会有问题. 同时, 对于接收数据而言, 上层应用(http) 依然会收到被 SSL 层解析好的明文数据, 这对于已存在的 http 应用提供了 100% 的兼容性.

https 实践

证书机构选择

当下有很多证书机构:

经过一番调研和对比, 直接选择 Let’s Encrypt 就好了. 免费, 快速, 方便自动化续签, 未来支持潜力更好…

Let’s Encrypt 工作流程

假设我们有:

  • 一台装有 nginx 的 vps
  • 一个域名(walfud.com), 这个域名已经指向了上述 vps
申请证书

想要使用 https, 我们需要向 Let’s Encrypt 证明你拥有该域名的控制权. 传统的证书申请机构是使用邮箱作为验证, 即: 发送一封邮件到你要签名的域名让你去确认. 而 Let’s Encrypt 使用一种更加先进的方法, 叫做 ACME. 这种方法的大致思路就是: 在你域名所指向的服务器上写入某些随机内容的文件, 如果发起 ACME 的服务器能够读成功, 就认为你拥有该域名的控制权. ACME 对比传统的邮件方式, 可以免去人工打开邮箱等过程, 从而实现自动化续签. 如图:

  1. 在你的 vps 上下载官方推荐的工具: certbot. 这里我们选择 NginxUbuntu 16.04 (xenial). 如图
  2. 下面的 automated 页面中, 介绍了具体执行的方法. 我直接给出命令:
    1
    2
    3
    wget https://dl.eff.org/certbot-auto
    chmod a+x certbot-auto
    ./certbot-auto --nginx --agree-tos --email hi@walfud.com -d walfud.com
    其中,
    • 第一次运行的话, 可能 install python… 这一步会卡很久, 请耐心等待, 它真的是很慢而已…
    • hi@walfud.com 替换为你的 email !!!
    • /usr/share/nginx/htmlACME Challenge 的路径(看后面的 术语)
    • walfud.com 这就是你要申请的域名. 这个域名一定要指向你的这台 vps!!! (先 ping 一下看看结果是不是你主机的 ip)
  3. 成功会出现: Congratulations! Your certificate and chain have been saved at…, 如图:

    图中以 test.walfud.com 作为域名进行验证. 请以你本地输出为准

检查 /etc/letsencrypt/live/walfud.com/ 这个目录(可能需要 sudo 权限), 应该已经产生了如下几个文件 (完整解释请看这里: certbot 的 webroot 插件文档):

  • cert.pem: 你不用关心 (这个实际上是服务器证书文件)
  • chain.pem: 你不用关心 (这个实际上是… 自己看文档吧, 我没读懂. 貌似是个递归查找用的链式证书)
  • fullchain.pem: cert.pem + chain.pem 的合体. 需要配置到 nginx 配置文件中的 ssl_certificate.
  • privkey.pem: 私钥. 需要配置到 nginx 配置文件中的 ssl_certificate_key.
配置 Nginx 使用证书
  1. 打开 /etc/nginx/default.conf(如果没有则创建), 然后粘贴我的配置:
    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
    server {
    listen 80;
    # listen [::]:80;
    server_name walfud.com;

    # Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
    return 301 https://$host$request_uri;
    }

    server {
    listen 443 ssl http2;
    # listen [::]:443 ssl http2;
    server_name walfud.com;

    location / {
    root /usr/share/nginx/html;
    index index.html index.htm;
    }

    #error_page 404 /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
    root /usr/share/nginx/html;
    }

    # certs sent to the client in SERVER HELLO are concatenated in ssl_certificate
    ssl_certificate /etc/letsencrypt/live/walfud.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/walfud.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
    # ssl_dhparam /etc/ssl/certs/dhparam.pem;

    # intermediate configuration. tweak to your needs.
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
    ssl_prefer_server_ciphers on;

    # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
    add_header Strict-Transport-Security max-age=15768000;

    # OCSP Stapling ---
    # fetch OCSP records from URL in ssl_certificate and cache them
    ssl_stapling on;
    ssl_stapling_verify on;

    ## verify chain of trust of OCSP response using Root CA and Intermediate certs
    # ssl_trusted_certificate /etc/letsencrypt/live/walfud.com/root_ca_cert_plus_intermediates;

    # resolver 8.8.8.8 8.8.4.4 valid=300s;
    # resolver_timeout 5s;
    }
    别忘了把 walfud.com 都替换为你的域名!!!
  2. 重启 Nginx: nginx -s reload. 好了, 打开浏览器, 试试吧.
证书续签

你买域名的时候, 都会有一个期限, 一年, 两年, 五年或者十年. 域名不能永远属于你, 那么证书也需要一个过期时间.

Let’s Encrypt 家的证书有效期是 90 天, 所以我们为了保证证书的有效性, 就需要定期的 renew(续签) 证书. 一般来说, 大家都是用 cron 系统服务来定期的执行 renew:

  • crontab -e 打开编辑界面
  • 写入每天夜里凌晨 2 点重新续签:
    1
    2
    3
    4
    5
    # 每天夜里凌晨 2 点续签:
    * 2 * * * letsencrypt renew

    # 重启 nginx 以使证书生效
    * 3 * * * nginx -s reload
    这样, 每天都会尝试续签一次证书. 你不用担心什么, 如果证书没过期, 这个命令什么也不做. 如果证书过期, 则会帮你自动刷新证书.

FAQ

http -> https 有什么变化? ![](/images/https_nginx/http_https.png)

Q: 证书是针对域名签发的还是 IP 签发的?
A: 是针对域名签发的. 我曾经在 123.206.49.60 这台机器上申请了 test.walfud.com 的证书, 然后把证书放到 123.56.81.162 机器上, 依然可以使用.

术语解释

  • 对称密码: 通信两端使用同一密码进行加密/解密. 这个密码就称对称密码. 参考 对称密码与公钥密码
  • 公钥 / 私钥: 通信两端使用非对称密码交换数据. 参考 对称密码与公钥密码
  • (数字)签名: 发送者证明所发的内容确实来自于自己的凭证. 其原理利用 私钥 对数据进行加密, 公钥 对加密后的数据进行解密验证. 参考 密码学 ABC
  • 证书: 其实是数字签名的一种应用. 即: 公钥 + 公钥的数字签名. 用以证明该公钥是可信的. 参考 <<图解密码技术>>
  • 续签: 每个证书都有有效期, 过了有效期需要重新向颁发机构申请.
  • ACME Challenge: 即: ACME 服务器生成一个随机序列, 然后尝试读取某个域名下的该序列. 如果该序列能够被读取, 则认为你拥有该域名的控制权.

Refs

Let’s Encrypt 给网站加 HTTPS 完全指南

在 Nginx 上使用 Let’s Encrypt 加密(HTTPS)你的网站

Let’s Encrypt Getting Started

Let’s Encrypt How It Works

certbot Retrive & Renew Certificates Automatically

certbot 的 webroot 插件文档

How To Secure Nginx with Let’s Encrypt on Ubuntu 16.04

How To Use Cron To Automate Tasks On a VPS

帮你生成 Nginx 配置的最佳实践: Mozilla SSL Configuration Generator

对称密码与公钥密码

密码学 ABC (可惜配图没了, 但不影响阅读)

https 运行原理

https 运行原理

对称密码 (也称: 共享密码)

简单来说, 任何一端生成一个密码, 发送给另一端. 此后两端通信使用都是用该密码进行加密解密.

存在的问题

如图中 Sniffer 所做, 任何 Client 和 Server 间的数据都可以被窃取. 包括图中 1 所做的: 交换对称秘钥. 一旦 Sniffer 获取了通信两端用于加密数据的对称秘钥, 那么后续的密文都可以被解密.

常见算法


公钥密码

公钥密码都是基于某个数学难题, 从而实现如下功能:

  • 某个算法可以生成两个数据, 一个叫做私钥, 一个叫做公钥
  • 私钥
    • 可加密任何数据
    • 可解密被公钥加密的数据
    • 用于生成公钥
    • 只能自己保留
  • 公钥
    • 可加密任何数据
    • 可解密被私钥加密的数据
    • 可以公开给别人

上图中,

  1. 客户端 -> 服务端: 传递 公钥
    1. 客户端使用某种算法生成了一对秘钥(公钥私钥), 并将 公钥 发送给了服务端
    2. 服务端收到了 公钥. 同时, Sniffer 也窃听到了公钥
  2. 服务端 -> 客户端: 传递 对称密码
    1. 服务端生成了一个 对称密码, 然后使用刚收到的公钥加密了这个 对称密码, 并发送给了客户端
    2. 客户端收到了这段密文. 使用自己的 私钥 解密了密文, 获得了从服务端传来的 对称密码
    3. 但此时, Sniffer 虽然也窃取到了这段密文, 但是 Sniffer 没有 私钥, 因此无法解密这段内容
  3. 客户端 -> 服务端: 使用 对称密码 通信
    1. 客户端使用刚解密得到的 对称密码 对数据加密, 然后发送给客户端
    2. 服务端又收到了密文, 并使用之前生成的 对称密码 解密数据, 得到了原文
    3. 此时, Sniffer 依然能够窃取到密文, 但是他没有 对称密码, 所以还是无法解密
  4. 客户端 -> 服务端: 使用 对称密码 通信
    1. 客户端和服务端继续使用 对称密码 进行通信
    2. Sniffer 即便窃取到密文, 也无法解密

解决的问题

解决了 对称密码秘钥配送问题. 即: 客户端成功的将密码安全的发送给服务端, 而不被窃听者获取到原文.

常见算法

  • RSA

本文不是基础教程, 如果对原型的基础概念还不理解, 请先看看 ref 中的链接.

基础概念

先来看看基础概念:

  • 普通对象
    • __proto__ 指向该对象的原型 (后面有解释), 通常是一个 ‘Object 原型对象’ (后图中的 A)
  • 构造函数对象
    • __proto__ 指向本函数对象的原型 (后面有解释), 通常是一个 ‘Function 原型对象’ (后图中的 B)
    • prototype 的作用是: 该函数所创建对象后, 为被创建对象的 __proto__ 赋值. 所以任何通过 new Xxx() 创建的对象的 __proto__ 都等于 Xxx.prototype, 即 new Xxx().__proto__ === Xxx.prototype
  • prototype 对象
    • 本对象也是一个普通对象
    • __proto__ 指向本对象的原型, 通常是一个 ‘Object 原型对象’ (后图中的 A)
    • constructor 是个反向链接, 反过来指向构造函数对象 (见后图)

有了上述基础, 我们来看一下 js 几个引擎内建对象的关系:

(红色是普通对象, 蓝色是函数对象)

首先, 引擎有个内建的 A 对象, 该对象:

B 也是引擎内建对象:

  • 是所有 ‘函数对象’ 的原型
  • 本对象的 __proto__ 指向了 A. 换句话说: B 扩展了 A, 并增加了一些函数会用到的方法, 如下
  • 提供了最基础的函数方法, 包括:

C 和 D 依然是引擎的内建对象, 不过这两个是内建的函数对象:

  • Object (即 C 对象) 函数:
    • __proto__ 指向 B, 即: Object 自身是一个函数对象
    • prototype 指向 A, 即: 通过 new Object() 创建的对象都是 ‘普通对象’
  • Function (即 D 对象) 函数:
    • __proto__ 指向 B, 即: Function 自身是一个函数对象
    • prototype 指向 B, 即: 通过 new Function() 创建的对象都是 ‘函数对象’

综上, 可以理解为: ObjectFunction 都是函数对象, 但是: new Object() 创建的都是普通对象(被创建的对象 __proto__ 指向 A), 而被 new Function() 创建的对象都是函数对象(__proto__ 指向 B).

是时候捋一捋脉络了. 看一下完整的 js 原型图:

从上往下看.

引擎内建级对象

最上面是 null, 下面紧接着是一切对象的原型 — Op, 其中包含了 hasOwnProperty / toString...对象 所需要的方法. Op.__proto__ == null 代表着 Op 没有原型.
Op 下面分别是 Fp, Sp (另一个泛指代表其他内建类型). Fp 是一切 函数对象 的原型, 包含了 call / bind函数 所需的方法. 不过 Fp 也是一个对象, 所以它的原型是 Op(Op 是一切对象的原型). Sp 同理.

API 级对象

再往下看, 这一层级是我们常见的 js API. 以 Object(图中 this is the built-in `Object` function 所代表的对象) 为例. Object 自身是一个对象, 因此其 __proto__ 指向 Op. 但是 Function 又是个函数, 所以它将具有 prototype 属性. 然而它的功能是创建新的 普通对象, 所以其 prototype 指向了 Op, 这代表凡是 const myObj = new Object() 创建的对象, 那么 myObj.__proto__ === Op(因为 new 方法会将 Object.prototype 赋值给新创建的对象).

用户级对象

看图中最后一行的 custom object 和 custom function object. 这些都是用户自定义对象, 也就是我们日常代码中的各种 const myObj = { ... }.

默认情况下, 引擎在我们创建对象时帮我们把新建 普通对象__proto__ 设置为 Op, 函数对象__proto__ 设置为 Fp. 因此我们自定义的对象才能够调用 getOwnProperty / call 等方法.

MISC.

判断自定义对象原型 (A instanceOf B)

沿着 A 的 __proto__ 这条线来找,同时沿着 B 的 prototype 这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回 true。如果找到终点还未重合,则返回 false.

refs:

快速入门

npm install mongoose

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
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');

const Cat = mongoose.model('Cat', { name: String });

// Model#save([options], [options.safe], [options.validateBeforeSave], [fn])
const kitty = new Cat(
{ name: 'walfud' }
);
kitty.save(function (err) {
...
});

// Query#find([criteria], [callback])
Cat.find(
{},
(err, doc) => {
...
});
// Query#findOne([callback]) --- 返回第一条 doc
// Query#findOne([criteria], [projection], [callback])
Cat.findOne((err, doc) => {
...
});

// Document#update(doc, options, callback)
kitty.update(
{ name: 'duflaw' },
{ w: 1 },
(err, doc) => {
...
});
// Query#update([criteria], [doc], [options], [callback])
Cat.update(
{ name: 'walfud' },
{ name: 'duflaw' },
(err, doc) => {
...
});

// Model#remove([fn])
kitty.remove((err, doc) => {
...
});
// Query#remove([criteria], [callback])
Cat.remove(
{ name: 'walfud' },
(err, doc) => {
...
});

Schema

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
const MySchema = new Schema({

// 支持的类型
str: String,
num: Number,
date: Date,
buffer: Buffer,
bool: Boolean,
anything: Schema.Types.Mixed,
id: Schema.Types.ObjectId,

// 数组和嵌套
strings: [String],
nested: {
stuff: { type: String, lowercase: true, trim: true }
},
nested2: OtherSchema,


// 限定
// 通用
general: {
type: Schema.Types.Mixed,
required: true,
default: "default value",
select: true, // 设置 `find` 的时候是否是默认的 projection
validate: { // 自定义 validator
validator: function(v, cb) {
setTimeout(function() {
cb(/\d{3}-\d{3}-\d{4}/.test(v));
}, 5);
},
message: '{VALUE} is not a valid phone number!'
},
required: [true, 'User phone number required']
}
// 针对 Number
range: {
type: Number,
min: 18,
max: 65
},
// 针对 String
lower: {
type: String,
lowercase: true,
uppercase: true,
trim: true,
match: /^...$/,
enum: ["foo", "bar"]
},


// Getter/Setter
integer: {
type: Number,
get: v => Math.round(v),
set: v => Math.round(v),
},


// MISC.
primary: {
type: String,
index: true,
unique: true, // 如果指定了 `unique: true`, 则默认 `index: true`, 因此可以不指定 `index`.
}

});

特别注意: 对于 Date/Mixed 类型, mongoose 无法追踪值得变更, 因此需要手动标记:

1
2
3
4
5
6
7
8
const Assignment = mongoose.model('Assignment', { dueDate: Date });
Assignment.findOne((err, doc) => {
doc.dueDate.setMonth(3);
doc.save(callback); // THIS DOES NOT SAVE YOUR CHANGE

doc.markModified('dueDate');
doc.save(callback); // works
});

Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Tank = mongoose.model('Tank', yourSchema);

const small = new Tank({ size: 'small' });
small.save(function (err) {
if (err) return handleError(err);
// saved!
});

// or

Tank.create({ size: 'small' }, function (err, small) {
if (err) return handleError(err);
// saved!
});

注意事项: model 方法会复制 schema 对象, 因此, 一定要在调用 .model() 之前设置好 schema 对象!

CRUD

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
const Person = mongoose.model('Person', yourSchema);

// find each person with a last name matching 'Ghost', selecting the `name` and `occupation` fields
Person.findOne({
'name.last': 'Ghost' // criteria, 查询条件, 与 mongo shell 一致
},
'name occupation', // projection, 选择返回的列
function(err, person) { // callback
if (err) return handleError(err);
console.log('%s %s is a %s.', person.name.first, person.name.last, person.occupation) // Space Ghost is a talk show host.
}
);

// or

// find each person with a last name matching 'Ghost'
const query = Person.findOne({
'name.last': 'Ghost'
});

// selecting the `name` and `occupation` fields
query.select('name occupation');

// execute the query at a later time
query.exec(function(err, person) {
if (err) return handleError(err);
console.log('%s %s is a %s.', person.name.first, person.name.last, person.occupation) // Space Ghost is a talk show host.
});

// or 使用 cursor 进行 stream query

const cursor = Person.find({
occupation: /host/
}).cursor();
cursor.on('data', function(doc) {
// Called once for every document
});
cursor.on('close', function() {
// Called when done
});
method return
find [{}, {}, …], document 列表
findOne {}, 某个 document
update Number, 影响的行数
count Number, 行数

其中, ‘criteria(查询条件)’ 与 mongo shell 一致, 参考 Operators.

Middleware(Hook)

Middleware 分为两种:

  • Document Middleware: this 引用是被更新的 document

    • init
    • validate
    • save
    • remove
  • Query Middleware: this 引用是 query 对象

    • count
    • find
    • findOne
    • findOneAndRemove
    • findOneAndUpdate
    • insertMany
    • update
1
2
3
4
5
6
7
8
9
10
11
// Pre
const schema = new Schema(..);
schema.pre('save', function(next) {
// do stuff
next();
});

schema.post('save', function(doc, next) {
// do stuff
next();
});

Plugin

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
// lastMod.js
module.exports = function lastModifiedPlugin(schema, options) {
schema.add({
lastMod: Date
});

schema.pre('save', function(next) {
this.lastMod = new Date
next()
});
}

// game-schema.js
const lastMod = require('./lastMod');
const Game = new Schema({...
});
Game.plugin(lastMod);

// player-schema.js
const lastMod = require('./lastMod');
const Player = new Schema({...
});
Player.plugin(lastMod);

// 也可以一次性对所有 model 设置 plugin

const mongoose = require('mongoose');
mongoose.plugin(require('./lastMod'));

const gameSchema = new Schema({ ... });
const playerSchema = new Schema({ ... });
// `lastModifiedPlugin` gets attached to both schemas
const Game = mongoose.model('Game', gameSchema);
const Player = mongoose.model('Player', playerSchema);

refs:

构造解析

1
2
3
4
5
6
7
8
9
10
11
moment();                               // 以当前时间构造
moment(1270451403123); // timestamp 构造 (毫秒)
moment.unix(1270451403123.123); // timestamp 构造 (秒)
moment([2010, 3, 5, 15, 10, 3, 123]); // [年, 月, 日, 时, 分, 秒, 毫秒]
moment({
years: '2010', months: '3', days: '5',
hours: '15', minutes: '10', seconds: '3', milliseconds: '123'
});

moment.utc([2016, 3, 5]).format('YYYY-MM-DD HH:mm:ss'); // utc 2016-04-05 00:00:00 -> local 2016-04-05 08:00:00
moment([2016, 3, 5]).utc().format('YYYY-MM-DD HH:mm:ss'); // local 2016-04-05 00:00:00 -> utc 2016-04-04 16:00:00

格式化输出

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
//   YYYY/YY: year
// MM/M: month, [1, 12]
// DD/D: day, [1, 31]
// HH/H: hour, [0, 23]
// mm/m: minute
// ss/s: second
// SSS: millisecond
// ZZ/Z: timezone, +08:00 / +0800
//
// a/A: am,pm / AM,PM
// DDDD/DDD: day of year, [1, 365]
// e: day of week, [0, 6]
// Q: quarter of year, [1, 4]
// MMMM/MMM: literal month, [Jan, Dec] / [January, December]
// dddd/ddd: literal week, [Sun, Sat] / [Sunday, Saturday]
//
// http://momentjs.com/docs/#/displaying/format/
moment().format('YYYY-MM-DD HH:mm:ss'); // local 2016-08-18 20:38:53
moment.utc().format('YYYY-MM-DD HH:mm:ss'); // utc 2016-08-18 12:38:53
moment().utcOffset(8).format('YYYY-MM-DD HH:mm:ss'); // specified 2016-08-18 20:38:53

// format en zh-cn
moment().format('LT'); // LT 11:06 AM 上午11点06分
moment().format('LTS'); // LTS 11:06:37 AM 上午11点6分37秒
moment().format('L'); // L/l 08/19/2016 2016-08-19
moment().format('LL'); // LL/ll August 19, 2016 2016年8月19日
moment().format('LLL'); // LLL/lll August 19, 2016 11:06 AM 2016年8月19日上午11点06分
moment().format('LLLL'); // LLLL/llll Friday, August 19, 2016 11:06 AM 2016年8月19日星期五上午11点06分

Set / Get 方法

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
// Set
moment().set({'year': 2013, 'month': 3});
moment().set('year', 2013);
moment().set('month', 3); // April
moment().set('date', 1);
moment().set('hour', 13);
moment().set('minute', 20);
moment().set('second', 30);
moment().set('millisecond', 123);

// Get
moment().valueOf();
moment().unix();
moment().get('year');
moment().get('month'); // 0 to 11
moment().get('date');
moment().get('hour');
moment().get('minute');
moment().get('second');
moment().get('millisecond');
moment().toObject() // {
// years: 2015
// months: 6
// date: 26,
// hours: 1,
// minutes: 53,
// seconds: 14,
// milliseconds: 600
// }

日期运算

1
2
3
4
moment().add(1, 'day');
moment().subtract(1, 'day');
moment([2008, 6]).diff(moment([2007, 0]), 'years'); // 1
moment([2008, 6]).diff(moment([2007, 0]), 'years', true); // 1.5

其他

1
2
3
4
5
6
7
8
9
moment().clone();

moment().isBefore(moment(1270451403123));
moment().isSameOrBefore(moment(1270451403123));
moment().isAfter(moment(1270451403123));
moment().isSameOrAfter(moment(1270451403123));
moment().isBetween(moment(1270451403123), moment(2270451403123));

moment().isLeapYear();

时区

1
2
moment().locale('zh-cn');   // local
moment.locale('zh-cn'); // global

拒绝无脑推荐!

想必看到本文的人都是经过纠结后最终选择 atom 的人. 目前看来, 我认为你的选择是明智的, 因为:

  • atom 跨平台. 你在 windows / mac / linux 都能用
  • atom 拥有大量的插件
  • atom 由 GitHub 力推
  • atom 开源并且使用 js(nodejs) 编写. 目前看来, js 还是最火的语言没有之一

那么这个号称 ‘二十一世纪’ 的编辑器没有插件一样无法起飞. 以下我介绍常用的快捷键以及几个必备利器.

常用

  • Ctrl-n 新建文件
  • Ctrl-Shift-n 新建文件夹
  • Ctrl-w 关闭文件
  • Ctrl-Shift-w 关闭 atom (慎用! 这个不保存内容!!!)
  • Alt-1 切换到第 1 个 tab, 同理, 可以使用 2, 3, 4… 切换 tab
  • Ctrl-PageUp / Ctrl-PageDown 快速向左/向右切换 tab
  • Ctrl-t 快速打开文件
  • Ctrl-f 当前文件中查找和替换
  • Ctrl-Shift-f 所有文件中查找和替换
  • Ctrl-r 在当前文件中查找函数的定义
  • Ctrl-g 快速跳转到某行
  • Ctrl-Alt-F2 打标签
  • F2 跳转到下一个标签
  • Shift-F2 跳转到上一个标签
  • Ctrl-F2 列出所有标签
  • Ctrl-Shift-l 指定当前文件的解析语言
  • Ctrl-Shift-u 指定当前文件的字符集
  • Ctrl-Alt-[ / Ctrl-Alt-] 折叠/展开代码
  • Ctrl-Alt-Shift-[ / Ctrl-Alt-Shift-] 全部折叠/展开
  • Ctrl-d 寻找下一个相同的串并多选 (很实用的列编辑功能)
  • Ctrl-鼠标左键 多选编辑
  • Ctrl-j 连接当前行和下一行
  • Ctrl-Shift-p 打开 command

Split

这是个内建功能, 而且没有快捷键. 可以通过点击 tab 进行快速分屏:

last-cursor-position

Alt-- / Alt-Shift-- 快速回到上/下次光标的位置.

goto-last-edit

Ctrl-i / Ctrl-Shift-i 快速回到上/下次编辑过的位置. 这个超级实用

找到配对的 ()[]{}

Ctrl-m 跳转到对应的 ()[]{}

Ctrl-Alt-m 选中匹配项中的所有数据

atom-beautify

Ctrl-Alt-b 帮你快速的整理代码.

atom-terminal

Ctrl-Alt-t 项目根目录下打开终端

Ctrl-Shift-t 当前文件根目录下打开终端 (目前版本这个快捷键有 bug, 只有打开过根目录终端后这个快捷键才响应)

pigments

帮你识别代码中的颜色

minimap

小地图功能也很实用…

file-icons

快速安装

‘’’
apm install last-cursor-position goto-last-edit atom-beautify atom-terminal pigments minimap file-icons
‘’’
map file-icons
‘’’
map file-icons
‘’’

Local System

File System

File I/O is provided by simple wrappers around standard POSIX functions. To use this module do require('fs'). All the methods have asynchronous and synchronous forms.

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');

// Asynchronous
fs.unlink('/tmp/hello', (err) => {
if (err) throw err;
console.log('successfully deleted /tmp/hello');
});

// Synchronous
fs.unlinkSync('/tmp/hello');
console.log('successfully deleted /tmp/hello');
Path

This module contains utilities for handling and transforming file paths. Use require(‘path’) to use this module.

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
var path = require('path');

// returns 'quux.html'
path.basename('/foo/bar/baz/asdf/quux.html');

// The platform-specific path delimiter, ; or ':'
console.log(path.delimiter);

// returns '/foo/bar/baz/asdf'
path.dirname('/foo/bar/baz/asdf/quux')

// returns '.html'
path.extname('index.html')
// returns '.md'
path.extname('index.coffee.md')
// returns '.'
path.extname('index.')
// returns ''
path.extname('index')
// returns ''
path.extname('.index')

// returns '/home/user/dir/file.txt'
path.format({
root : "/",
dir : "/home/user/dir",
base : "file.txt",
ext : ".txt",
name : "file"
});

// returns '/foo/bar/baz/asdf'
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..')

// returns '/foo/bar/baz/asdf'
path.normalize('/foo/bar//baz/asdf/quux/..')

// returns
// {
// root : "/",
// dir : "/home/user/dir",
// base : "file.txt",
// ext : ".txt",
// name : "file"
// }
path.parse('/home/user/dir/file.txt')
Child Process

The child_process module provides the ability to spawn child processes in a manner that is similar, but not identical, to popen(3).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const spawn = require('child_process').spawn;
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});

ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
OS

Provides a few basic operating-system related utility functions.

Network

HTTP

The HTTP interfaces in Node.js are designed to support many features of the protocol which have been traditionally difficult to use. To use the HTTP server and client one must require('http').

URL

This module has utilities for URL resolution and parsing. Call require(‘url’) to use it.

Example: ‘http://user:pass@host.com:8080/p/a/t/h?query=string#hash'

  • href: ‘http://user:pass@host.com:8080/p/a/t/h?query=string#hash'
  • protocol: ‘http:’
  • slashes: true or false (The protocol requires slashes after the colon)
  • host: ‘host.com:8080’
  • auth: ‘user:pass’
  • hostname: ‘host.com’
  • port: ‘8080’
  • pathname: ‘/p/a/t/h’
  • search: ‘?query=string’
  • path: ‘/p/a/t/h?query=string’
  • query: ‘query=string’ or {‘query’:’string’}
  • hash: ‘#hash’
1
2
3
url.resolve('/one/two/three', 'four')         // '/one/two/four'
url.resolve('http://example.com/', '/one') // 'http://example.com/one'
url.resolve('http://example.com/one', '/two') // 'http://example.com/two'

Global Objects

These objects are available in all modules.

Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

1
2
3
4
5
6
7
8
const output = fs.createWriteStream('./stdout.log');
const errorOutput = fs.createWriteStream('./stderr.log');
// custom simple logger
const logger = new Console(output, errorOutput);
// use it like console
var count = 5;
logger.log('count: %d', count);
// in stdout.log: count 5
Exports & Modules

TODO: …

Process

The process object is a global object and can be accessed from anywhere. It is an instance of EventEmitter.

Timers

TODO: …

Misc.

ArrayBuffer

The ArrayBuffer is a data type that is used to represent a generic, fixed-length binary data buffer.

1
2
3
4
5
var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
for (var i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}
Events

Much of the Node.js core API is built around an idiomatic asynchronous event-driven architecture in which certain kinds of objects (called “emitters”) periodically emit named events that cause Function objects (“listeners”) to be called.

1
2
3
4
5
6
7
8
9
const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
myEmitter.emit('event');