- 💬 Ask me about ... https://github.com/MengZhaoFly/blog/issues
blog's Introduction
blog's People
Forkers
zzxgl shunwuyu guoyihan 1017704538 itwangcode lushan-hao zhbosir variinlkt xxswkllxy gfromyh wuhaodemo ybfj onetwe acaciaj positionc lideqiang2019 keynesking forevercpublog's Issues
前端该知道的测试知识
测试分层
- 单元测试
- 集成测试
- 自动化 e2e 测试
单元测试
以 karma 为例
karma:一个管理测试工具的框架,组织各个工具的动作
Karma can be easily extended through plugins. In fact, all the existing preprocessors
, reporters
, browser launchers
and frameworks
are also plugins
karma 配置:
files:detail 配置哪些文件会被加载到浏览器中;
files: [
'src/**/*.js',
'test/**/*.js'
],
preprocessors:detail 文件被加载到浏览器之前,做些事
preprocessors: {
'src/**/*.js': ['webpack', 'coverage'],
'test/**/*.spec.js': ['webpack']
},
frameworks:加载哪些插件
frameworks: ['jasmine']
Vue React 组件测试
e2e
end-to-end:即端对端测试,属于黑盒测试,通过编写测试用例,自动化模拟用户操作,确保组件间通信正常,程序流数据传递如预期。
Selenium 和 Nightwatch 配合
通过 chromedriver 启动 chrome ,模拟用户操作。两者版本需要对应。历史版本下载
测试相关框架
- ui测试:
Macaca:面向多端的自动化测试
webdriver
uirecorder
nightwatchjs;
- react:
enzyme: Enzyme is a JavaScript Testing utility for React
安全漏洞检查
- xss
- sql
- csrf
- 敏感路径
参考文章
记一次简单的vue组件单元测试
了解 http2 / http3
HTTP 2协议分析
HTTP/2 没有改动 HTTP 的应用语义。 HTTP 方法、状态代码、URI 和标头字段等核心概念一如往常。
http2 特性
HTTP2的特点:
- 使用二进制格式传输,更高效、更紧凑。
- 对报头压缩,降低开销。
- 多路复用,一个网络连接实现并行请求。
- 服务器主动推送,减少请求的延迟
- 默认使用加密
- 服务器推送
- 服务端可以向客户端推送资源
二进制分帧层
- HTTP/2 所有性能增强的核心在于新的二进制分帧层,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。这里所谓的“层”指的是位于套接字接口与应用可见的高级 HTTP API 之间一个经过优化的新编码机制。
- HTTP/1.x 协议以换行符作为纯文本的分隔符,而HTTP/2 将所有传输的信息分割为更小的消息和帧,并采用二进制格式对它们编码。客户端和服务器会替我们完成必要的分帧工作。
多路复用
- 在 HTTP/1.x 中,如果客户端要想发起多个并行请求以提升性能,则必须使用多个 TCP 连接。这种模型也会导致队首阻塞,从而造成底层 TCP 连接的效率低下。将 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装是 HTTP
2。 最重要的一项增强。这个机制会在整个网络技术栈中引发一系列连锁反应,从而带来巨大的性能提升。
- 并行交错地发送多个请求,请求之间互不影响。
- 并行交错地发送多个响应,响应之间互不干扰。
- 使用一个连接并行发送多个请求和响应。
- 不必再为绕过 HTTP/1.x 限制而做很多工作
- 消除不必要的延迟和提高现有网络容量的利用率,从而减少页面加载时间。
服务器推送
HTTP/2 新增的另一个强大的新功能是,服务器可以对一个客户端请求发送多个响应。 换句话说,除了对最初请求的响应外,服务器还可以向客户端推送额外资源,而无需客户端明确地请求。HTTP/2 打破了严格的请求-响应语义,支持一对多和服务器发起的推送工作流服务器已经知道客户端下一步要请求什么资源,这时候服务器推送即可派上用场。
推送资源可以进行以下处理:
http2 伪头字段
http2 内置的几个特殊的以 “:” 开头的 key
- :method 目标URL模式部分(请求)
- :scheme 目标URL模式部分(请求)
- :authority 目标RUL认证部分(请求)
- :path 目标URL的路径和查询部分(绝对路径产生式和一个跟着"?"字符的查询产生式)。 (请求)
- :status 响应头中的HTTP状态码部分(响应)
- :status 响应头中的HTTP状态码部分(响应)
http3
运行在 QUIC 协议之上,QUIC 基于 UDP,看中 UDP 的效率速度,同时 QUIC 也集合了 TCP,TLS,http2 的 优点,并加以优化。
特点:
- 减少握手延迟
- 多路复用
- 连接迁移(客户端 WIFI 转 4G)
http3 将会是一个全新的协议,不会 http2 的扩展,和 http1 和 http1.1 没有直接联系。
linux-basic-command
文件系统图解
-
/etc: linux 主要配置文件, 类似 windows 注册表
etc配置目录 修改策略:先备份,再修改- hosts
- rc0.d 开机启动的,
- rpm 本地包 rum远程包
-
/usr: unix software resource (unix 操作系统软件资源)
-
/dev: 设备文件,设备文件不能直接访问
-
/bin: 放置可执行文件的地方,cat chmod mv cp...
-
/home: 用户目录,超级用户的 目录 是: /
-
/mnt:挂载目录,
-
/opt:放自己的程序,类似 windows 的
C:program filses/
-
/proc:进程相关的,实时的,关机以后就消失,虚拟路径
-
/tmp:临时目录
-
/var:日志 var/log
cd ~
:用户当前的目录
soft link hard link
soft link: 类似 快捷方式
lrwxrwxrwx. 1 root root 8 4月 21 2016 sbin -> usr/sbin
体系
平时说的 linux 说的就是 内核,常见的 centos,unbutun, ...(linux 发行版),
公用函数库:语言调用 操作系统 用到的方法
进程调用就在两个模式之间切换
ls -al
drwxr-xr-x 3 zhaomeng staff 96 5 5 23:39 WeChatProjects
- 第一列:
字母 d 目录
符号 - 普通文件
user权限 group权限 other 权限
- 第二列:
若是文件则代表该文件的硬链接数,若是目录,则代表该目录下的子目录数。
- 第三列:
当前文件所属的用户名
- 第四列:
当前文件所属的用户组
- 第五列:
若是文件则代表该文件的大小,若是目录则代表该目录的大小。
- 第六列:
该文件最近修改或者查看的时间
- 第七列:
文件名
万能的 man (manual) 以及 -h
不知道命令有什么用,查看命令的手册。
修改权限
选项 +o(user权限 group权限 other 权限 ) 代表需要给哪一个角色添加权限
chmod +ox foo.js
复制文件
cp
或者 cp -R
复制整个目录
cp 源文件 目标文件
选项:-p 保留原文件的修改时间
删除文件
rm -r
删除目录
移动文件
mv
:
mv 源文件 目标文件
文本内容查看
-
cat
: 文本内容显示到终端 -
head
: 查看文件开头
- [- 5] 前 5 行
tail
: 查看文件结尾
- [- 5] 结尾 5 行
- [-f] 当文件的内容更新的时候同步更新
wc
: 统计文件内容信息
打包 压缩
windows:.zip .rar
linux: 压缩分开的 第一步先打包 第二部压缩
tar 打包:
常用参数:
- c 打包
- x 解包
- f 指定操作类型为文件
打包/压缩:
tar cf backupjs.tar ./js
: 把 js 目录打包
tar 命令比较强大 也集合了 压缩功能:
tar czf backupjs.tar.gz ./js
: 把 js 目录 打包 并且以 zip 压缩,采用双文件名的形式便于表达该文件的格式。
tar cjf backupjs.tar.bz2 ./js
: 把 js 目录 打包,并且以 bzip2 压缩。
解包:
tar zxf backupjs.tar.gz -C backup
: -C 指定了 解包之后文件需要存在哪个位置。
tar xjf backupjs.tar.bz2 -C backup
: bzip2 同理
gzip bzip2 压缩:
vi
vim 是vi的升级版本。
四种模式:
- 正常模式 vim main.js
- 插入模式(i,o,a 进入插入模式后,光标位置有所区别)
- 命令模式 (ESC 返回正常模式)
- 可视模式
用户权限管理
linux 和 windows 多用户系统
- useradd 新建用户
- userdel 删除用户
- passwd 修改用户密码
- usermod 修改用户属性
- chage 修改用户属性
添加用户
- 添加用户
useradd lmstudent
- 查看用户
id lmstudent
- 用户自己 的目录
cd home/lmstudent/
- 更改用户的密码
passwd lmstudent
即可使用 新的用户登录。
创建链接
ln:
硬链接:同一个文件
软链接:类似快捷方式,修改任意⼀个⽂件,另⼀个都会改变
-s 创建软链接
ln -s [源⽂件] [⽬标⽂件]
⽂件搜索命令
locate
- 只能搜索⽂件名
- 在后台数据库中按⽂件名搜索,速度⽐较快
- 数据保存在 /var/lib/mlocate 后台数据库,每天更新⼀次, 可以 updatedb 命令⽴刻更新数据库
whereis
- 搜索命令所在路径以及帮助⽂档所在位置
- whereis 命令名
- -b 只查找可执⾏⽂件
- -m 只查找帮助⽂件
whereis nginx
which
- 可以看到别名 which ls
- 能看到的都是外部安装的命令
- ⽆法查看Shell⾃带的命令,如 which cd
echo $PATH
环境变量
- 定义的是系统搜索命令的路径
/usr/local/nginx/sbin:/downloads/node-src/node-v8.9.3-linux-x64/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
find
- ⽂件搜索命令
- find [搜索范围] [搜索条件]
按名称搜索
避免⼤范围的搜索,会⾮常消耗系统资源
find / -name softlink.js
通配符
-
- 匹配任意内容
- ? 匹配任意⼀个字符
- [] 匹配任意⼀个中括号内的字符
find / -name "*.js"
忽略大小写
-i
find / -iname A.log
按所有者进⾏搜索
-user
按时间搜索
- atime ⽂件访问时间
- ctime 改变⽂件属性
- mtime 修改⽂件内容
按⼤⼩搜索
-size
综合应⽤
find /tmp -size +10k -a -size -20k
-a and 逻辑与,两个条件都满⾜
-o or 逻辑或,两个条件满⾜⼀个就可以
find /tmp -size +10k -a -size -20k -exec ls -lh
exec 对上个命令的结果进⾏操作
grep
在⽂件当中匹配符合条件的字符串
grep "10" access.log
-i 忽略⼤⼩写
-v 排除指定字符串
grep var ./learn-linux/softlink.js
关机和重启命令
shutdown
shutdown 关机命令
init
关机 init 0
重启 init 6
系统的运⾏级别
0 关机
1 单⽤户
2 不完全多⽤户,不包含NFS服务
3 完全多⽤户
4 未分配
5 图形界⾯
6 重启
logout
退出登录
查看登录⽤户信息
w
查看登录⽤户信息
USER 登录的⽤户名
TTY 登录的终端 tty1 本地终端 pts/0远程终端
FROM 登录的IP
LOGIN 登录时间
IDLE ⽤户闲置时间
JCPU 该终端所有进程占⽤的时间
PCPU 当前进程所占⽤的时间
WHAT 正在执⾏的命令
who
查看登录⽤户信息
USER 登录的⽤户名
TTY 登录的终端 tty1 本地终端 pts/0远程终端
LOGIN 登录时间(登录的IP)
last
查看当前登录和过去登录的⽤户信息 默认读取 /var/log/wtmp ⽂件
⽤户名
登录终端
登录IP
登录时间
退出时间(在线时间)
软件包管理器
包管理器是方便软件安装,卸载,解决软件依赖关系的重要工具
- centos redhat 使用 yum 包管理器,格式 rpm
- ubuntu 使用 apt 包管理器,格式为 deb
源代码编译安装
- wget 下载
- 解包
- ./configure
- make 编译
- make install 安装
练习使用:http://openresty.org/cn/installation.html
about fe
网络端口
门:一个门提供一个服务,一个进程。
netstat -anp | grep 80 可以拿到 pid,linux 下是父子进程,杀掉子进程,可能又回由父进程唤起,
ipv4:
[root@VM_0_3_centos ~]# netstat -anp | grep 80
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 2358/nginx: master
ipv6
tcp6 0 0 :::3000 :::* LISTEN 16873/node /root/pr
tcp6 0 0 :::8888 :::* LISTEN 30148/node /root/pr
systemctl [start | stop | restart | status] service_name
kill: 向进程发送一个 停止信号 kill -9 pid
what?
静态库,动态库
WEB通讯技术 - 短轮询、长轮询(comet)、长连接(SSE)、WebSocket
短轮询
浏览器每隔一段时间向浏览器发送http请求
comet
指的是,当服务器收到客户端发来的请求后,不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制(服务器端设置)后关闭连接。
长轮询和短轮询比起来,明显减少了很多不必要的http请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费
SSE
SSE是HTML5新增的功能,全称为Server-Sent Events。它可以允许服务推送数据到客户端。SSE在本质上就与之前的长轮询、短轮询不同,虽然都是基于http协议的,但是轮询需要客户端先发送请求。而SSE最大的特点就是不需要客户端发送请求,可以实现只要服务器端数据有更新,就可以马上发送到客户端。
SSE的优势很明显,它不需要建立或保持大量的客户端发往服务器端的请求,节约了很多资源,提升应用性能。并且后面会介绍道,SSE的实现非常简单,并且不需要依赖其他插件.
SSE是使用HTTP传输的,这意味着我们不需要一个特殊的协议或者额外的实现就可以使用.
一个客户端去从服务器端订阅一条“流”,之后服务端可以发送消息给客户端直到服务端或者客户端关闭该“流”, 短轮训,长轮训之间的对比:
详见:
WebSocket
WebSocket是Html5定义的一个新协议,与传统的http协议不同,该协议可以实现服务器与客户端之间全双工通信。简单来说,首先需要在客户端和服务器端建立起一个连接,这部分需要http。连接一旦建立,客户端和服务器端就处于平等的地位,可以相互发送数据,不存在请求和响应的区别。
响应式编程
RxJs
nodeJS
异步
-
libuv
Libuv是一个高性能的,事件驱动的异步I/O库,它本身是由C语言编写的,具有很高的可移植性。libuv封装了不同平台底层对于异步IO模型的实现,所以它还本身具备着Windows, Linux都可使用的跨平台能力。
-
event loop
sync -> microtask(tick, promise) -> macrotask(timeout, immediate) -
sleep
settimeOut 模拟假休眠
内存
- Node使用JavaScript在服务端操作大内存对象受到了一定的限制。(堆区),64位系统下约为1.4GB,(栈区)32位操作系统下是0.7G.
- 新生代64位是32M 32位是16M
新生代:新申明的变量,生命周期比较短
老生代:已经被 GC 过一次的变量
内存分析
node-heapdump
v8_writeheapsnapshot
性能监控
异常处理
业务错误:try catch
全局兜底:process
日志处理
架构演进
MVC
node 中间层
中台(技术中台,业务中台)
多进程
node属于 单进程单线程。
serverLess / FASS
基础知识加强版
函数申明提升
if (false) {
if (false) {
function foo() {
console.log(123)
}
}
}
console.log(foo); // undefined
函数被提升到作用域最顶端,虽然为 false 但是依然将 foo 变量提升。
看输出
// 1.请写出如下代码输出值,并解释为什么。
console.log(a); // undefined
console.log(typeof foo(a)); // 报错
var flag = true;
if (!flag) {
var a = 1;
}
if (flag) {
function foo(a) {
foo = a;
console.log("foo1");
}
} else {
function foo(a) {
foo = a;
console.log("foo2");
}
}
//js代码执行前引擎会先进行预编译,
//预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端。
//函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖
变量输出 this
function fn() {
console.log(this.length);
}
var bar = {
length: 5,
method: function () {
"use strict";
fn(); // 指向 window window.length 代表 iframe 的长度 0
console.log(arguments);
arguments[0]() // 指向 arguments 传了两个参数 2
}
}
const result = bar.method.bind(null);
result(fn, 1);
// bar.method(fn, 2, 3);
加强版:
function foo(a, b, c) {
console.log(this.length); // this 指向 arguments:4
console.log(this.callee.length);
// arguments.callee 属性包含当前正在执行的函数。指向 fn:1
}
function fn(d) {
/*
0: ƒ foo(a, b, c)
1: 10
2: 20
3: 30
callee: ƒ fn(d)
length: 4
*/
arguments[0](10, 20, 30, 40, 50);
}
fn(foo, 10, 20, 30);
请问变量a会被GC回收么,为什么呢?
function test(){
var a = "foo-test";
return function(){
eval(""); //
//eval 词法语境分析的是否无法预知上下文,因此无法做优化
// 包含当前所有上下文信息
}
}
test()();
foo-test, 存在 distance(到根结点的路径,依然可以访问到)没有回收。
再来一题
function Foo(name) {
this.name = name;
}
var wang = new Foo('wang');
var li = new Foo('li');
setTimeout(function () {
wang = null;
}, 0);
只有 li
存在 wang 已经被回收。
假如是闭包呢?
function Bar(name) {
this.name = name;
}
function makeBarFactory(name) {
var p = new Bar(name);
return function () {
return p;
}
}
var p1 = makeBarFactory('zhang');
p1();
console.log(p1)
p1 = null;
Bar distance 为 - 表示 为一个野指针,没有任何东西能访问到。存在内存中无法回收。
闭包正确操作
function makeBarFactory(name) {
var bar1 = new Bar(name);
return {
create: function () {
return bar1;
},
destroy: function() {
bar1 = null;
}
}
}
var p1 = makeBarFactory('zhang');
p1.create();
console.log(p1);
// p1 = null;
p1.destroy();
原型链输出
Object.prototype.a = 'a';
Function.prototype.a = 'a1';
function Person() { };
var foo = new Person();
console.log(Person.a); // 'a1'
console.log(foo.a); // a
console.log(1..a); // 'a' // 1. a 转为一个对象
// console.log(1.a); // error
console.log(foo.__proto__.__proto__.constructor.constructor.constructor);
// foo.__proto__ 对象
// 对象.__proto__.constructor Function
// 之后也是 Function
输出
- {
var a = 1;
const b = 2;
function test(){}
test = 3;
console.log(typeof test) // number
}
console.log(a) // 1
console.log(typeof test) // global : function
console.log(b) // error
ES6元编程
- Proxy 不存在节点的时候创建一个 节点
function Tree() {
return new Proxy({}, handler);
}
const handler = {
get(target, key, receiver) {
if (!(key in target)) {
//自动创建一个树
target[key] = Tree(); // 继续 Proxy 处理
}
return Reflect.get(target, key, receiver);
}
}
let tree = Tree();
tree.yideng.student.a = "小牛🐂";
console.log(tree);
- Symbol iterator
const arr = [4, 5, 6, 7, 8, 9];
//定义一个尽在奇数索引地方生产值
arr[Symbol.iterator] = function* () {
let idx = 1;
console.table(this);
do {
yield this[idx];
} while ((idx += 2) < this.length)
};
for (const v of arr) {
console.log('v', v);
}
- 异步的队列
错误代码
// 异步调用转顺序
const callback = () => { console.log('callback') };
const asyncFunc = (cb) => {
setTimeout(() => {
cb();
console.log(Date.now())
}, 1000)
}
// asyncFunc(callback);
// asyncFunc(callback);
// asyncFunc(callback);
如何转换成异步的队列?
function createAsyncQueueProxy(asyncFunc) {
let promise = null;
return new Proxy(asyncFunc, {
apply(target, context, [cb, ...args]) {
promise = Promise.resolve(promise).then(() => {
return new Promise(resolve => {
Reflect.apply(target, this, [() => {
resolve();
cb();
}, ...args])
})
})
}
})
}
// const asyncFuncProxy = createAsyncQueueProxy(asyncFunc);
// asyncFuncProxy(callback);
// asyncFuncProxy(callback);
// asyncFuncProxy(callback);
babel编译后的async原理
let a = 0;
let foo = async () => {
// a = a + await 10;
// console.log(a)
// foo 先运行 这个时候记录下来 a = 0, 0 + 10 得到 10
let b = await 10;
a = a + b;
console.log(a); // 11
// a 后来才记录的 a 已经增加了
}
foo();
console.log(++a); // 1
加强版本
1 2 4 3
async function async1(){
console.log(1)
await async2();
console.log(3)
}
async function async2(){
console.log(2)
}
async1();
console.log(4)
会输出吗,为啥
document.getElementById('test').click(function (argument) {
console.log(1);
});
setTimeout(function () {
console.log(2);
}, 0);
while (true) {
console.log(Math.random());
}
不会,单线程,所有的资源都被,while 占据了,可采用时间片轮转。
JS 一些手写 Polyfill
深拷贝
function deepCopy(obj) {
var result = Array.isArray(obj) ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
result[key] = deepCopy(obj[key]); //递归复制
} else {
result[key] = obj[key];
}
}
}
return result;
}
考虑到 buffer
function cloneBuffer(buf) {
var len = buf.length,
result = Buffer.allocUnsafe(len);
buf.copy(result);
return result;
}
详解 MVC
MVC
一个架构模式,系统有若干个模块组成
模块:
- 业务
- 技术
view
视图,展示数据
controller
控制器,流程控制。
用户提交过来的数据,怎么到数据库。
数据库查出来的数据,怎么交给页面。
model
核心,解决程序的数据
你的页面为什么慢,Performance Timeline 简介
介绍
Performance timeline ,w3c 有两个版本的规范,本文基于 第二版本 介绍。
工欲善其事,必先利其器。要想使页面更快,那么准确测量出性能数据也是很重要的。
那么我们来看一下,在一个web页面的生命周期之内,我们基于 Performance Timeline
可以得到哪些性能指标。
主要分为以下三大类:
- navigation-timing:navigation of the document
- resource-timing:页面资源
- user-timing:开发者自定义的一些监控, 主要是(mark 和 measure,下文会讲)
举个🌰, w3c 上的例子,我稍加改造 :
<img id="image0" src="https://www.w3.org/Icons/w3c_main.png" />
<script>
function init() {
// see [[USER-TIMING-2]]
performance.mark("startWork");
setTimeout(() => {
performance.mark("endWork");
measurePerf();
}, 2000);
}
function measurePerf() {
performance
.getEntries()
.map(entry => JSON.stringify(entry, null, 2))
.forEach(json => console.log(json));
}
引入了一个 外部图片,
mark
可以理解为标记了一个时间点
getEntries
得到所有的性能数据,最后输出。
具体结果可看:
{
"name": "",
"entryType": "navigation",
"startTime": 0,
"duration": 50.07500003557652,
}
{
"name": "https://www.w3.org/Icons/w3c_main.png",
"entryType": "resource",
}
{
"name": "startWork",
"entryType": "mark",
"startTime": 49.990000028628856,
"duration": 0
}
{
"name": "first-paint",
"entryType": "paint",
"startTime": 94.83499999623746,
"duration": 0
}
{
"name": "first-contentful-paint",
"entryType": "paint",
"startTime": 94.83499999623746,
"duration": 0
}
{
"name": "endWork",
"entryType": "mark",
"startTime": 2050.5150000099093,
"duration": 0
}
由此我就得到了页面上,当时的所有性能指标,包括navigation
, resource
, FP
, FCP
...等一些自定义的指标。
ps:
FP
:页面上第一个像素落点的时候FCP
: 页面上开始有内容绘制的时候
现在我想要过滤一下只要我自己 mark 的点,可采用:getEntriesByType(mark)
,
只想要页面绘制相关的 fp,fcp,采用 getEntriesByName('first-paint')
。
but,我们得到的就只是当时得到的性能指标,假如后面又有 图片请求了呢,又有 js 请求了呢 🤣🤣。
我们要一直 轮询我们的 measurePerf
吗?
当然有新的解决方式。
PerformanceObserver
PerformanceObserver,是浏览器内部对Performance实现的观察者模式,即: 当有性能数据产生时,主动通知你。
这解决了我们之前的问题:
- 重复轮训
- 轮巡时不断判断,这个数据是新产生的,还是以前的
- 可能其他数据的消费者也需要操作数据
监测页面FP,FCP
现在可以:
// 定义一个观察者
const observer = new PerformanceObserver(list => {
list.getEntries().forEach((entry) => {
console.log('entry对象', entry);
});
});
// 观察的类型
observer.observe({
entryTypes: ['paint']
});
关于 entryTypes
, 可以取如下值:
- frame:event-loop 时的每一帧
- navigation:导航
- resource:资源
- mark: 打点,得到一个时间戳
- measure:在两个点之间测量
- paint:绘制
- longtask(好像只有 chrome支持):任何在浏览器中执行超过 50 ms 的任务,都是 long task
关于 entry
:
每个事件类型的 entry 对象都不一样。
navigation
entry 对象里能拿到相关的数据有
这里完整地描述了一个 页面
呈现的完整流程。
拿到每个时间点可以进行分析每个区间的时间耗费。
let t = entry
console.log('DNS查询耗时 :' + (t.domainLookupEnd - t.domainLookupStart).toFixed(0))
console.log('TCP链接耗时 :' + (t.connectEnd - t.connectStart).toFixed(0))
console.log('request请求耗时 :' + (t.responseEnd - t.responseStart).toFixed(0))
console.log('解析dom树耗时 :' + (t.domComplete - t.domInteractive).toFixed(0))
console.log('白屏时间 :' + (t.responseStart - t.startTime).toFixed(0))
console.log('domready时间 :' + (t.domContentLoadedEventEnd - t.navigationStart).toFixed(0))
console.log('onload时间 :' + (t.loadEventEnd - t.navigationStart).toFixed(0))
ps
关于 dom
几个重要的时间点:
domInteractive :
网页DOM结构结束解析、开始加载内嵌资源时,即Document.readyState属性变为“interactive”。domComplete
:当前文档解析完成,即Document.readyState 变为 'complete'。domContentLoadedEventStart
: 触发当前 文档的domContentLoadedEvent
的时候domContentLoadedEventEnd
: 在 当前文档的domContentLoadedEvent
完成之后发生document.redayState
: 的状态,- loading / 正在加载 document 仍在加载。
- interactive / 可交互 文档已被解析,"正在加载"状态结束,但是诸如图像,样式表和框架之类的子资源仍在加载。
- complete / 完成 文档和所有子资源已完成加载。表示 load 状态的事件即将被触发
其他:
window.onload:
default, it is fired when the entire page loads , including its content (images, CSS, scripts, etc.) In some browsers it now takes over the role of document.onload and fires when the DOM is ready as well.
resource
entry 对象里能拿到相关的数据有
这里道理和上面的 navigation
一样。
mark
这里打了四个点,用来标识两个任务的开始时间和结束时间,两个任务分别采用 Promise
推迟执行。doTask
模拟推迟行为。
const doTask = (ms) => new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, ms);
})
async function run() {
performance.mark("startTask1");
await doTask(1000); // Some developer code
performance.mark("endTask1");
performance.mark("startTask2");
await doTask(2000); // Some developer code
performance.mark("endTask2");
// Log them out
const entries = performance.getEntriesByType("mark");
for (const entry of entries) {
console.table(entry.toJSON());
}
}
run();
分别得到四个点的时间。
measure
用于在两个 **点 (即上文提到的打点) ** 之间测量时间
举个 例子:需要测量一个 for
循环花费的时间。
const observer = new PerformanceObserver(list => {
list.getEntries().forEach((entry) => {
console.table(entry);
});
});
observer.observe({
entryTypes: ['measure']
});
performance.mark('cal-start');
// 模拟耗时任务
for(let i = 0; i < 10000; i ++) {
}
performance.mark('cal-end');
// 该 measure 的名字:my-cal
// measure 开始的时间点:cal-start
// measure 结束的时间点:cal-end
performance.measure('my-cal', 'cal-start', 'cal-end')
longtask
这个 支持度 好像不高,chrome 亲测可以。
可以检测 应用里 任何在浏览器中执行超过 50 ms 的任务。
原因来源还比较多:
- 浏览器的render
- 自身的js执行
比如上面得 实例, 我们仅需加入一个需要的检测 的 entryType
既可。
observer.observe({
entryTypes: ['measure', 'longtask']
});
可以看到,Performance timeline 第二版本,相对以前还是有很大改进的,我们也有了更多的手段得到想要监控的数据。
EOF。
最后
如果喜欢本篇文章,可以关注的微信公众号,如果不嫌烦,还可以把它添加到桌面😀。
脚手架
npm link
把当前这个 命令 链接到 命令的全局环境
一些比较好的 npm 包
commander: 命令行参数的整理
chalk:色彩
semver: 版本规范
inquirer: 命令行交互
ora: loading 图
handlebars: 一个模板,根据用户的输入组装代码
metalsmith: 文件生成,提供了插件的机制,在生成的时候,可以自定义一些事,比如
- 获取用户的输入 存放metalData 2. 使用 handlebars 生成,
package.json
engines: node 版本要求
webpack 多页应用
step
服务端渲染多页应用:
思路从传统的:
router 对应 不同的 页面或者不同模板文件
模板编写的时候,得考虑公共组件吧?,比如 swig 模板,继承用上
模板处理好了 静态资源怎么插入到页面里面去?
webpack 多页打包得用上了,
webpack 打包只认 js 入口,
只能为不同的 页面 添加不同的 入口 js
webpack 打包出了不同页面的静态资源,怎么插入到页面中
htmlwebpackplugin 可以插入资源
自定义插件doc
compiler 这个对象在启动 webpack 时被一次性建立,是一个完整的 webpack 环境配置
compilation 对象代表了一次资源版本构建
webpack 采用 tapable 这个库管理生命周期
step2
js 通过 webpack 打包完成,但是 每个页面只需要引用自己的 入口的js 就 ok,
这就需要在js 插入页面的时候 ,根据 js 的 名字 和 页面名字 进行 过滤,
css 同理。
package.json 里面 命令太多
scripty 用上
工程化 webpack 区分 dev - prod
css 压缩 : optimize-css-asserts-plugin
postCss: css 届的 babel,css 处理,也有丰富的插件
html 压缩:html-minifier
打包结果检查
step3
模板 css js 打包成静态资源插入,
node 代码 需要和 静态资源 合并在一个目录管理,
gulp 任务流管理:
如果考虑 @符号 还可以使用 babel
node 环境也需要 需要 区分dev 和 prod
在 config 里面 根据不同的环境区分配置
但是 既然区分了 更定有些 代码在另外一个环境里面就永远不会运行到,
gulp 里面也可以用上 gulp
weboack 每一个打包结果都是一个闭包,scope-hosti 合并闭包
编码及其 Buffer 基础知识
编码
计算机世界只有 0 和 1,文字,图片,视屏或者程序本身都不例外,
互联网由计算机节点构成,我们在聊天,本质上是交换数据,
交换的数据又由 0 和 1 构成,无论是英文字母,还是中文汉字,
都对应一段 0 和 1 这样一组二进制数据。
这样的二进制,需要一种规则来把它对应到正确的字符,
这样的规则就是编码。
utf-8 编码字符串
发送
接收方 再用 utf-8 解码 这样就能拿到正确的字符串。
常用编码
- binary:二进制
- hex:16进制
- ASCII:基于拉丁字母的编码,用 8 个 0 1 二进制表示,前一个 bit 为 0,后面 7个 定义了 128 个字符,比如 字母 A 它的二进制 0100 0001,十进制是 65。
- Unicode: 国际码,是计算机字符业界标准,为了兼容全世界的语言文字字符,这些字符都能找到一个对应的 Unicode 编码。
- utf-8: 是 Unicode 标准的一种实现而已,它可以表示 Unicode 标准的任何字符。
- base64:基于 64 个 可打印字符来表示二进制数据的一种编码格式
binary / hex / ascii / utf8 / base64,也是 node 所支持的编码格式,utf8是默认编码,
而类似 GBK/GBK2312 等编码 node 无法解析。
字节和字符
Byte,字节,是一种对数据大小的衡量单位,1GB = 1024MB = 1024 * 1024 KB = 1024 * 1024 * 1024B 即 10亿字节
一字节 = 8bit 也就是 8位。
8位 是一个 长度为8的二进制,比如 1001 1001,
能表示的 范围就是 0 - 255 也就是 2 的 8次方。
所以 ASCII 实际上 可以容纳 26 个字符。
我们知道一个英文字母占 1 个字节(8位二进制),而一个汉字 占据 2 个字节,
而 字母或者汉字 其实就是字符。
二进制数据搬运
了解进制,编码,字节,字符后,
我们知道数据无非是 0 1,
配合编码就能传输和解析我们的文字,文件。
打开一个网页很快,看电影很慢,无非就是二进制数据的搬运效率,编码解码等问题。
假设一下,有一大堆沙子需要搬走(100GB),想要一次性搬走,先不说电脑带宽,光服务器内存就不行,所以的有一种机制先把沙子拿出来放到管子里面像水一样流到另外一边,这就是 Stream 和 pipe。既然能流动每次搬运数据接受收据,数据都存在哪里呢?为了保证速度应该是放到内存里面来吧,那它长什么样子呢,接下来我们了解 Buffer
Buffer 基础
从 A 往 B 搬运东西,A 不断提供数据,B 处理数据。
两头效率不一致,要么 A 等待 B,要么 B 等待 A。
一定有一部分数据无处安放,那么就需要一个地方来暂时存储这个数据,缓冲一下传输速度,
这个地方就是内存,这个专门开辟出来的区域就是 Buffer。
{ [Function: Buffer]
// 分配缓冲区内存容量
poolSize: 8192,
// 根据传入的内容创建 Buffer
from: [Function: from],
of: [Function: of],
// 正常创建 Buffer
alloc: [Function: alloc],
// 两种不安全的创建 Buffer 的方法
allocUnsafe: [Function: allocUnsafe],
allocUnsafeSlow: [Function: allocUnsafeSlow],
// 判断
isBuffer: [Function: isBuffer],
// 比较两个 Buffer 对象 的相对位置
compare: [Function: compare],
// 判断 Node 是否支持某种编码
isEncoding: [Function: isEncoding],
// 拼接几个 Buffer
concat: [Function: concat],
// 跟进特定编码统计 buffer 字节数
byteLength: [Function: byteLength],
[Symbol(kIsEncodingSymbol)]: [Function: isEncoding] }
from
字符串创建 buffer
const bufFromStr = Buffer.from('Hello world')
// <Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64>
里面就是填充的二进制数据
就是 ASCII 码表的二进制,8个一组,只不过这里是以 16 进制显示方便理解。
- 48 H 0100 1000
- 65 e 0110 0101
buffer 数组创建 buffer
const bufFromBuf = Buffer.from([0x48, 0x65]);
转为 字符串
bufFromBuf.toString()
alloc
传递一个 size, 以字节为单位, 传参给 alloc 生成一段内存区间,
Buffer.alloc 返回指定大小的 buffer 实例
缓冲 写入 Buffer
向已经创建的 buffer 中加入新的字符串,
let bufForWrite = Buffer.alloc(32)
bufForWrite.write('1234567890', 0, 9)
console.log(bufForWrite.toString())
// 123456789
截取
buf.slice([start[, end]])
let bufFromArr1 = Buffer.from([1, 2, 3, 4, 5])
// <Buffer 01 02 03 04 05>
let bufFromArr2 = bufFromArr1.slice(2, 4)
// <Buffer 03 04>
复制
buf.copy(target[, targetStart[, sourceStart[, sourceEnd]]])
let bufCopy1 = Buffer.from('Hello')
let bufCopy2 = Buffer.alloc(4)
console.log(bufCopy1)
// <Buffer 48 65 6c 6c 6f>
bufCopy1.copy(bufCopy2, 0, 1, 5)
console.log(bufCopy2)
<Buffer 65 6c 6c 6f>
console.log(bufCopy2.toString())
// ello
填充
buf.fill(value[, offset[, end]][, encoding])
const bufForFill = Buffer.alloc(12).fill('11-11 ')
// <Buffer 31 31 2d 31 31 20 31 31 2d 31 31 20>
console.log(bufForFill.toString())
// 11-11 11-11
总结认知
在内存里面申请和存储一段数据,好处是先把数据缓存起来,用的时候开箱即用,减少 IO 层面的开销,缓冲积压,来为流的读写提供一个中间地带。
example
const fs = require('fs')
// 通过 fs.readFile 读取图片时候,拿到的是缓冲的 Buffer 数据
fs.readFile('img.png', (err, buffer) => {
console.log(Buffer.isBuffer(buffer) && 'readFile 读取图片拿到的是 Buffer 数据')
// 把读取到的 Buffer 数据,通过 fs writeFile 写入到一个新图片文件中
fs.writeFile('logo.png', buffer, function(err) {})
// 再基于原始的 Buffer 创建一个新的 Buffer,通过 toString base64 解码为字符串打印出来
const base64Image = Buffer.from(buffer).toString('base64')
// console.log(base64Image)
// base64Image 是 base64 后的字符串,传参给 from,同时指定编码生成一个新的 Buffer 实例
const decodedImage = Buffer.from(base64Image, 'base64')
// 比较两个 Buffer 实例的数据,并写入到一个新的图片中
console.log(Buffer.compare(buffer, decodedImage))
// 返回 0 代表相同
fs.writeFile('img_decoded.jpg', decodedImage, (err) => {})
})
js 模块化
CommonJS
exports === module.exports // true
AMD
AMD = Asynchronous Module Definition
代表:RequireJS
RequireJS会先尽早地执行(依赖)模块, 相当于所有的require都被提前了。
定义 mod1:
define(function() {
console.log('require module: mod1')
return {
hello: () => {
console.log('hellow mod1');
}
}
})
定义 mod2:
define(function() {
console.log('require module: mod2');
return {
hello: () => {
console.log('hellow mod2');
}
}
})
集合调用:
define(function(require, exports, module) {
console.log('require module: main');
var mod1 = require('./mod1');
mod1.hello();
var mod2 = require('./mod2');
mod2.hello();
return {
hello: function() {
console.log('hello main');
}
};
});
主入口文件引入:
requirejs.config({
baseUrl: '',
paths: {
print: './print',
util: './util',
custommodule: './module'
}
});
require(['custommodule'], (custommodule) => {
})
整个执行结果:更好地解释了 把所有模块提前。
require module: mod2
require module: mod1
require module: main
hellow mod1
hellow mod2
CMD
CMD = Common Module Definition
代表:SeaJS
上面的代码在 CMD 环境中运行完的结果。比较符合我们的预期。
<script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>
<script>
// seajs 的简单配置
seajs.config({
base: "",
alias: {
}
})
// 加载入口模块
seajs.use('./module.js')
</script>
require module: main
require module: mod1
hellow mod1
require module: mod2
hellow mod2
AMD CMD 区别
AMD
中只要模块作为依赖时,就会加载并初始化。
CMD
中,模块作为依赖且被引用时才会初始化,否则只会加载。
UMD
UMD = Universal Module Definition,即通用模块定义。UMD 是AMD 和 CommonJS的糅合。
UMD 先判断是否支持 Node.js 的模块(exports)是否存在,存在则使用 Node.js 模块模式。再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块。
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
//module ...
});
CommonJS
导出模块:
var counter = 3;
var obj = {
name: 'David'
};
function changeValue() {
counter++;
obj.name = 'Peter';
};
module.exports = {
counter: counter,
obj: obj,
changeValue: changeValue,
};
导入模块,这个时候 counter 没变,对象变了
CommonJs 导入类似 类比于基本类型和引用类型的赋值操作
var { counter, obj, changeValue} = require('./module.js');
console.log(counter); // 3
console.log(obj.name); // 'David'
changeValue();
console.log(counter); // 3
console.log(obj.name); // 'Peter'
// Or
console.log(require('./module.js').counter); // 3
console.log(require('./module.js').obj.name); // 'Peter'
Es Module
导出模块
export var counter = 3;
export var obj = {
name: 'David'
};
export function changeValue() {
counter++;
obj.name = 'Peter';
};
导入模块
import { counter, obj, changeValue } from './module.js';
console.log(counter); // 3
console.log(obj.name); // 'David'
changeValue();
console.log(counter); // 4
console.log(obj.name); // 'Peter'
发现 counter 和 对象都变了,
Es Module 输出的是值得引用。原始值变了,import 加载的值也会跟着变
you don't know html
引入跨域资源的标签 属性 crossorigin
一个协商跨域的值:MDN-attr-crossorigin
- anonymous
如果使用这个值的话就会在请求中的header中的带上Origin属性。
for example:
标签 image 如果服务端未设置Access-Control-Allow-Origin HTTP header, 图片将会 error.
标签 script 中,如果没有设置 CORS, 跨域 js 发生错误,将会向 window.error,提供很少的信息,通过 crossorigin 就能得到详细的信息。
<script src="http://localhost:9999/test.js" crossorigin="anonymous"></script>
- use-credentials
请求的时候将会带上:cookie, certificate, or HTTP Basic authentication。
localStorage
同一个域名document.domain
共享同一个localStorage, localStorage 存在溢出问题。
解决方式:可通过 iframe + postMessage 扩容
for example: set
function setAnotherOriginLocalStorage(key, value) {
iframe.postMessage({key, value, operation: 'set'}, 'http://localhost:9999');
}
setAnotherOriginLocalStorage('key', {s: 'string'})
for example: get
getAnotherOriginLocalStorage('key', (err, res) => {
console.log('get key:', res);
});
function getAnotherOriginLocalStorage(key, cb) {
window.onmessage = (e) => {
console.log(1111);
var localStorageData = e.data;
cb(null, localStorageData);
}
iframe.postMessage({key, operation: 'get'}, 'http://localhost:9999');
}
cookie domain
默认为当前的请求地址
假设 baidu.com 产生的 cookie要让子域 a.baidu.com b.baidu.com 访问,就需要 domain。
script async
defer: 延缓 解析html同时加载js,在解析完元素之后,DOMContentLoaded 之前执行,且按照在文档中出现的顺序执行;
async: 异步 解析html同时加载js,加载完js之后就执行,无顺序;
一图胜千言
容器与函子
容器
容器可以想象成一个盒子,盒子里面放着值.
var Container = function (x) {
this.__value = x;
}
Container.of = function (x) {
return new Container(x);
}
Container.of("test")
// Container {__value: "test"}
函子
遵守特定规则(从一个范畴到另外一个范畴)的容器
提供一种转换规则。
函子分为三大类:
普通函子
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
static of(val) {
return new Functor(val);
}
}
Functor.of(2).map(function (two) {
return two + 2;
});
// Functor(4)
Maybe 函子
函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个
空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
Functor.of(null).map(function (s) {
return s.toUpperCase();
});
// TypeError
class Maybe extends Functor {
map(f) {
return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
}
}
Maybe.of(null).map(function (s) {
return s.toUpperCase();
});
//
Either 函子
条件运算if...else是最常见的运算之一,函数式编程里面,使用 Either 函子表达。
Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的
值,左值是右值不存在时使用的默认值。
class Either extends Functor {
constructor(left, right) {
this.left = left;
this.right = right;
}
map(f) {
return this.right ?
Either.of(this.left, f(this.right)) :
Either.of(f(this.left), this.right);
}
}
Either.of = function (left, right) {
return new Either(left, right);
};
var addOne = function (x) {
return x + 1;
};
Either.of(5, 6).map(addOne);
// Either(5, 7);
Either.of(1, null).map(addOne);
// Either(2, null);
AP 函子
有 ap 函数,特殊在:val 是一个函数
class Ap extends Functor {
ap(F) {
return Ap.of(this.val(F.val));
}
}
const addTwo = (a) => a + 2;
Ap.of(addTwo).ap(Functor.of(2))
// 还可以
const add = (x) => (y) => x + y;
const ap = Ap.of(add).ap(Functor.of(2)).ap(Functor.of(3))
// { val: 5 }
Monad 函子
右边函数接受函子返回函子,该函数作用于 Monad 内部的值,Monad 接受一个返回函子的函数。
Monad 函⼦的作⽤是,总是返回⼀个单层的函⼦。它有⼀个 flatMap⽅法,与map⽅法作⽤相同,唯⼀的区别是如果⽣成了⼀个嵌套函⼦,它会取出后者内部的值,保证返回的永远是⼀个单层的容器,不会出现嵌套的情况。
嵌套例子:
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
static of(val) {
return new Functor(val);
}
}
function parseJson(val) {
try {
return Functor.of(JSON.parse(val))
} catch (err) {
return Functor.of(err)
}
}
var t = Functor.of('{"a":1}').map(parseJson);
console.log(t);
// Functor { val: Functor { val: { a: 1 } } }
解决val 嵌套问题:
// 解决
var t1 = Monad.of('{"a":1}').flatMap(parseJson).flatMap(parseStringfy)
console.log(t1);
// Monad { val: '{"a":1}' }
Monad 定义:
class Monad extends Functor {
join() {
// val 为 上一步 compose 之后得到的结果,为print 里面返回了 IO 函子
var res = this.val();
return res;
}
flatMap(f) {
//1.f == 接受一个函数返回的事IO函子
//2.this.val 等于上一步的脏操作
//3.this.map(f) compose(f, this.val) 函数组合 需要手动执行
//4.返回这个组合函数并执行 注意先后的顺序
return this.map(f).join();
}
}
IO
真正的程序总要去接触肮脏的世界。
function readLocalStorage(){
return window.localStorage;
}
跟前面那几个 Functor 不同的地方在于,它的 __value 是一个函数。
它把不纯的操作(比如 IO、网络请求、DOM)包裹到一个函数内,从而
延迟这个操作的执行。所以我们认为,IO 包含的是被包裹的操作的返回
值。
.IO其实也算是惰性求值。
IO负责了调用链积累了很多很多不纯的操作,带来的复杂性和不可维
护性。
//IO函子用来包裹📦脏操作
class IO extends Monad {
//val是最初的脏操作
static of(val) {
return new IO(val);
}
map(f) {
let val = compose(f, this.val);
// 组合完的
// 1 最初的脏操作
// 2 flatMap 新的操作 新的操作又是一个新的 IO
// { val: function() { 返回新的 IO } }
return IO.of(val)
}
}
使用
var readFile = function (filename) {
return IO.of(function () {
return fs.readFileSync(filename, 'utf-8');
});
};
var print = function (x) {
return IO.of(function () {
return x + "print";
});
}
var tail = function (x) {
return IO.of(function () {
return x + "tail";
});
}
const result = readFile('./user.txt')
.flatMap(print)
.flatMap(tail);
// console.log(result);
console.log(result.val()); // 得到结果 Hello printtail
函数式编程-必备的概念之《减少副作用》
函数
数学里面的函数:
f(x) = x ^ 2;
副作用
因果关系:我们调用一个函数(起因),屏幕上产生变化(结果)。
通读程序但不能看到因果的直接关系,程序的可读性就会降低。
没有任何歧义:
function foo(x) {
return x * 2;
}
var y = foo( 3 );
有一些函数存在一些潜在的原因:foo 的结果取决于 y 变量的值
function foo(x) {
return x + y;
}
var y = 3;
foo( 1 ); // 4
当使用固定的值:
const PI = 3.141592;
function foo(x) {
return x * PI;
}
foo( 3 ); // 9.424776000000001
每次调用 foo(3),都将会返回 9.424.. 这里认为并不违反副作用的精神,
常见的不纯的场景:
- 一个使用 Math.random() 的函数永远都不是纯的
- I/O 效果
幂等
数学的角度来看,幂等:指的一次调用和多次调用产生的结果是一样的。
foo(x) 将产生与 foo(foo(x))、foo(foo(foo(x))) 等相同的输出。
如:Math.abs,
编程中的幂等:编程中的幂等仅仅是 f(x) 的结果与 f(x),f(x) 相同。而不是要求 f(x) === f(f(x))。换句话说,之后每一次调用 f(x) 的结果和第一次调用 f(x) 的结果没有任何改变。
// 幂等的: 执行一次 和 多次 结果相同
obj.count = 2;
a[a.length - 1] = 42;
person.name = upper( person.name );
// 非幂等的:执行多次 每次都会产生新的 影响
obj.count++;
a[a.length] = 42;
person.lastUpdated = Date.now();
再比如:更新 DOM
var hist = document.getElementById( "orderHistory" );
// 幂等的:无条件覆盖以前的内容
hist.innerHTML = order.historyText;
// 非幂等的:叠加 html
var update = document.createTextNode( order.latestUpdate );
hist.appendChild( update );
纯函数
没有副作用的函数称为纯函数。
function add(x,y) {
return x + y;
}
// 输入(x 和 y)和输出(return ..)都是直接的,没有引用自由变量。调用 add(3,4) 多次和调用一次是没有区别的
纯函数的判别就是:
- 相同的输入,会不会产生相同的输出;
- 不依赖外部的状态;
- 不会产生副作用
纯函数 好处
缓存
错误捕获
try catch
只能捕获同步代码
window.onerror
window.onerror = function (msg, url, row, col, error) {
console.log("我已经捕获到错误");
console.log(msg, url, row, col, error);
return true;
}
- onerror用来捕获意料之外的错误
- try--catch 意料之内的错误
- 返回true 异常才会向上抛出
- 当遇到静态资源错误,无法捕获
资源错误:window.addEventListener("error")
addEventListener("unhandledrejection")
捕获 promise reject
node
// 捕获错误
process.on('uncaughtException', err => {
//do something
})
// promise
process.on('unhandledRejection', err => {
//do something
})
数据结构
常见数据结构操作的时间复杂度
哈希表
假设班级 30 个学生。
每个学生有自己名字,
假设 30 个学生放在数组里面,每次查找都是 O(n) 。
O(1) 的解决方式:
假设一个人 名字 lies
存在 一个hash 函数
计算得出 lies 得出的 hash 值为 9
那么我们在 9 的位置放入 lies 的信息:
这就是 hash 表的存储方式。
但是 通常情况存在 hash 碰撞,会出现重复的值,
解决方式可以:在同一个位置,以链表的方式存储,称为 拉链法
List
可放重复元素
list_x = [1, 2, 3, 3, 4]
insert:O(1) find: 0(n)
实现方式:数组 链表
Map
映射
map_x = { lies: 90 }
分类:HashMap TreeMap
区别:前者 hash 表实现 查找效率高 后者 二叉树实现 有序
Set
集合,
实现方式:哈希表 或者 二叉树
注意: Set 查找效率比 List 高
分类:HashSet TreeSet
区别:前者 hash 表实现 查找效率高 后者 二叉树实现 有序
堆
堆的实现方式也比较多,
这里以 Binary Search Tree 讲解
小顶堆
大顶堆
和小顶堆相反
树
二叉搜索树 BST
BST 新建
节点
/*
TreeNode
*/
function TreeNode(val) {
this.val = val;
this.left = this.right = null;
}
插入
function BinarySearchTree() {
this.root = null;
}
BinarySearchTree.prototype.insert = function(val) {
const node = new TreeNode(val);
// 如果没有根节点
if (!this.root) {
this.root = node;
return false;
}
let curNode = this.root;
// 不停查找节点的正确位置,
while(curNode) {
// 放置一个新的 左/右 节点
// 或者 再继续找位置
if (val < curNode.val) {
if (!curNode.left) {
curNode.left = node;
break;
}
curNode = curNode.left
}
else {
if (!curNode.right) {
curNode.right = node;
break;
}
curNode = curNode.right;
}
}
}
中序遍历
// 中序 左 根 右
function inOrder(node) {
if (!node) return;
inOrder(node.left);
// inOrder(node.val);
console.log(node.val);
inOrder(node.right);
}
webpack
公共代码
vender: ['react', 'react-dom'] dll
common: ['util'] commonchunk
home.js
性能
初级性能优化
静态资源处理:
- webpack:压缩,合并,MD5
- 多个 CDN,和前端不同源,能不带 cookie,
- gzip
- 缓存:离线缓存,http缓存
http:缓存优先级
- 强缓存:不发请求直接走缓存
cache-control:设置一个时间的长度(在当前时间基础上,再过多长时间过期),解决了 expires 的问题;
expires:设置一个过期时间,缺点: 浏览器时间和服务端时间不一致
返回状态码:200 form memory/disk - 协商缓存:会发请求,判断是否过期,没有过期才走缓
etag:请求带上 if-none-match 一个 由文件内容生成的 hash 值,判断的是文件内容
last-modified:请求 if-modified-since,服务端对比时间,是否一样,一样说明文件没有修改过,
返回状态码:304
一些不会变更的库:强缓存,业务代码:协商缓存
htt2.0 多路复用
1.0时代:请求-> 解析域名-> 找到服务器-> 建立http连接-> 传输数据
如果短时间内多个请求,每个请求都走这个流程。
keep-alive
1.1时代:
保持连接不关闭,可以复用连接,但是解析域名还是必要的
依然有 线头阻塞 ,要等上一个请求完了才能接着下一个请求
htt2.0
所有请求基于流,同一个域名下不管访问多少文件,只建立一个连接。
一个 http 连接可以处理多个请求。
解决了原来的线头阻塞: 一个 http 连接 在同一个时间只能处理一个请求
本请求要 依次发送 ,现在可以 并发。
HTTP2.0 还有:server push,
离线缓存
- localstorage:同步
- sessionstorage
- indexDb:异步
- websql:异步
作用:
缓存我们的业务代码
basket.js 资源通过 ajax 请求,然后帮你缓存起来
localForage 包装了 localstorage,indexDb,websql的缓存 - Service Worker
基于web work
的
web work: 可以新开启一个线程,可以干一些计算密集型的操作,不会阻塞主线程。任务执行完了之后会发一个信号给主线程。但是不能操作 dom
后端同时更改一个资源,有锁的机制。
Service Worker:也是在另外一个线程做事。可以拦截请求
Service Worker:简介, google doc
流程:
- 注册 service work
- 开启 web work 的线程
- 激活
- 监听页面的所有请求,
- 当页面发起请求,触发监听器,有缓存,取缓存内容,没有的话再走浏览器请求,缓存到 cache storage,这里面的内容只能走 service work。
- 资源更改的时候,webpack 打包生成 md5,
相关的库有:workbox
注意:
service work 拦截请求,一定只在 https 生效。localhost 也可以。
w3c navigation timing
- 把上一个页面清除,相等于,新的页面请求回来需要内存,需要资源,先把老的清除;
- ** redirect :** 检查 本地是否有缓存, 缓存是否过期 (cache-control)。并行地还会执行上一个页面的 unload 事件。
- app cacahe:从缓存里面取出
- 之后流程就和网络相关,DNS查找,DNS 解析第一次可能比较慢,但是之后可以缓存,
- TCP 连接,假如加了 https,那么还有 secureConnectionStart
- Request: 没有 request end,其实这里并不知道这个时间点,这个时间是由服务器决定的。所以 request start 和 response start 之间大多分工作在于服务端。
- response:response start, response end。如果建立的是长连接,那么在这里 之前的 tcp 连接还是能复用的。网络阶段结束
- Process:得到 dom,处理dom 树,字符串转换为 对应的数据结构
渲染中的性能优化
渲染
- 获取 DOM 元素,并分割多层,分层
- 对每个图层节点进行样式的计算,Recalculate Style
- 为每个节点生成 图形和位置,Layout 重排
- 为每个节点 进行绘制 Paint,填充到图层中去 重绘
- 把图层作为纹理上传 GPU
- 图层重组,渲染到页面中 Composite Layers
**简化流程:**Layout -> Paint -> Composite Layers 。一般的样式都会触发这个流程
网页里面的哪些元素会触发分层:
- 根元素
- position 不为 static
- transform
- 半透明
- canvas
- video
- overflow 不为 visible
什么是分层:
一个页面就是一个大的分层,一个元素的改变会影整个分层,为了减少影响,可以将页面分为多个分层,减小影响区域。
重排重绘影响的也是分层。
让 GPU 参与分层,GPU 参与渲染
- 让硬件加速 CSS 3D
- Video
- webgl 非常耗资源,CPU 特别卡,开启 GPU 加速。
- 滤镜
transform
CSS Triggers
做动画的时候优先使用 CSS 动画,
CSS 优先使用 transform
哪些元素会触发元素重排
- 只要结构改变,就重排,比如:添加删除元素,盒子模型变了
- 当读到某个属性 offset,scroll,clientWidth 重排。浏览器为了给到最准确的属性,会重排一次,得到准确数据给开发。重排。
- 举个例子:
display: '' 重排。
改变颜色:重绘
注意
操作的时候注意 读写分离。最大程度的减少重排。
const h1 = dom.clientHeight;
h1.style.display = 'xx';
const h2 = dom.clientHeight;
h2.style.display = 'xx';
// 更好的做法:
requestAnimationFrame(() => {
const h1 = dom.clientHeight;
const h2 = dom.clientHeight;
})
requestAnimationFrame(() => {
h1.style.display = 'xx';
h2.style.display = 'xx';
})
CPU GPU
CPU 负责操作系统相关的
GPU 渲染相关的
页面性能优化
指标
- FP:first paint 首次绘制,首次任何像素对用户可见的时间(第一个像素落地)
<div id="app"></div>
- FCP: first content paint 首次内容绘制 ,FCP 标记的是浏览器渲染来自 DOM 第一位内容的时间点
<div id="app">
<div class="header"></div>
<div class="body"></div>
</div>
// mounted 生命周期
- FMP: 首次有效绘制 (对用户最为有用的部分)
- Long Tasks:任何耗时超过 50 毫秒的任务
- TTI:可交互时间
- 各个时间综合:
- DOMContentLoaded:HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完成加载。
- load:一个资源及其依赖资源已完成加载时
怎么监控:performanceObserver
解决:
- ssr
首屏直出,服务器渲染很快就会产生First Paint(FP)和First Contentful Paint(FCP)。 - 预渲染,骨架屏
- 离线化,客户端参与
- 客户端启动,拿到页面的 html,css,js
- 当在客户端访问webview判断页面的连接是否在本地已经存在,不发网络请求
内存泄漏
- 发生情况
- 不使用Map,Set 多使用 Weakmap WeakSet
- 不在 node 里面存储数据,放在 redis
- 消费队列不及时
- 推荐
多用 buffer,stream - 压测
wrk -t 模拟线程数 -c 连接数 -d 持续时间
指标:
avg: 平均值 每次测试平均值
stdev :标准偏差
max: 最大值
+/- stdev:
词法环境
感叹!国外文章质量真的高。
本文分为两部分:
- 第一部分讲解通用理论知识
- 第二部分让讲解ESMAScript实现
通用理论
作用域
静态作用域
在程序解析阶段,就被确定下来,不需要运行时确定
今天使用静态作用域的语言有:C,Java,ECMAScript, Python, Ruby, Lua
等等
var x = 10;
var y = 20;
function foo() {
console.log(x, y);
}
foo(); // 10, 20
function bar() {
var y = 30;
console.log(x, y); // 10, 30
foo(); // 10, 20
}
bar();
上图的代码静态作用域划分
但是 加上 with
,eval
情况就有点特殊,他们无法在编译时确定。被认为他们给静态作用域添加了动态性。
var x = 10;
var o = { x: 30 };
var storage = {};
(function foo(flag) {
if (flag == 2) {
eval("var x = 20;");
}
if (flag == 3) {
storage = o;
}
with (storage) {
// x可能被解析到全局作用域中,值为10
// 也可能被解析的函数的局部作用域中,值为20
// 来源于eval方法执行结果的,或者从storage对象中获取
alert(x); // ? – 变量x的作用域在编译阶段无法确定
// 10 20 30
}
// 递归调用3次
if (flag < 3) {
foo(++flag);
}
})(1);
但是后来添加了 "use strict";
无法使用 with
, eval
不会在当前上下文创建变量。
所以,可以认为 ES5的严格模式下是完全遵守词法环境的实现
动态作用域
这里不做过多解释,借用作者代码;
Pascal伪代码:
// *pseudo* code – 使用动态作用域
y = 20;
procedure foo()
print(y)
end
// 在当前的全局变量栈上,变名为y的变量
// 当前只拥有一个值20
// {y: [20]}
foo() // 20, OK
procedure bar()
//在这里变量栈上拥有两个y值{y: [20, 30]};
//首先使用的是栈顶的
y = 30
// 因此: 这里作用域就是运行时候确定的。
foo() // 30!, not 20
end
bar()
环境
ECMAScript 通过 调用栈
来管理函数的执行,也叫 执行上下文栈
环境栈模型
自由变量
函数使用的既不是函数内声明的变量,也不是函数参数。
// Global environment (GE)
var x = 10;
function foo(y) {
//通过函数“foo”创建的环境(E1)
var z = 30;
function bar(q) {
// 通过函数“bar”创建的环境( (E2)
return x + y + z + q;
}
// 将“bar”返回到外部
return bar;
}
var bar = foo(20);
bar(40); // 100
环境 (E2) 中,变量 x,y,z,q 就属于自由变量。
为了能找到 这些变量。一个简单的方法就是,
函数激活时先创建自己的环境,包含函数内部的参数,变量,在创建一个外部环境的属性。
比如环境 (E2)
函数创建和应用规则
函数创建:
函数创建时由: 函数体
和 当时所在环境
的一个指针组成
比如上面(E2)环境
barframe = {
q: q,
outer: E1
}
加上函数体,可以组成
bar {
code: 'return x + y + z + q;',
environment: {
q: q,
outer: E1
}
}
函数调用时
executeCode(bar.code, barFrame);
闭包
闭包被发明是为了解决函数式参数- “funarg problem” 的问题。
备注:函数是一等公民,函数可以当做参数传递,function argument。
funarg问题:
- 一个函数返回值从父函数中被返回
(function (x) {
return function (y) {
return x + y;
};
})(10)(20); // 30
在内部函数中,会引用着外部的环境。
- 使用自由变量的函数被作为参数传递给另外一个函数时
var x = 10;
(function (funArg) {
var x = 20;
funArg(); // 10, not 20
})(function () { // 创建一个参数函数
console.log(x); //自由变量x
});
词法作用域,函数创建时就被绑定。
闭包,一等函数,这些问题都和词法作用域有关。 而词法环境正是用于实现闭包和静态作用于的通用技术。
ECMAScript 怎么实现 词法环境的
文档对象模型 (DOM)
-
Attr
使用对象来表示一个DOM元素的属性,
Element.getAttribute() 获取属性值;
Element.getAttributeNode() 获取 Attr 类型(即为一个对象) -
ChildNode
ChildNode.remove()
ChildNode.before()
ChildNode.after()
ChildNode.replaceWith() -
Document
Document 接口表示任何在浏览器中载入的网页
Document.all
Document.body
Document.documentElement
Document.forms
Document.images
Document.links
Document.scripts -
Element
所有 Document 对象下的对象都继承自它,接口描述了所有相同种类的元素所普遍具有的方法和属性。
方法太多,不一一列举。 -
Event
介绍了 DOM Event 模型
祭上这张图
-
MutationObserver
MutationObserver接口提供了监视对DOM树所做更改的能力。 -
Node
Node 是一个接口,许多 DOM API 对象的类型会从这个接口继承,
以下接口都从 Node 继承其方法和属性: Document,Element,Attr,DocumentFragment。
方法也比较丰富。 -
NodeList
NodeList 对象是一个节点的集合,是由 Node.childNodes 和 document.querySelectorAll 返回的.
http 缓存
浏览器:进程线程以及渲染
线程 VS 进程
多线程可以并行处理任务,但是线程是不能单独存在的,它是由进程来启动和管理的。那什么又是进程呢?
一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
- 进程中的任意一线程执行出错,都会导致整个进程的崩溃
- 线程之间共享进程中的数据
- 当一个进程关闭之后,操作系统会回收进程所占用的内存
- 进程之间的内容相互隔离
CPU、进程、线程之间的关系
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
- 不同进程之间也可以通信,不过代价较大
- 单线程与多线程,都是指在一个进程内的单和多
浏览器是多进程的
每一个应用程序都是一个进程, 而每一个应用程序都会分别有很多的功能模块,这些功能模块实际上是通过子进程来实现的。 对于这种子进程的扩展方式,我们可以称这个应用程序是多进程的。
可以看到一个Chrome浏览器启动了好多个进程:
- 浏览器是多进程的
- 每一个Tab页,就是一个独立的进程
只要将父进程杀死,其他子进程都会 over,sudo kill pid
同一域名处于同一进程
浏览器包含了哪些进程
- 主进程
- 协调控制其他子进程(创建、销毁)
- 浏览器界面显示,用户交互,前进、后退、收藏
- 处理不可见操作,网络请求,文件访问等
- 第三方插件进程
- 每种类型的插件对应一个进程,仅当使用该插件时才创建
- GPU进程
- 用于3D绘制等
- 渲染进程,就是我们说的浏览器内核
- 负责页面渲染,脚本执行,事件处理等
- 每个tab页一个渲染进程
那么浏览器中包含了这么多的进程,那么对于普通的前端操作来说,最重要的是什么呢?
答案是渲染进程,也就是我们常说的浏览器内核.
浏览器内核(渲染进程)
从前文我们得知,进程和线程是一对多的关系,也就是说一个进程包含了多条线程。
而对于渲染进程来说,它当然也是多线程的了,接下来我们来看一下渲染进程包含哪些线程。
GUI渲染线程
- 负责渲染页面,布局和绘制
- 页面需要重绘和回流时,该线程就会执行
- 与js引擎线程互斥,防止渲染结果不可预期
JS引擎线程
- 负责处理解析和执行javascript脚本程序
- 只有一个JS引擎线程(单线程)
- 与GUI渲染线程互斥,防止渲染结果不可预期
事件触发线程
- 用来控制事件循环(鼠标点击、setTimeout、ajax等)
- 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中
定时触发器线程
- setInterval与setTimeout所在的线程
- 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的
- 计时完毕后,通知事件触发线程
异步http请求线程
- 浏览器有一个单独的线程用于处理AJAX请求
- 当请求完成时,若有回调函数,通知事件触发线程
当我们了解了渲染进程包含的这些线程后,我们思考两个问题:
- 为什么 javascript 是单线程的(历史设计)
- 为什么 GUI 渲染线程为什么与 JS 引擎线程互斥
JS 是可以操作 DOM 的,如果同时修改元素属性并同时渲染界面(即 JS线程和UI线程同时运行), 那么渲染线程前后获得的元素就可能不一致了
从 Event Loop 看 JS 的运行机制
- JS引擎线程只执行执行栈中的事件
- 执行栈中的代码执行完毕,就会读取事件队列中的事件
- 事件队列中的回调事件,是由各自线程插入到事件队列中的
- 如此循环
宏任务、微任务
什么是宏任务
我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行),
每一个宏任务会从头到尾执行完毕,不会执行其他。
我们前文提到过JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。
// 宏任务-->渲染-->宏任务-->渲染-->渲染..
主代码块,setTimeout,setInterval等,都属于宏任务
例子:
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';
页面不会都抖动,属于同一 宏任务。
document.body.style = 'background:blue';
setTimeout(function(){
document.body.style = 'background:black'
},0)
页面抖动,属于不同批次的 宏任务。
什么是微任务
宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成在当前宏任务执行后立即执行的任务。
当宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完。
Promise,process.nextTick等,属于微任务。
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:black'
});
console.log(3);
依然无抖动,
宏任务 -> 微任务 -> 渲染
setTimeout(() => {
console.log(1)
Promise.resolve(3).then(data => console.log(data))
}, 0)
setTimeout(() => {
console.log(2)
}, 0)
// print : 1 3 2
总结
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
渲染
常见浏览器的渲染引擎
Trident: IE、Edge(旧)
Gecko: Firefox
WebKit: Safari
Blink(WebKit fork): Chromium/Chrome,Opera,Edge(新)
流程
注:其中经典的 DOM tree,Render Tree 都是数据,Layout 是对数据的操作,Painting 拿数据绘制。
DOM tree
Render tree
Layout tree
合成
GPU 渲染
一旦创建了层树并确定了绘制顺序,主线程就会将该信息提交给合成器线程。 合成器线程然后栅格化每个图层。 一个图层可能像页面的整个长度一样大,因此合成器线程会将它们分成图块,并将每个图块发送到光栅线程。 栅格线程栅格化每一个tile并将它们存储在GPU内存中。
webkit 相关
WebKit 官网:https://webkit.org/
Blink 是未来
Blink官方文档:http://www.chromium.org/blink
webkit 处于中心核心的部分,其中 js core 也有大名鼎鼎的 v8
参考
浏览器开发者工具简介
web安全 xss
攻击方式
反射型:
for exaple:
url: http://localhost:8080/?xss=%3Cimg%20src=%22%22%20alt=%22%22%20onerror=%22alert(1)%22%3E
const query = ctx.query;
// close browser xss protect
ctx.set('X-XSS-Protection', 0);
await ctx.render('index', {
xss: query.xss
})
攻击
<img src="" alt="" onerror="alert(1)">
<p onclick="alert(123456)">点我</p>
<!-- 广告 -->
<iframe src="http://www.badu.com" frameborder="0"></iframe>
防范
编码
html enentity 编码:
过滤
1.dom 属性 onerror onclick (事件相关)
2. style (!important)script(操作js) iframe(广告)
较正
避免直接对 html enentity 解码
使用 dom parse 转换 较正不匹配的 dom 标签
函数式编程-必备的概念之《管理函数的输入》
管理函数的输入
偏函数
英译:(partially-applied functions)
function getPerson(data, cb) {
ajax("http://some.api/person", data, cb);
}
function getCurrentUser(cb) {
getPerson({ user: CURRENT_USER_ID }, cb);
}
getPerson 是 ajax 的偏函数,
偏函数:减少函数参数个数的过程,通过 getPerson(..)
把原函数 ajax(..)
的参数个数从 3 个减少到了 2 个。
为了完成上文的功能 getCurrentUser,
定义一个专门处理偏函数的 函数;
function partial(fn, ...presetArgs) {
return function partiallyApplied(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
利用该函数,改写我们的 getCurrentUser,
var getPerson = partial(ajax, "http://some.api/person");
// 版本 1 一步到位
var getCurrentUser = partial(
ajax,
"http://some.api/person",
{ user: CURRENT_USER_ID }
);
// 版本 2 又偏了一次
var getCurrentUser = partial(getPerson, { user: CURRENT_USER_ID });
版本1,比较利于常规思维,
版本2,奇奇怪怪,getPerson 已经是一个 接受 { user: CURRENT_USER_ID } 即可完成功能的函数,但是有偏了一次,多套用了一层,这就是函数式编程,
刚刚怪异的一步只是函数式编程的冰山一角。
偏函数应用
存在一个 add 方法:
function add(x,y) {
return x + y;
}
现在,想象我们要拿到一个数字列表,并且给其中每个数字加一个确定的数值,使用 map(..) 。
[1,2,3,4,5].map( function adder(val){
return add( 3, val );
} );
因为 add 的函数签名 不符合 map 方法,
但是 我们 给 add 函数 偏一下
[1,2,3,4,5].map(partial(add, 3));
实参顺序颠倒
如上有一个函数ajax(url, data, cb)
要使得倒序传参:
function reverseArgs(fn) {
return function argsReversed(...args){
return fn( ...args.reverse() );
};
}
结合偏应用,我们还可以把参数顺序恢复。
var cache = {}; // 缓存结果
// 现在调用: ajax(cb, data, url)
var cacheResult = reverseArgs(
partial( reverseArgs( ajax ), function onResult(obj){
cache[obj.id] = obj;
} )
);
// 处理后:
cacheResult( "http://some.api/person", { user: CURRENT_USER_ID } );
真是任性呢。
右偏应用
实际上,就是参数从最后开始往前偏,
function partialRight( fn, ...presetArgs ) {
return reverseArgs(
partial( reverseArgs( fn ), ...presetArgs.reverse() )
);
}
// 调用例子
var cacheResult = partialRight( ajax, function onResult(obj){
cache[obj.id] = obj;
});
cacheResult( "http://some.api/person", { user: CURRENT_USER_ID } );
柯里化(currying)
将一个 接受连续参数的一个函数,改造为每次接受单一参数的 函数。
function curry(fn, arity = fn.length) {
return (function nextCurried(prevArgs) {
return function curried(nextArg) {
var args = prevArgs.concat([nextArg]);
if (args.length >= arity) {
return fn(...args);
}
else {
return nextCurried(args);
}
};
})([]);
}
应用之前的 map 例子
[1,2,3,4,5].map( curry( add )( 3 ) );
另一个例子:
function sum(...args) {
var sum = 0;
for (let i = 0; i < args.length; i++) {
sum += args[i];
}
return sum;
}
// 好,我们看看用柯里化怎么做:
// (5 用来指定需要链式调用的次数)
var curriedSum = curry( sum, 5 );
curriedSum( 1 )( 2 )( 3 )( 4 )( 5 );
partial(add,3) 和 curry( add )( 3 )
两个风格:柯里化风格(sum(1)(2)(3)),偏应用风格(partial(sum,1,2)(3))
柯里化:每次函数调用传入一个实参
偏应用:预先偏应用一部分实参,产出一个等待剩余参数的函数。
反柯里化
拿到一个柯里化后的函数,却想要它柯里化之前的版本 —— 这本质上就是想将类似 f(1)(2)(3) 的函数变回类似 g(1,2,3) 的函数。
拿到一个 curry 化之后的函数,把参数一一给它。
function uncurry(fn) {
return function uncurried(...args) {
var ret = fn;
for (let i = 0; i < args.length; i++) {
ret = ret(args[i]);
}
return ret;
};
}
只要一个实参
只希望函数接收单一实参。
以 map 方法为例,提取出 unary
function unary(fn) {
return function onlyOneArg(arg){
return fn( arg );
};
}
const foo = (...arg) => {
console.log(arg);
}
var arr = [0, 1, 2];
arr.map(unary(foo));
再比如,著名的 考题:
["1","2","3"].map( unary( parseInt ) );
传一个返回一个
及其简单
function identity(v) {
return v;
}
用处:identity 在 filter 的时候会被自动转 Bool 值。
words;
// ["","Now","is","the","time","for","all","...",""]
words.filter( identity );
// ["Now","is","the","time","for","all","..."]
再比如两个函数:
// 简单的输出
function output(msg,formatFn = identity) {
msg = formatFn( msg );
console.log( msg );
}
// 转大写
function upper(txt) {
return txt.toUpperCase();
}
output( "Hello World", upper ); // HELLO WORLD
output( "Hello World" ); // Hello World
利用 上文提到的 右偏 , 如果第二个参数,缺失,可用 identity 作为默认值。
var specialOutput = partialRight( output, upper );
var simpleOutput = partialRight( output, identity );
specialOutput( "Hello World" ); // HELLO WORLD
simpleOutput( "Hello World" ); // Hello World
恒定参数
Certain API 禁止直接给方法传值,而要求我们传入一个函数,就算这个函数只是返回一个值。JS Promise 中的 then(..) 方法就是一个 Certain API。
function constant(v) {
return function value(){
return v;
};
}
解决 Promise:
p1.then( foo ).then( () => p2 ).then( bar );
// 对比:
p1.then( foo ).then( constant( p2 ) ).then( bar );
spread gather
spread:
如下例子 foo 调用是失败的,参数不符合。
function foo(x,y) {
console.log( x + y );
}
function bar(fn) {
fn( [ 3, 9 ] );
}
bar( foo );
运用一个 spread:
function spreadArgs(fn) {
return function spreadFn(argsArr) {
return fn( ...argsArr );
};
}
// 再次改造一下
bar( spreadArgs( foo ) ); // 正确
gather:
如下 combineFirstTwo 的调用,不符合 reduce 语法,
function combineFirstTwo([ v1, v2 ]) {
return v1 + v2;
}
[1,2,3,4,5].reduce( combineFirstTwo );
// 15
再用 gather
function gatherArgs(fn) {
return function gatheredFn(...argsArr) {
return fn( argsArr );
};
}
[1,2,3,4,5].reduce( gatherArgs( combineFirstTwo ) );
无形参风格
风格1:
function double(x) {
return x * 2;
}
[1,2,3,4,5].map( function mapper(v){
return double( v );
} );
风格2:
function double(x) {
return x * 2;
}
[1,2,3,4,5].map( double );
// [2,4,6,8,10]
风格2即属于 无形参风格
sql command
查询函数
- min 在查询最小值的时候也想显示其他列
SELECT MIN(`birthdate`), `student`.* from student
- max
- count
- now 当前时间
- rand 随机数 (0 ~ 1)
- sqrt 平方根
- concat 拼接字符串
sql function详细文档
函数式编程-必备的概念之《函数组合》
组合
一个复杂的功能,通过一系列单一的函数组合而成
function compose(...fns) {
return function composed(result){
// 拷贝一份保存函数的数组
var list = fns.slice();
while (list.length > 0) {
// 将最后一个函数从列表尾部拿出
// 并执行它
result = list.pop()( result );
}
return result;
};
}
reduce 风格更简单。
算法
时间复杂度
通常会用大 O 表示法,常见的时间复杂度:
举个例子:
常数:O(1)
console.log(1);
对数:O(log n)
当数据量增大 n 倍,耗时增大 log n 倍。这里的 log 计算 以 2 为底
for (let i = 0; i < n; i = 2 ^ i) {
// code
}
线性:O(n)
for (let i = 0; i < n; i ++) {
// code
}
平方:O(n ^ 2)
for (let i = 0; i < n; i ++) {
for (let j = 0; j < n; j ++) {
// code
}
}
立方:O(n ^ 3)
指数:O(2 ^ n)
for (let i = 0; i < Math.pow(2, n); i ++) {
// code
}
阶乘:O(n!)
链表
栈 队列
Map && Set
二叉树
todo:
- 堆,小顶堆,大顶堆
- 双端队列
child_process 模块
child_process
模块提供了衍生子进程的能力
child_process.exec(command[, options][, callback])
exec文档
exec开始一个子进程执行shell命令,
并缓存输出传入callback的第二个参数 (err, stdout, stderr)
面向对象
JS 写多了,快忘记了面向对象是啥,以 php 为 例:
类
类,即使抽象的一个东西,
<?php
class Stu{
private $name;
private $sex;
function setInfo($name,$sex){
if($sex!='男'&&$sex!='女'){
echo '性别必须为男或女';
exit;
}
$this->name=$name;
$this->sex=$sex;
}
function getInfo(){
echo '姓名: '.$this->name.'<br>';
echo '性别: '.$this->sex.'<br>';
}
}
$stu=new Stu;
$stu->setInfo('tom','男');
$stu->getInfo();
echo '<hr>';
$stu1=new Stu;
$stu->setInfo('Sunny','女');
$stu->getInfo();
?>
修饰符:
public (公有的) | 在类的内部和外部都能访问
private (私有的) | 只能在类的内部访问
protected (受保护的) | 在整个继承链上访问
构造方法 和 析构方法
构造函数, 当实例化对象的时候自动执行。
<?php
class Stu{
private $name;
private $sex;
function __construct($name,$sex){
$this->name=$name;
$this->sex=$sex;
}
function show(){
echo "姓名: {$this->name}<br>";
echo "性别: {$this->sex}<br>";
}
}
$stu=new Stu('Wang','男');
$stu->show();
?>
析构方法: 当对象销毁的时候自动调用
class Stu{
private $name;
function __construct($name){
$this->name=$name;
echo "{$name}出生了<br>";
}
function __destruct(){
echo "{$this->name}销毁了<br>";
}
}
$stu1=new Stu('Tom');
$stu2=new Stu('Wang');
echo '<hr>';
继承
extend 关键词
<?php
class Person{
public function __construct($flag){
$this->flag = $flag;
}
}
class Stu extends Person{
public function __construct($flag, $name){
parent::__construct($flag);
$this->name = $name;
}
}
$stu = new Stu('人', '学生');
echo $stu->flag.$stu->name
?>
多态
一个类,被多个子类继承,如果这个类的某个方法,在多个子类中,表现出不同的功能,我们称这种行为为多态。(同一个类的不同子类表现出不同的形态)。
<?php
abstract class animal{
abstract function fun();
}
class cat extends animal{
function fun(){
echo "cat say miaomiao...";
}
}
class dog extends animal{
function fun(){
echo "dog say wangwang...";
}
}
function work($obj){
if($obj instanceof animal){
$obj -> fun();
}else{
echo "no function";
}
}
work(new dog());
work(new cat());
?>
dog,cat 都有 fun 方法,而且每个的表现都不一样。
abstract 定义的类,抽象类,不做具体的事,抽象类不能被直接实例化。抽象类中只定义(或部分实现)子类需要的方法。子类可以通过继承抽象类并通过实现抽象类中的所有抽象方法,使抽象类具体化。
接口
实现接口的类都要实现接口中所定义的所有方法,(除了抽象类) 。
<?php
interface iA {
const AVAR=3;
public function iAfunc1();
public function iAfunc2();
}
class E implements iA {
public function iAfunc1(){echo "in iAfunc1";}
public function iAfunc2(){echo "in iAfunc2";}
}
?>
再来两个接口
interface iB {
public function iBfunc1();
public function iBfunc2();
}
php 没有多继承,只能继承一个类,但是可以实现多个接口。
class D implements iA,iB {
public function abstract_func1() {
echo "implement the abstract_func1 in class A/n";
}
public function abstract_func2() {
echo "implement the abstract_func2 in class A/n";
}
public function iAfunc1(){echo "in iAfunc1";}
public function iAfunc2(){echo "in iAfunc2";}
public function iBfunc1(){echo "in iBfunc1";}
public function iBfunc2(){echo "in iBfunc2";}
}
接口不可以实现另一个接口,但可以继承多个
interface iC extends iA,iB{}
class F implements iC {
public function iAfunc1(){echo "in iAfunc1";}
public function iAfunc2(){echo "in iAfunc2";}
public function iBfunc1(){echo "in iBfunc1";}
public function iBfunc2(){echo "in iBfunc2";}
}
$f = new F();
$f->iAfunc2();
抽象类 VS 接口: interface 强调特定功能的实现,而 abstract class 强调所属关系。
认识 for...of
for...of
在 可迭代对象
上面创建一个迭代循环,调用自定义的迭代钩子。
可迭代对象
:Array, Map,Set,String,arguments
可迭代对象
有两个协议:
- 可迭代协议
js 对象 实现了@@iterator 方法(原型链上存在 Symbol.iterator 属性) - 迭代器协议
有 next 方法,并且返回 一个 对象:
{
done:
value:
}
基于以上两点
即:反过来也成立,我们认为:
一个结构可不可以用 for...of
迭代,可以判断它有没有部署了 iterator,成为一个 可迭代对象。
举个例子:
var obj = {
}
obj[Symbol.iterator] = function () {
return {
next: function () {
if (initIndex < 5) {
initIndex++
return {
done: false,
value: {v: initIndex}
}
} else {
return {
done: true,
value: undefined
}
}
}
}
}
最后我们使用 for...of 循环输出
for (let a of obj) {
console.log('-', a);
}
得到 每一个对象 {v: 1}
ES5-基础知识
变量提升
if (false) {
var a = 1;
}
console.log(a); // undefined
变量和函数重名的情况:需要关注,变量是否被赋值,如果被赋值,输出 变量,否则 输出 函数
function foo() {
console.log(1);
}
console.log(foo)
var foo = 1;
题目:
alert(a)
a();
var a = 3;
function a() {
alert(10)
}
alert(a)
a = 6;
a();
- 函数体 (函数申明,赋值都会提升)
- 10 (函数 a 执行)
- 3 (var 声明提升,赋值不会提升)
- 报错 (a 变成了一个变量)
再来一题:
var x = 1,
y = 0,
z = 0;
function add(x) {
return (x = x + 1);
}
y = add(x);
console.log(y)
function add(x) {
return (x = x + 3);
}
z = add(x);
console.log(z)
- 4
- 4
提升之后 被覆盖了
this
题目:
this.a = 20;
function go() {
console.log(this.a);
this.a = 30;
}
go.prototype.a = 40;
var test = {
a: 50,
init: function (fn) {
fn();
console.log(this.a);
return fn;
}
};
console.log((new go()).a);
// - go 里面的 this 指向 实例, 实例上面虽然没有赋值,但是原型链上有 40,第二个已有实例 30
test.init(go);
// 20 50 init 里面的 函数调用方式依然是是一个普通的函数调用
var p = test.init(go);
// 30 50 需要注意 上一步修改了 window 下面的 a 的值
p();
// 30
题目:
```js
var length = 10;
function fn() {
// 需要注意 全局有个 length 变量,如果没有。 window.length 表示 iframe 数量。
console.log(this.length);
}
var obj = {
length: 5,
method: function (fn) {
fn(); // 指向 window
arguments[0](); // 指向 arguments,arguments.length 为 2
}
};
obj.method(fn, 1, 3);
## 严格模式
题目:需要注意,严格模式是在哪个作用域内开启的。
```js
var num = 1;
function foo() {
"use strict";
console.log(this.num++); // error
}
function bar() {
console.log(++this.num); // 输出 2
}
(function() {
"use strict"; // 虽然是严格模式 但是 没在该作用域内 获取 this
bar();
})();
foo(); // 严格模式 this 没有指向 window 了
new
function C1(name) {
if (name) this.name = name;
}
function C2(name) {
this.name = name;
}
function C3(name) {
this.name = name || 'fe';
}
C1.prototype.name = "1";
C2.prototype.name = "2";
C3.prototype.name = "3";
console.log((new C1().name) + (new C2().name) + (new C3().name));
- 自己没有,找到原型链
- 自己有一个 undefined
- 运算符 || fe
1 undefined fe
li 列表问题
如li,正确输出li里的数字。
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
如下:
// 1
for (var i = 0; i < list_li.length; i++) {
list_li[i].onclick = function () {
console.log(this.innerHTML);
}
}
// 2
for (var i = 0; i < list_li.length; i++) {
(function(i) {
list_li[i].onclick = function () {
console.log(i);
}
})(i)
}
函数提升
function foo() {
console.log(1);
}
(function () {
if (false) {
function foo() {
console.log(2);
}
}
foo();
})();
报错,函数只会声明提升。
请用一句话算出0-100之间学生的学生等级,如90-100输出为1等生、80-90为2等 生以此类推。
等级 = 10 - score / 10
请用一句话遍历变量a。(禁止用for 已知var a = “abc”)
Array.prototype.slice.call(a)
继承
寄生组合继承
继承属性:
parentConstructor.call(this)
继承方法 修正 constructor:
Object.create(parentConstructor.prototype,constructor: { value: () => {}, writeable: false})
继承静态属性:
遍历一次:Object.entries(function)
正则表达式
如果正则表达式设置了全局标志,test() 的执行会改变正则表达式 lastIndex属性。连续的执行test()方法,后续的执行将会从 lastIndex 处开始匹配字符串,(exec() 同样改变正则本身的 lastIndex属性值).
var regex = /a/g;
console.log(regex.test('abcabc'));
console.log(regex.test('abcabc'));
console.log(regex.test('abcabc'));
console.log(regex.test('abcabc'));
函数声明,函数表达式
var foo = function bar() {
bar = 1; // 函数表达式内部,修改不了函数
console.log(typeof bar); // 依然是 function
}
foo();
bar = 123; // 外部可以修改
console.log(typeof bar); // number
前端工程化 Linux 预备
linux
只是一个内核
发行版本有:ubuntu、CentOS、redhat、Fedora、Debian
redhat 企业版收费
CentOS 基于 redhat 编译出来,去除 redhat 收费组件
Fedora 基于 redhat,新特性比较多
ubuntu 以桌面应用为主的Linux操作系统.
常用Linux命令
- 行编辑器 vi/vim
- 服务管理命令 systemctl
- 网络管理命令 ifconfig、ip命令、router
- 命令行下载命令 curl、wget
linux 两种模式
- 运行 1 + 1 处于 用户态
- 输出结果 处于 内核模式
程序,进程、线程与协程
- 程序:
代码,静态的
- 进程:
代码运行时,加载到内存中,程序就叫进程,CPU 从 内存中获取指令执行,程序类似 类,进程类似 对象。
进程的目的就是担当分配系统资源(CPU时间,内存)的实体。
CPU是多核甚至众核的,
一个进程可以跑在多个核心上
- 线程:
是操作系统能够进行运算调度的最小单位
进程,线程属于内核管理,
可以使用多核资源
- 协程:
调度方式类似线程,轻量级线程,不需要操作系统调用,
IO密集型应用发展:多进程(线程还没出现) -> 多线程 -> 事件驱动 -> 协程 (node, nginx 集中在网络上的应用)
CPU 密集型应用发展: 多进程 -> 多线程
调度和切换时间:进程 -> 线程 -> 协程
进程与线程
- 以多进程的形式,允许多个任务同时运行
nginx 工作进程一个,干活的子进程,另外还有一主进程负责调度
worker_processes 1;
- 以多线程的形式,允许单个任务分成不同的部分运行
3.提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面 允许进程之间和线程之间共享资源。
进程线程之间的资源共享
进程管理相关命令
top 命令详解
ps 命令 详解
kill、pkill 命令及使用注意事项
w 命令
Linux网络管理
查看和配置网络基本信息 ifconfig、ip
重启网卡
查看路由配置 router
排查网络故障 tracerout
怎样找到占用网络端口的进程 :ss命令、netstat命令
免密登录
采用非对称加密
一对秘钥
公钥给服务器,
1、生成秘钥对
ssh-keygen -t rsa -C "你自己的名字" -f "你自己的名字_rsa"
2、上传配置公钥
上传公钥到服务器对应账号的home路径下的.ssh/中 ( ssh-copy-id -i "公钥文件名" 用户名@服务器ip或域名 )
配置公钥文件访问权限为 600
3、配置本地私钥
把第一步生成的私钥复制到你的home目录下的.ssh/ 路径下
配置你的私钥文件访问权限为 600
chmod 600 你的私钥文件名
4、免密登陆功能的本地配置文件
编辑自己home目录的.ssh/ 路径下的config文件
配置config文件的访问权限为 644
yum
- yum:rpm的前端程序,自动解决软件包相关依赖性。
- yum repository:yum repo,存储了众多rpm包,以及包的相关的元数据文件(放置于特定目录repodata下)
- yum客户端配置文件:
/etc/yum.conf:为所有仓库提供公共配置
/etc/yum.repos.d/*.repo:为仓库的指向提供配置
其他
Yum-utils 功能简介: 管理repository及扩展包的工具 (主要是针对repository)
react 一些文章收集整理
v16 生命周期
react fiber 架构
从React16生命周期到React fiber架构
作者: 桃园,的博客
react Reconciliation 协调算法
中文文档
W3c关于requestidlecallback解释
国外系列,动画演示build-your-own-react
国外系列
浏览器层合成
页面展示
在编写页面中,我们要知道浏览器如何处理 HTML
、JavaScript
和 CSS
。
需要了解并注意五个主要区域, 这些我们拥有控制权的部分,也是像素至屏幕管道中的关键点。
每一步简介
- JavaScript。 一般来说,我们会使用 JavaScript 来实现一些视觉变化的效果。比如用 jQuery 的 animate 函数做一个动画、对一个数据集进行排序或者往页面里添加一些 DOM 元素等。当然,除了 JavaScript,还有其他一些常用方法也可以实现视觉变化效果,比如:CSS Animations、Transitions 和 Web Animation API。
- 样式计算。 此过程是根据匹配选择器(例如 .headline 或 .nav > .nav__item)计算出哪些元素应用哪些 CSS 规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。
- 布局。 在知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,例如 元素的宽度一般会影响其子元素的宽度以及树中各处的节点,因此对于浏览器来说,布局过程是经常发生的。
- 绘制。 绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的。
- 合成。 由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。
ps: 当然,不是每一步更改都会遵循上图这个流程。
每一步不是必经的
比如:
- 更改了元素的布局相关的属性:
width
,height
,位置
... 那么浏览器就会检查其他元素,自动重排一次
。 - 更改了元素的
color
,阴影
... 不会影响页面的布局,那么浏览器就会跳过布局。这就是我们平常说的:重排一定引起重绘,重绘不一定引起重排
。 - 如果更改了 一个既不会布局,也不会绘制的属性,那么浏览器直接跳到最后一步,不得不说,这是最高效的
使用 csstriggers 可以详细看到 css 属性改变时触发的流程。
如何提升绘制的性能
尽量使用影响较少的属性
举个🌰:
<div class="box box1">1</div>
<div class="box box2"> 2 </div>
<script>
const box1 = document.querySelector('.box1');
setTimeout(() => {
box1.style.display = 'none'
}, 3000);
</script>
我们可以看到,box1
和 box2
都绿(重绘)了一次,说明 box1
的变化影响了 box2
。那这个属性变化的代价是比较大的。
假如是我让 box1
的位置 向右移动 60px
,我们做如下更改:
box1.style.display = 'none'
现在 box2
的位置不受影响,直观地看到 box2
是没被绿(重绘)的。
提升为合成层(Compositing Layers)
我们在上一步做了优化,box2
已经不受影响,但是 box1
依然被重绘,那能不能在优化呢。
答案是能的。
left 这个属性的改变会造成的影响是:
layout -> painted -> composited
这个流程可以在 csstriggers 看到。
那现在我们要找到一个 css 属性,既能让元素位移,又能造成的影响最小。
答案是有的:
transform
:影响最小,直接到达最后一步 compositor。
做如下更改:
box1.style.transform = 'translateX(60px)'
好像事与愿违。box1
, box2
都被重绘了。
这里因为:他们都在一个层上,一个元素的变化也影响了其他元素的变化。浏览器会联合需要绘制的区域,而导致整个屏幕重绘
其实这里有个条件:
更改属性所在的元素应处于其自身的合成层,如果没在,我们可以提升为合成层
这样就不会影响其他元素,而能减少绘制区域。
提升为合成层的原因有一下几种
这里我大概罗列了这么多
- video
- 有 3D transform
- backface-visibility 为 hidden
- 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)
- will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等)
- 重叠原因
在 box1
上面做如下更改:
will-change: transform;
再次观察效果:
大功告成:
- box1 不在重绘了
- box2 不受影响
我们可以查看最终的分层效果:
和 ps
里面的图层差不多,每一个图层叠加在一起组成我们看到的网页。
物极必反
图层越多越好吗?
当然不是。提升合成层也得 消耗额外的内存和管理资源
正如MDN所说:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。 will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题
参考
前端性能优化之 Composite
關鍵轉譯路徑 Critical Rendering Path
will-change
坚持仅合成器的属性和管理层计数
Node 里面的 Stream/pipe 知识
what
流是 UNIX 系统中的一个标准概念,很多场景都需要流,
比如 cat main.js | grep fs
,
先打印出来 main.js 的内容,再过滤 fs 相关的。
流在 Node 里面有很多应用。
比如:
- fs:createReadStrem / createWriteStrem
- http:request / response
- process:stdin / stdout
四个流
-
Readable,可读流,用来提供数据,外部来源的数据存到 Buffer 内缓存,两个模式 pause / resume
-
Writeable,可写流,用来消费数据,对拿到 Buffer 数据进行处理消耗,把它写入对象,它有 drain 事件 ,判断是否将缓存数据写入完毕。
-
Duplex:双工流,即是 Readable 又是 Writeable
-
Transform:转换流 ,本身也是双工流,它只是和输入输出有一定的关联关系,不保存数据,只负责处理加工经过他的数据。
总结:四大流都有不同的能力,都是对缓冲的数据进行处理。
把他们结合起来通常通过 pipe 管道来连接或者反转
pipe
一段比较经典的代码
const fs = require('fs')
const http = require('http')
http.createServer((req, res) => {
res.writeHeader(200, {'Content-Type': 'application/pdf'})
fs.createReadStream('./js.pdf').pipe(res);
}).listen(9999)
- 负责获取外部数据,并把外部数据存在内部 Buffer
- 可写流负责消费数据,从可写流中获取数据,
- 数据块会进行处理,至于如何处理取决于这个可写流内部 Write 方法如何实现。
- pipe 会自动控制数据的读取速度,来帮助数据以一种比较合理的速度,源源不断传输给目的地
自定义 流
http://nodejs.cn/api/stream.html#stream_api_for_stream_implementers
根据所创建的流类型,新的流类必须实现一个或多个特定的方法,如下图所示:
如下:字符串处理的例子,pipe 自己处理 write 方法,直接让我们拿到数据处理。
// 拿到 stream 里面的可读可写流接口
const fs = require('fs');
const Readable = require('stream').Readable
const Writeble = require('stream').Writable
const rs = new Readable()
const ws = new Writeble()
let n = 0
// 一次次往流里面推数据
rs.push('I ')
rs.push('Love ')
rs.push('Juejin!\n')
// 推送 null 来告诉流可以 close 了
rs.push(null)
// 每一次 push 的内容在 pipe 的时候
// 都会走到 _write 方法,在 _write 里面可以再做处理
ws._write = function(chunk, ev, cb) {
n++
console.log('chunk' + n + ': ' + chunk)
// chunk1: I
// chunk2: Love
// chunk3: Juejin!
fs.appendFile('./demo.txt', chunk.toString(), () => {});
cb()
}
// pipe 将两者连接起来,实现数据的持续传递,我们可以不去关心内部数据如何流动
rs.pipe(ws)
加上转换流
const stream = require('stream')
class ReadStream extends stream.Readable {
constructor() {
super()
}
/**
* 所有可读流的实现必须提供 readable._read() 方法从底层资源获取数据
* http://nodejs.cn/api/stream.html#stream_readable_read_size_1
*/
_read () {
this.push('I ')
this.push('Love ')
this.push('Juejin!\n')
this.push(null)
}
}
class WriteStream extends stream.Writable {
constructor() {
super()
}
/**
*
* @param {*} chunk
* @param {*} encode
* @param {*} cb
* 所有可写流的实现必须提供 writable._write() 方法将数据发送到底层资源。
* http://nodejs.cn/api/stream.html#stream_constructor_new_stream_writable_options
*
*/
_write (chunk, encode, cb) {
console.log(chunk.toString())
cb()
}
}
class TransformStream extends stream.Transform {
constructor() {
super()
this._storage = Buffer.from('')
}
/**
*
* @param {*} chunk
* @param {*} encode
* @param {*} cb
* 转换流的实现都必须提供 _transform() 方法来接收输入并生产输出。
* transform._transform() 的实现会处理写入的字节,进行一些计算操作,
* 然后使用 readable.push() 输出到可读流。
*/
_transform (chunk, encode, cb) {
this.push(chunk)
cb()
}
/**
*
* @param {*} cb
* 某些情况下,转换操作可能需要在流的末尾发送一些额外的数据
*/
_flush (cb) {
this.push('Oh Yeah!')
cb()
}
}
const rs = new ReadStream()
const ws = new WriteStream()
const ts = new TransformStream()
rs.pipe(ts).pipe(ws)
递归
尾递归
函数调用自身成为递归,
尾调用自身称为尾递归。
// 不是尾递归,无法优化 斐波那契数列
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
cookie 和 session
cookie
保存在客户端的小段文本, 随客户端 每一个请求发送该 url 下 所有 cookie
到服务器
sweet cookie
客户端发送 cookie:cookie
服务端设置 cookie: set-cookie:
session
存在服务端,通过唯一的 值 sessionID 来区分每一个用户,
sessionID 随每一个链接请求发送到服务器,服务器根据 sessionID 来识别客户端,(东哥脸盲)
token
令牌。
周期性的东西
前端工程化 CI&CD
CI & CD
“CI”始终指持续集成,集成是一个很可能发生未知错误的过程。持续集成是一种软件开发实践,希望团队中的成员频繁提交代码到代码仓库,且每次提交都能通过自动化测试进行验证,从而使问题尽早暴露和解决。
“CD”指的是持续交付和/或持续部署,
主要解决程序迭代过程中的自动化。持续集成的扩展,指的是将通过自动化测试的软件部署到产品环境。持续交付的本质是把每个构建成功的应用更新交付给用户使用。
主要应用于 web 端。
jenkins 环境 下载
jenkins支持多个版本下载
jenkins依赖jdk8下载
或者
// 安装 JDK
rpm -i jdk-8u231-linux-x64.rpm
// 安装 jenkins
rpm -i jenkins-2.190.2-1.1.noarch.rpm
// 启动
systemctl status jenkins
// 查看状态
systemctl status jenkins
脚本发布
较小规模可以选择 脚本简单发布,以下伪代码演示
// 安装依赖
console.log(colors.yellow('🐛️ 安装依赖'));
if (shelljs.exec('npm i').code !== 0) {
shelljs.echo('error: npm install error.');
shelljs.exit(1);
}
// 测试
console.log(colors.yellow('🐛️ 进行测试'));
if (shelljs.exec('npm run test').code !== 0) {
shelljs.echo('error: npm install error.');
shelljs.exit(1);
}
// 构建
sendNotify('开始构建');
console.log(colors.yellow('☕️ 开始构建'));
if (shelljs.exec('npm run build').code !== 0) {
shelljs.echo('error: npm install error.');
shelljs.exit(1);
}
Drone
一个比较新的 CI系统,基于 docker,源码为 go 书写。
安装 docker:
安装必要的系统工具
yum install -y yum-utils device-mapper-persistent-data lvm2
添加 docker 源
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
更新yum缓存
yum makecache fast
安装docker 社区版
yum -y install docker-ce
配置国内镜像源
curl -sSL http://oyh1cogl9.bkt.clouddn.com/setmirror.sh | sh -s <镜像加速地址>
curl -sSL http://oyh1cogl9.bkt.clouddn.com/setmirror.sh | sh -s http://dockerhub.azk8s.cn # Azure
# https://github.com/Azure/container-service-for-azure-china/blob/master/aks/README.md#22-container-registry-proxy
启动Docker
systemctl start docker
Docker Compose 是一个工具,命令行工具,管理运行多个 Docker 容器。
安装docker compose
curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
给docker compose可执行权限
chmod +x /usr/local/bin/docker-compose
参考:
docker 安装入门
MPA SPA
MPA
每点击一个连接
提交一个表单
或者请求一个数据,
都需要从服务器请求一个新的页面回来。
SPA
几种不同的渲染
-
CSR:FP 快;客户端体验好;SEO 不好;FCP,FMP比较慢
-
预渲染:preRender-spa-plugin,无头浏览器,在本地构建,上线前,执行 build 完的 vue 代码,截取当前那一帧页面的 html,输出到 html 文件里面。相当于构建的时候生成 html。
缺点:
html 截取的是当前那一帧,所以一些实时性比较高的页面就不适合 -
SSR:jsp php 等后端语言输出 html
-
同构:同一套代码运行于服务端,客户端
Vue ssr
main.js:生成 Vue 实例,路由
client-entry:将 Vue 挂载节点
server-entry:将 Vue 实例 渲染
webpack 打包:
base.conf:添加入口
客户端配置:vue-server-render/client-plugin, 生成客户端的构建清单,不需要 html-webpack-plugin。
vue-ssr-client-manifest.json:一份静态资源清单,需要下发浏览器
服务端配置:vue-server-render/server-plugin,生成服务端渲染需要的 (vue-ssr-server)
vuex 的 同构
需要 dispatch 请求数据的时候,给每一个组件添加一个方法,比如 asyncData。
得到组件上面的 component是属性,在拿到每个组件的 asyncData,最后 Promise.all 得到所有的请求结果。
vue-server-render
createRender:通过app 实例 构建 html
createBoundleRender:通过 打包出来的 json 构建 html
export const createBundleRenderer = createBundleRendererCreator(createRenderer)
// 使用
createBundleRenderer(json文件, {模板})
// 3
解析 json 里面的 entry files
// 4
const renderer = createRenderer(rendererOptions)
// 5
const run = createBundleRunner(
entry,
files,
basedir,
rendererOptions.runInNewContext
)
打包出来你的 files 是个 js 文件但是是字符串,
最后 vue 将他放在 vm.Script 里运行,放在 node 里面的沙箱里运行。最终返回 vue 实例。
怎么生成 html
renderer.renderToString()
with(this){return ${code}}
http 协议
浏览器行为与HTTP协议
输入一个 url:
处理流程:
- 输入网址并回车
- 解析域名
- 浏览器发送HTTP请求
- 服务器处理请求
- 服务器返回HTML响应
- 浏览器处理HTML页面
- 继续请求其他资源
第二步,跨越防火墙(传输层)还可以认为,经过代理(应用层),向电信拨号( ppp连接层 )
DNS: 域名 -> IP,向 DNS 服务器查询,是一个大数据库,k-v 数据结构,ipv4(2 ^ 32)个,所以导致不够用,产生 ipv6(2 ^ 128)。
取到数据,拆为数据包,在不同的路由器之间跳跃(在路由之间传递一次称为一跳),不断传输
例如:访问百度的时候跳过了这么多路由
什么是 http 协议
最先设计为军用,后民用。
超文本传输协议,从 http 服务器传输到本地。
http 协议是从 request 到 response 进行 约束和规范
http1.1 (1999) -> http2(2015)
计算机存储:
- 二进制
- 文本( ASCII 编码后的二进制)
超文本:不局限于文本,例如 video, audio
TCP / IP 协议
- 左边 ISO/OSI
- 右边 TCP/IP
- 应用层:为用户提供所需要的各种服务,例如:HTTP、FTP、DNS、SMTP,webSocket
- 传输层:TCP/UDP
- 网络层:IP协议,ICMP协议报(ping命令)
- 数据链路层:数据信号转为电信号
- 物理层:(硬件相关,载体)
在TCP/IP协议栈中的位置
一次完整的 http 事务
同:输入 url 到 看到页面
TCP 连接后端开,主要是连接需要计算机资源,比如端口,断开释放资源
请求 响应
请求报文
一个HTTP请求报文由
- 请求行
- 请求头部
- 空行
- 请求体
空行
为了把头和请求数据分开
下图给出了请求报文的一般格式
请求方法
- GET: 请求获取Request-URI所标识的资源
- POST: 在Request-URI所标识的资源后附加新的数据
- HEAD: 请求获取由Request-URI所标识的资源的响应消息报头
- PUT: 请求服务器存储一个资源,并用Request-URI作为其标识
- DELETE: 请求服务器删除Request-URI所标识的资源
- TRACE: 请求服务器回送收到的请求信息,主要用于测试或诊断
- CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
- OPTIONS: 请求查询服务器的性能,或者查询与资源相关的选项和需求
HTTP状态码
状态代码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值:
- 1xx:指示信息--表示请求已接收,继续处理
- 2xx:成功--表示请求已被成功接收、理解、接受
- 3xx:重定向--要完成请求必须进行更进一步的操作
- 4xx:客户端错误--请求有语法错误或请求无法实现
- 5xx:服务器端错误--服务器未能实现合法的请求
常用的请求报头
Connection: keep-alive 暂时不断开连接继续请求响应
Accept: 客户端支持哪些文件,
Content-type:
Accept-Encoding:
常见响应头
Content-Encoding:
DNS
Domain Name System 域名系统,用于将域名转换为 IP
一个 IP 有四段,都为 0 ~ 255 之间,比如 127.0.0.1
一个 int 4个 字节,一个 ip 刚好分为 4段 ,一段对应一个 字节。
顶级域名
baidu.com 顶级域名
www.baidu.com 二级域名
域名解析
- 自己的电脑
- 域名解析的服务器,位于运营商那里,负责 将域名发送到其他服务器,得到结果然后缓存
- root server 解析 .com
- TLD server 解析 google.com
- name server 解析 www.baidu.com,得到服务器 ip
每一层的职业都不一样,root server 全球只有 13 台。
这个过程是比较麻烦的,所以 2 位置的服务器有的会镜像一下国外的根服务,每隔一段时间同步一次。
域名资源记录
TCP 三次握手 四次挥手
TCP:面向连接的,通信之前要建立连接,确认没问题才会发送接收
UDP:类似广播,不管会不会收到
三次握手,四次握手,只是一种机制,一种确保可靠通信的机制。
挥手:
- 客户端发送断开请求,
- 服务端响应断开请求
- 服务端发送断开请求
- 客户端响应断开请求
服务端需要多一步,服务端需要做一些清理工作
握手:
- 客户端请求连接
- 服务端接收到连接请求服务端响应连接请求
- 连接开始建立
CDN
图一:只有一个服务器,所有的客户端都从一个服务器拿数据
图二:分布式的一个解决方案,
https 协议
密码学入门
- 散列
单向不可逆,一种数据一旦转换为其他形式将永远无法恢复的加密技术。
MD5,
比如用户注册:
CSDN:拖库事件
- 加密
对称加密(AES、DES、3DES)
非对称加密(RSA) :一对密钥,公钥加密,私钥解密
- 密钥交换算法
Diffie-Hellman算法是一种著名的密钥协商算法,这种算法可以使得信息交换的双方通过公开的非安
全的网络协商生成安全的共享密钥。
(1)Alice与Bob确定两个大素数n和g,这两个数不用保密
(2)Alice选择另一个大随机数x,并计算A如下:A=gx mod n
(3)Alice将A发给Bob
(4)Bob选择另一个大随机数y,并计算B如下:B=gy mod n
(5)Bob将B发给Alice
(6)计算秘密密钥K1如下:K1=Bx mod n
(7)计算秘密密钥K2如下:K2=Ay mod n
K1=K2,因此Alice和Bob可以用其进行加解密。
尽管双方公开 A,B,因为 mod 运算不可逆,且有随机数,所有有破解难度。
证书签发机构(CA)
CA的工作流程
- 服务器 example.com将从CA请求TLS证书,例如 Digicert。
- Digicert将为example.com创建证书,证书将包含必要的数据,例如服务器名称,
服务器的公钥等。 - Digicert将创建数据(证书)的哈希值,并使用自己的私钥对其进行加密。
- 浏览器和操作系统自带Digicert等权威机构的公钥。
- 当浏览器收到签名证书时,它将使用公钥从签名生成哈希值,它还将使用证书中
指定的散列算法生成数据(证书)的散列,如果两个哈希值匹配,则签名验证成
功并且证书是可信的。 - 现在浏览器可以使用证书中指定的example.com的公钥继续进行身份验证过程。
在这里,我们可以将Digicert称为 Root CA.
浏览器如何验证服务器证书的有效性
- 证书颁发机构是为服务器创建并签署证书,很少有组织从事这项工作,即Digicert,Geotrust,Comodo等。如果他们正在为所有服务器签署证书,则必须为所有签名使用相同的私钥,如果它被盗,那么所有的信任都会丢失。为了解决这个问题并增加更多的平均信息量,引入了中间CA(intermediate CA)的概念。
- 服务器使用中级证书颁发机构的签名,因此,在与浏览器通信时,服务器将共享两个证书:
- 包含服务器的公钥,即实际的服务器证书;
- 由 Root CA 颁发的 intermediate CA 证书。
- 在签名验证期间,浏览器首先使用已经存储在浏览器中的Root CA的公钥来验证中间证书的数字签名,如果成功,浏览器现在可以信任中间证书及其公钥。现在使用此公钥,浏览器将验证原始服务器证书的签名,该组织可以注册为intermediate CA,以便为其域签署证书。
SSL/TLS协议
- 传输层安全性协议(Transport Layer Security - TLS),及其前身安全套接层(Secure Sockets Layer - SSL)是一种安全协议,目的是为互联网通信提供安全及数据完整性保障。
- HTTPS协议的安全性由SSL协议实现,当前使用的TLS协议 1.2 版本包含了四个核心子协议:握手协议、密钥配置切换协议、应用数据协议及报警协议。
- TLS适用于对称密钥
- 对称密钥可以通过安全密钥交换算法共享
- 如果请求被截获,密钥交换可能会被欺骗
- 使用数字签名进行身份验证
- 证书颁发机构和信任链。
- HTTPS协议、SSL协议、TLS协议、握手协议的关系
- HTTPS是Hypertext Transfer Protocol over Secure Socket Layer的缩写,即HTTP over SSL,可理解为基于SSL的HTTP协议。HTTPS协议安全是由SSL协议实现的。http https 互不相同。
- SSL协议是一种记录协议,扩展性良好,可以很方便的添加子协议
- 握手协议是SSL协议的一个子协议。
- TLS协议是SSL协议的后续版本,本文中涉及的SSL协议默认是TLS协议1.2版本。
HTTPS协议分析
TLS 握手的步骤:
HTTPS协议分析
- ClientHello:客户端发送所支持的 SSL/TLS 最高协议版本号和所支持的加密算法集合及压缩方法集合等信息给服务器端。
- ServerHello:服务器端收到客户端信息后,选定双方都能够支持的 SSL/TLS 协议版本和加密方法及压缩方法,返回给客户端。
以上双方互通一下,确定基本信息。 - SendCertificate(可选):服务器端发送服务端证书给客户端。
- RequestCertificate(可选):如果选择双向验证,服务器端向客户端请求客户端证书。
- ServerHelloDone:服务器端通知客户端初始协商结束。
- ResponseCertificate(可选):如果选择双向验证,客户端向服务器端发送客户端证书。
- ClientKeyExchange:客户端使用服务器端的公钥,对客户端公钥和密钥种子进行加密,再发送给服务器端。
- CertificateVerify(可选):如果选择双向验证,客户端用本地私钥生成数字签名,并发送给服务器端,让其通过收到的客户端公钥进行身份验证。
- CreateSecretKey:通讯双方基于密钥种子等信息生成通讯密钥。
- ChangeCipherSpec:客户端通知服务器端已将通讯方式切换到加密模式。
- Finished:客户端做好加密通讯的准备。
- ChangeCipherSpec:服务器端通知客户端已将通讯方式切换到加密模式。
- Finished:服务器做好加密通讯的准备。
- Encrypted/DecryptedData:双方使用客户端密钥,通过对称加密算法对通讯内容进行加密。
- ClosedConnection:通讯结束后,任何一方发出断开 SSL 连接的消息
密钥种子:大随机数。
客户端公钥:素数
通讯密钥:mod 运算
经济舱机票怎么定价
who
民航局规定
计算
价格:log(150, 航线距离 * 0.6) * 航线距离 * 1.1
每个航空公司还有一定的涨价空间。
原本的计算方式
里程 * 0.75
对比
航空成本,其实是按照里程来递减的
因此引入 log 函数
nginx
反向代理
nginx 根据不同的正则采取不同的匹配策略,
如图片结尾的文件走文件服务器
动态页面走web服务器
负载均衡
nginx 提供的负载均衡策略有两种:
- 内置策略:
- 扩展策略
web 缓存
配置详解
默认分块
#user nobody;
# nginx 全局块
worker_processes 1;
# events 块
events {
worker_connections 1024;
}
# http 块
http {
# http 全局块
server {
# server 块
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
# location 块
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
- 全局块:全局的指令,nginx 服务器的用户组,nginx 进程pid存放路径,日志存放路径,配置文件引入
- events:nginx 服务器与用户的连接,每个进程最大连接数,选取哪种事件驱动模型处理连接请求,是否允许同时接受多个网络连接,开启多个网络连接序列化。
- http:嵌套多个 server 配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置,如文件引入,mime-type定义,日志自定义,连接超时
- server:配置虚拟主机的相关参数
- location:配置请求的路由
负载均衡
upstream balance {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}
location = /balance {
proxy_pass http://balance;
}
- backup:热备 其他服务器出错的时候才会启用热备服务器
server 192.168.10.121:3333 backup;
- 轮询的时候默认权重都为 1
- 加权轮询,设置不同的 权重
upstream mysvr {
server 127.0.0.1:7878 weight=1;
server 192.168.10.121:3333 weight=2;
}
- ip hash 相同的客户端请求都会在同一服务器上
upstream mysvr {
server 127.0.0.1:7878;
server 192.168.10.121:3333;
ip_hash;
}
文件路径配置
root alias
- alias: location 的匹配和 alias 指定的目录是重叠的
- root:location 的匹配 会在 root 指定的目录之后追加
location /static/ {
alias /var/www/static/;
}
location /static/ {
root /var/www/;
}
两者都能匹配:/var/www/static/
location 语法
syntax: location [=|~|~*|^~] /uri/ { ... }
可以使用 字符串 和 正则表达式 来配置它
- 使用正则表达式,必须使用前缀 ~* 进行不区分大小写匹配 ~ 进行区分大小写的匹配
- 匹配顺序:
- 检查 字符串
- 按照配置文件中定义的顺序检查正则表达式,找到 则 停止
- 没找到 使用 字符串 的结果
- 有两种方法可以改变这种行为
- 使用 = 它只匹配一个完全相同的请求。如果请求匹配,则停止搜索,并立即处理请求。
- 使用前缀 ^~,与 字符串一起使用并告诉 nginx 如果提供的路径匹配,则不要检查正则表达式
优先级:
- location = 完整路径
- location ^~ 路径
- location 正则顺序
- location 部分起始路径
- /
BFF 浅析
单体应用
比如传统的 MVC:
- 前后端严重依赖
- 前端无法单独调试,不可避免遇到后端代码
前后端分离:半分离
SPA:html,css,js 放在 CDN 上,通过 ajax 和后端交互
缺点也有:
- SEO
- 复杂业务可能会有多次 http 请求
前后端分离:彻底分离
前端:负责view / controller
后段:负责 Model 层,业务数据处理
后端程序的单端调用,多端调用
单端:
提供接口给单独的一方使用,比如供 web 使用,有业务针对性;
多端:
接口同时给 web,移动端使用;
不同端,业务不同;
每一个端的接口复用度不会太高
BFF
在传统的 客户端 服务端之间 加入一个 BFF (Backend For Frontend)服务于前端的后端。
BFF 会预先聚合前端需要的数据,
BFF 和 传统后端,都会在一个机架上,之间的通信可以达到 理论上限。
- 访问控制
例如,服务中的权限控制,将所有服务中的权限控制集中在 BFF 层,使下层服务更加纯粹和独立。
- 应用缓存
项目中时常存在一些需要缓存的临时数据,此时 BFF 作为业务的汇聚点,距离用户请求最近,遂将该缓存操作放在 BFF 层。
- 第三方入口
在业务中需要与第三交互时,将该交互放在 BFF 层,这样可以只暴露必要信息给第三方,从而便于控制第三方的访问。
但是:
非必要,莫新增
引入新的分层,增加复杂度
微服务
- 属于架构层面的设计模式
- 设计概念以业务功能为主
- 提供对应的业务功能
- 约等于:模块化开发 + 分布式计算
进程 线程
进程
计算机资源(内存, CPU)进行调度的基本单位,
程序运行的时候,就为其创建一个进程,并为它分配资源,
把一个进程放入进程就绪队列,
等待进程调度器选中的时候就为他分配 cpu 时间,程序开始运行
线程
线程是程序执行时的最小单位,
是 CPU 调度和分派的基本单位,
一个进程可以由多个 线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量,
线程由 cpu 独立调度执行
区别
- 进程是 资源分配的 最小单位, 线程是 程序执行的最小单位
- 进程 有自己的 独立地址空间,每启动一个进程 就会为它分配地址空间, 这个操作非常昂贵
而线程共享进程中的数据,具有相同的地址空间,因此 CPU 切换一个线程的开销远比进程要小得多。 - 线程通信更加方便,共享全局变量,静态变量等数据,而进程之间的通信,通常以通信的方式(IPC) 进行。
- 但是多进程比多线程更加强壮,多线程一个线程挂掉,整个进程也就第死掉了,而进程,一个进程死掉,不会对另外的进程造成影响。
node
单线程
单线程就是一个进程只开一个线程
const http = require('http');
const longComputation = () => {
let sum = 0;
for (let i = 0; i < 1e10; i++) {
sum += i;
};
return sum;
};
const server = http.createServer();
server.on('request', (req, res) => {
if (req.url === '/compute') {
console.info('计算开始',new Date());
const sum = longComputation();
console.info('计算结束',new Date());
return res.end(`Sum is ${sum}`);
} else {
res.end('Ok')
}
});
server.listen(3000);
//打印结果
//计算开始 2019-07-28T07:08:49.849Z
//计算结束 2019-07-28T07:09:04.522Z
一些说明
- Node.js 虽然是单线程模型,但是其基于事件驱动、异步非阻塞模式,可以应用于高并发场景,避免了线程创建、线程之间上下文切换所产生的资源开销。
- 当你的项目中需要有大量计算,CPU 耗时的操作时候,要注意考虑开启多进程来完成了。
- Node.js 开发过程中,错误会引起整个应用退出,应用的健壮性值得考验,尤其是错误的异常抛出,以及进程守护是必须要做的。
- 单线程无法利用多核CPU,但是后来Node.js 提供的API以及一些第三方工具相应都得到了解决,文章后面都会讲到。
开启多进程不是为了解决高并发,主要是解决了单进程模式下 Node.js CPU 利用率不足的情况,充分利用多核 CPU 的性能
process 模块
Node.js 中的进程 Process 是一个全局对象,无需 require 直接使用,给我们提供了当前进程中的相关信息。官方文档提供了详细的说明,感兴趣的可以亲自实践下 Process 文档。
- process.env:环境变量,例如通过 process.env.NODE_ENV 获取不同环境项目配置信息
- process.nextTick:这个在谈及 Event Loop 时经常为会提到
- process.pid:获取当前进程id
- process.ppid:当前进程对应的父进程
- process.cwd():获取当前进程工作目录,
- process.platform:获取当前进程运行的操作系统平台
- process.uptime():当前进程已运行时间,例如:pm2 守护进程的 uptime 值
- 进程事件:process.on(‘uncaughtException’, cb) 捕获异常信息、process.on(‘exit’, cb)进程推出监听
- 三个标准流:process.stdout 标准输出、process.stdin 标准输入、process.stderr 标准错误输出
- process.title 指定进程名称,有的时候需要给进程指定一个名称
Node.js 进程创建
child_process
反向代理
反向代理
客户端需要访问服务器
先到达 代理服务器转发到:
服务器1
服务器2
服务器3
(代理服务器和其他服务器存在同一 LAN)
用途
- 加密/SSL 加速
- 负载均衡
- 缓存静态资源
- 压缩
- 减速上传(百度网盘)
- 安全 (增加攻击成本,堡垒机监测)
- 外网发布 (反向代理统一提供端口)
Nginx
worker_processes 2
proxy_pass: 对应的 stream 块
proxy_set_header
proxy_http_version
proxy_set_header
正向代理
客户端1
客户端2 ...
所有客户端需要访问网络就必须通过
代理服务器(唯一的出口)转发请求/响应,代理服务器还可以缓存请求响应
到达外网
(客户端和代理服务器存在同一 LAN)
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.