bfcache

往返缓存 bfcache

bfcache 是内存缓存,可在用户离开网页时存储网页(包括 JavaScript 堆)的完整快照。将整个页面保存在内存中后,当用户决定返回时,浏览器就可以快速轻松地恢复该页面。

  • 暂停执行 JavaScript 任务队列中的所有待处理任务,从 bfcache 中恢复时继续执行
  • 监听 bfcache
    • 使用 pageshow 和 pagehide 事件
  • 阻止浏览器进行 bfcache 优化
    • 使用 unload 事件
    • 使用 Cache-Control: no-store, 可以使用 Cache-Control: no-cacheCache-Control: max-age=0 替代
    • 使用 window.opener 或 window.postMessage() 进行跨页面引用
  • 推荐用法
    • 在 pagehide 时,关闭 fetch/xhr 请求,indexedDB 连接,WebSocket/WebRTC 连接,在 pageshow 时重新连接
    • 确实部署前在 devtools 中测试

搜索技巧

搜索技巧

搜信息咨询

  • google
    • 搜索语法:双引号,intitle, allintitle, intext, inurl, site, imagesize, filetype
  • 油管
  • 微信公众号 -> 搜狗搜索
  • AI, chatgpt

搜索来源

  • 信息源头
  • 行业垂直网站
  • 确定目的内容形式搜索对应文件格式
  • 不知道哪些网站有我想要的内容,先搜索 best xxx ten site 找到相关网站

199it.com 行业报告

similarsites 寻找同类网站

CSS colors

CSS colors

css 支持使用多种语法描述颜色

  • keywords
  • hex codes
  • 使用 16 进制表示 #RRGGBB
  • color function
    • rgb/rgba
      • rgb() 前三个参数分别指定 red, green, blue 通道,最后一个参数指定 alpha 通道
    • hsl/hsla
      • hsl() 的三个参数分别是色调、饱和度和明度,最后一个参数指定 alpha 通道
    • hwb
      • hwb() 的三个参数分别是色调、白度和黑度,最后一个参数指定 alpha 通道
    • lch
    • lab
    • color
      • 指定预定义的颜色空间
      • color(srgb …)
      • color(display-p3 …)

sRGB 色域可以用 16 进制, rgb/rgba, hsl/hsla, hwb 直接指定

1
2
3
4
5
// 不同用法表示同一个颜色
color: #ff0000;
color: rgb(255 0 0 / 0.5);
color: hsl(0 100% 50%);
color: hwb(0 0% 0%);

将 HSL 颜色转为 sRGB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @param {number} hue - Hue as degrees 0..360
* @param {number} sat - Saturation in reference range [0,100]
* @param {number} light - Lightness in reference range [0,100]
* @return {number[]} Array of RGB components 0..1
*/
function hslToRgb(hue, sat, light) {
hue = hue % 360;

if (hue < 0) {
hue += 360;
}

sat /= 100;
light /= 100;

function f(n) {
let k = (n + hue / 30) % 12;
let a = sat * Math.min(light, 1 - light);
return light - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
}

return [f(0), f(8), f(4)];
}

将 sRGB 颜色转为 HSL

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
/**
* @param {number} red - Red component 0..1
* @param {number} green - Green component 0..1
* @param {number} blue - Blue component 0..1
* @return {number[]} Array of HSL values: Hue as degrees 0..360, Saturation and Lightness in reference range [0,100]
*/
function rgbToHsl(red, green, blue) {
let max = Math.max(red, green, blue);
let min = Math.min(red, green, blue);
let [hue, sat, light] = [NaN, 0, (min + max) / 2];
let d = max - min;

if (d !== 0) {
sat = light === 0 || light === 1 ? 0 : (max - light) / Math.min(light, 1 - light);

switch (max) {
case red:
hue = (green - blue) / d + (green < blue ? 6 : 0);
break;
case green:
hue = (blue - red) / d + 2;
break;
case blue:
hue = (red - green) / d + 4;
}

hue = hue * 60;
}

return [hue, sat * 100, light * 100];
}

monorepo & turbo

将多个项目存放在同一个 git 仓库中,将项目放入不同的工作区,各个工作区之间互相引用模块,实现代码共享。

monorepo 文件结构

前端项目使用包管理器管理 monorepo。

1
2
3
4
5
6
7
8
9
my-monorepo
├─ docs
├─ apps
│ ├─ pc
│ └─ mobile
├─ packages
│ ├─ tsconfig
│ └─ shared-utils
└─ root-files

如果将docsappspackages 下的所有目录设为工作区,需要在根目录如下设置

npm

在根目录的 package.json 文件中配置 workspaces 字段。在workspaces 中配置工作区列表。

1
2
3
4
5
{
"name": "my-monorepo",
"version": "1.0.0",
"workspaces": ["docs", "apps/*", "packages/*"]
}

yarn

在根目录的 package.json 文件中配置 workspaces 字段。在workspaces 中配置工作区列表。

1
2
3
4
5
{
"name": "my-monorepo",
"version": "1.0.0",
"workspaces": ["docs", "apps/*", "packages/*"]
}

pnpm

在根目录的 pnpm-workspace.yaml 文件中配置工作区列表。

1
2
3
4
packages:
- 'docs'
- 'apps/*'
- 'packages/*'

添加工作区依赖

在某个工作区将另一个工作区作为依赖项,需要在其package.json 指明:

npm

1
2
3
4
5
{
"dependencies": {
"shared-utils": "*"
}
}

yarn

1
2
3
4
5
{
"dependencies": {
"shared-utils": "*"
}
}

pnpm

1
2
3
4
5
{
"dependencies": {
"shared-utils": "workspace:*"
}
}

原理解释

在根目录执行npm install 后,npm 会在根目录和每个工作区下载依赖包到 node_modules , 当遇到工作区依赖后,会将工作区符号链接node_modules, 所以可以将其当做普通依赖一样正常导入。

使用 Turborepo 管理 monorepo

Turborepo 是一个在包管理器提供的monorepo 管理功能之上,提供了更方便并且性能更好的构建系统。

使用 Turborepo只要在根目录安装turbo, 同时在根目录下添加turbo.json 即可开始使用

1
2
3
4
5
6
7
8
{
"pipeline": {
"build": {
"dependsOn": ["^build"]
},
"lint": {}
}
}

当运行  turbo lint  时,Turborepo 会查看每个工作区中的每个  lint  脚本并运行它。

turbo.json 主要配置

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
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
// 全局环境变量
"globalEnv": ["GITHUB_TOKEN"],
// 任务管道
"pipeline": {
// 任务名称,会去匹配工作区的同名任务
"build": {
// 此任务所依赖的任务列表, 执行此任务前,会先执行依赖的任务
// ^ 前缀表示,此任务取决于工作区的依赖关系
// ^build 表示依赖的工作区都执行完 build 之后,再执行此任务
// 下面的配置表示,执行 turbo build 时,会先执行 turbo lint
// 和依赖项中的 build 任务
"dependsOn": ["lint", "^build"],
// 缓存输出文件
"outputs":["dist/**"],
// 是否缓存 outputs, 默认 true
"cache": true,
// 只在匹配的文件发生更改时重新运行任务
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"],
// 标记长时间运行的任务,比如 dev
"persistent": true,
}
}
}

浏览器存储

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com; path=/; secure
Other-header: other-header-value

这里创建的 cookie 对所有 wrox.com 的子域及该域中的所有页面有效(通过 path=/ 指定)。不过,这个 cookie 只能在 SSL 连接上发送,因为设置了 secure 标志。 要知道,域、路径、过期时间和 secure 标志用于告诉浏览器什么情况下应该在请求中包含 cookie。 这些参数并不会随请求发送给服务器,实际发送的只有 cookie 的名/值对。

SameSite

  • Non**e**。浏览器会在同站请求、跨站请求下继续发送 cookies,不区分大小写。

  • **Strict****。**浏览器将只在访问相同站点时发送 cookie。(在原有 Cookies 的限制条件上的加强,如上文 “Cookie 的作用域” 所述)。

  • **Lax**与 **Strict** 类似,但用户从外部站点导航至 URL 时(例如通过链接)除外。 在新版本浏览器中,为默认选项,Same-site cookies 将会为一些跨站子请求保留,如图片加载或者 frames 的调用,但只有当用户从外部站点导航到 URL 时才会发送。如 link 链接。

未设置 SameSite, 主流浏览器默认设为 Lax。

JS 想要操作 cookie 只能通过 BOM 接口 document.cookie 操作,并且需要 URL 编码(可以用 decodeURIComponent),一般都是封装下方法再使用。

如果 cookie 设置了 HttpOnly, 则前端无法操作。

限制

  • 不超过 300 个 cookie;

  • 每个 cookie 不超过 4096 字节;

  • 每个域不超过 20 个 cookie;

  • 每个域不超过 81920 字节。

Storage

方法

  • clear():删除所有值。

  • getItem(name):取得给定 name 的值。

  • key(index):取得给定数值位置的名称。

  • removeItem(name):删除给定 name 的名/值对。

  • setItem(name, value):设置给定 name 的值。

sessionStorage

使用 sessionStorage 存储的数据只在当前会话有效。有效的页面:

  • 设置 sessionStorage 的页面页面1
  • 页面1打开的页面

如果在地址栏敲入新的页面,即使是同一个页面,也无法获取到 sessionStorage。

localStorage

满足域、端口和协议都相同的页面可以获取到同一个 localSrtorage。

区别

localStorage: 存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存。localStorage 数据不受页面刷新影响,也不会因关闭窗口、标签页或重新启动浏览器而丢失。

sessionStorage: sessionStorage 中的数据不受页面刷新影响,会在关闭标签页后丢失。

IndexedDB

todo

Cache Storage API

Cache 这个 API 是针对 Request Response 的。Cache 一般结合 Service Worker 使用,因为请求级别的缓存与具有页面拦截功能的 Service Worker 最配。

创建或打开一个命名空间

// 不存在则创建
const myCache = await caches.open(‘myCache’);

添加缓存

通过 add/addAll 方法添加,调用 add 会类似 fetch 一样发送请求,并将响应放到 Cache Storage 里面。

在 put 方法里传一个 Response 也可以实现添加缓存。

// 参数同 fetch
myCache.add(‘/test-url’);

// 使用 put
fetch(url).then(function (response) {
if (!response.ok) {
throw new TypeError(‘bad response status’);
}
return cache.put(url, response);
})

读取

通过 match 或 matchAll 方法读取。

// 参数同 fetch, 可以是 URL 地址,也可以是 Request 对象
const res = await myCache.match(“/subscribe”);
// const res = await myCache.matchAll(“/subscribe”);

更新

通过 add 或 put 方法更新。

const request = new Request(“/subscribe”);
const fetchResponse = await fetch(request);
myCache.put(request, fetchResponse);

销毁

使用 delete 方法删除某个路径的缓存或者直接删除整个命名空间。

myChache.delete(‘/subscribe’);
caches.delete(‘myChache’);

使用场景

在 service worker (在全局环境里也能用) 中用来缓存 js/css/img 文件或者不经常更新的接口,加快请求或者提供离线访问能力。

babel plugin-transform-runtime 配置和编译结果差异

准备源代码

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
// module.js
const foo = () => {
return new Promise(resolve => resolve(1));
};
export const bar = async () => {
await foo();
};
export const far = (...rest) => {
console.log(rest.map(item => item?.aa));
};

// index.js
import { bar, far } from './module';

function* gen() {
const y1 = yield 1;
return y1;
}
const bar1 = async () => {
await far();
};
const p1 = (...rest) => {
return new Promise(resolve => {
resolve(...rest);
});
};

配置 1 : 仅使用 @babel/env

1
2
3
4
5
6
7
8
9
10
11
{
"presets": [
[
"@babel/env",
{
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
]
}

编译结果 1

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
// module.js
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.far = exports.bar = void 0;

require("regenerator-runtime/runtime.js");

require("core-js/modules/es.object.to-string.js");

require("core-js/modules/es.promise.js");

require("core-js/modules/es.array.map.js");

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

var foo = function foo() {
...
};

var bar = /*#__PURE__*/function () {
...
}();

exports.bar = bar;

var far = function far() {
...
};

exports.far = far;

// index.js
"use strict";

require("core-js/modules/es.object.to-string.js");

require("core-js/modules/es.promise.js");

require("regenerator-runtime/runtime.js");

var _module = require("./module");

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

var _marked = /*#__PURE__*/regeneratorRuntime.mark(gen);

function gen() {
...
}

var bar1 = /*#__PURE__*/function () {
...
}();

var p1 = function p1() {
...
};

从结果来看,module.js 和 index.js 都从 core-js 引入了自身需要的 polyfill (全局变量),针对 async/await 分别生成了同样的 asyncGeneratorStep 和 _asyncToGenerator

配置 2 : 使用 @babel/env 和 @babel/plugin-transform-runtime (默认配置)

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
[
"@babel/env",
{
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
],
"plugins": [["@babel/plugin-transform-runtime"]]
}

编译结果 2

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
// module.js
"use strict";

var \_interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "\_\_esModule", {
value: true
});
exports.far = exports.bar = void 0;

var \_regenerator = \_interopRequireDefault(require("@babel/runtime/regenerator"));

var \_asyncToGenerator2 = \_interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
require("core-js/modules/es.object.to-string.js");

require("core-js/modules/es.promise.js");

require("core-js/modules/es.array.map.js");

var foo = function foo() {
...
};

var bar = /_#**PURE**_/function () {
...
}();

exports.bar = bar;

var far = function far() {
};

exports.far = far;

// index.js
"use strict";

var \_interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

require("core-js/modules/es.object.to-string.js");

require("core-js/modules/es.promise.js");

var \_asyncToGenerator2 = \_interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));

var \_regenerator = \_interopRequireDefault(require("@babel/runtime/regenerator"));

var \_module = require("./module");

var \_marked = /_#**PURE**_/\_regenerator.default.mark(gen);

function gen() {
...
}

var bar1 = /_#**PURE**_/function () {
...
}();

var p1 = function p1() {
...
};

添加 @babel/plugin-transform-runtime 后,对于 async/await 不再重复生成 asyncGeneratorStep 和 _asyncToGenerator,改为从 @babel/runtime 中引入, 其余 polyfill 依然从 core-js 引入

配置 3 使用 @babel/env 和 @babel/plugin-transform-runtime (配置 corejs, 需要安装 @babel/runtime-corejs3 依赖)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"presets": [
[
"@babel/env",
{
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}

编译结果 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
// module.js
"use strict";

var \_interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

Object.defineProperty(exports, "\_\_esModule", {
value: true
});
exports.far = exports.bar = void 0;

var \_regenerator = \_interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var \_asyncToGenerator2 = \_interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));

var \_promise = \_interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var \_map = \_interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/map"));

var foo = function foo() {
return new \_promise.default(function (resolve) {
return resolve(1);
});
};

var bar = /_#**PURE**_/function () {
...
}();

exports.bar = bar;

var far = function far() {
...
};

exports.far = far;

// index.js
"use strict";

var \_interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var \_promise = \_interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var \_asyncToGenerator2 = \_interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));

var \_regenerator = \_interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var \_module = require("./module");

var \_marked = /_#**PURE**_/\_regenerator.default.mark(gen);

function gen() {
...
}

var bar1 = /_#**PURE**_/function () {
...
}();

var p1 = function p1() {
...
};

@babel/plugin-transform-runtime 添加配置 corejs: 3 后,所有的 polyfill 都改为从 @babel/runtime-corejs3 导入,同时由全局覆盖变为了局部变量,不会污染全局

结论:

@babel/presets 的 『”useBuiltIns”: “usage”』会引入模块中需要的 polyfill 并且是全局覆盖的,但是每个模块都会重复生成辅助函数
添加插件 『@babel/plugin-transform-runtime』 后,辅助函数不会在每个模块中重复生成,而是从 『@babel/runtime』引入
@babel/plugin-transform-runtime 进一步配置 『 “corejs”: 3 』后,所有的 polyfill 都从 『@babel/runtime-corejs3』引入,并且是局部变量,不会污染全局

HTTP 速览

HTTP 的标准是由 IETF 组织制定,跟它相关的标准:

HTTP Documentation (httpwg.org)

HTTP/1.1 相关

中文翻译: duoani/HTTP-RFCs.zh-cn: 翻译 HTTP 相关的 RFC (中英文对照) (github.com)

HTTP/2 相关

中文翻译: abbshr/rfc7540-translation-zh_cn: RFC 7540 - HTTP/2 中文翻译版 (github.com)

HTTP 协议是基于 TCP 协议实现的,在 TCP 的基础上规定了一个 Request-Response 的模式。这个模式就是客户端发送消息给服务端,服务端才能返回消息给客户端,服务端不能主动发送消息。

HTTP 协议格式

HTTP 协议大概可以划分成 Request 和 Response。

Request

Request 是客户端需要发送到服务端的数据,可以分成下面这三部分:

  • Request-Line(请求行)
  • method(方法)
  • URL
  • version(版本)
  • request-header(头)
  • message-body(实体)

具体的数据格式:

GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

sp 是空格,cr 是 \r , lf 是 \n

Response

Response 是服务端返回给客户端的数据,对应的也可以分成三部分:

  • Response Line(响应行)

  • version(版本)

  • status code(状态码)

  • status text(状态文本)

  • Response Header(响应头)

  • message-body(实体)

HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: “34aa387-d-1568eb00”
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

Hello World! My payload includes a trailing CRLF.

响应状态码

HTTP 2

HTTP2 是 HTTP1.1 的升级版,HTTP2 相对于 HTTP1.1 来说主要有下面几点的改进:

  1. 对 HTTP 的头进行压缩,对于相同的头只需要发送索引表中的索引。解决了 HTTP1.1 每次都要将 header 来回传送。
  2. 支持服务端推送。能够在客户端发送第一个请求时,提前把一部分内容推送给客户端。这可以避免客户端请求顺序带来的并行度不高,从而导致的问题。
  3. 支持 TCP 的连接复用。使用同一个 TCP 连接来传输多个 HTTP 请求,避免了 TCP 连接时的三次握手开销,和初建 TCP 连接时传输窗口小的问题。

Hooks 的 Capture Value 特性

Function Component 是更彻底的状态驱动抽象,甚至没有 Class Component 生命周期的概念,只有一个状态,而 React 负责同步到 DOM。

1
2
3
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

函数组件容易阅读和测试,没有状态或生命周期。因此可以让我们快速的写一个渲染 UI 的组件,这也与 React 推崇函数式编程的思想契合。

在 React v16.8 推出之前函数组件只能简单的渲染组件,没有自身的状态,Hooks 的到来改变了这一现状,它赋予函数组件更多的能力。

通过 useState 让函数组件拥有了自身的状态。useEffect 让函数组件能够执行包含副作用的行为。

Capture Value

首先说明 class 组件是不具备 Capture Value 特性的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ClassCount extends Component {
state = {
count: 0,
};

timer() {
setTimeout(() => {
console.log(`点击 3 秒后的 count: ${this.state.count}`);
}, 3000);
}

render() {
return (
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
this.timer();
}}
>
class 组件点击了 {this.state.count} 次
</button>
);
}
}

上面的代码中,每次点击后 timer 输出的值和当前组件 state 的值是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function FunctionCount() {
const [count, setCount] = useState(0);

const timer = () => {
setTimeout(() => {
// timer 打印的 count 永远是上一次 render 中的值
console.log(`function 组件 3 秒后的 count: ${count}`);
}, 3000);
};

return (
<button
onClick={() => {
setCount(count + 1);
timer();
}}
>
function 组件点击了 {count} 次
</button>
);
}

在函数组件里,每次点击后 timer 输出的值和当前组件中的 state 不一致,这是因为函数组件他就是一个普通函数,react 每次渲染都会重新执行一遍这个函数,这样就导致,对于函数内部来说每次渲染都有自己的 Props 与 State。

每次渲染都有自己的 Props 与 State

可以认为每次 Render 的内容都会形成一个快照并保留下来,因此当状态变更而 Rerender 时,就形成了 N 个 Render 状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Counter() {
const [count, setCount] = useState(0);

return (
<button
onClick={() => {
setCount(count + 1);
}}
>
function 组件点击了 {count} 次
</button>
);
}

当我们更新状态的时候,React 会重新渲染组件。每一次渲染都能拿到独立的 count 状态,这个状态值是函数中的一个常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
// react 第一次渲染时
function Counter() {
const count = 0;
return <div>{count}</div>;
}

// 点击触发 react 重新渲染,整个函数组件被重新调用了
function Counter() {
const count = 1;
return <div>{count}</div>;
}

// ...

React 仅仅只是在渲染输出中插入了 count 这个数字, 这个数字由 React 提供。当 setCount 的时候,React 会带着一个不同的 count 值再次调用组件。然后,React 会更新 DOM 以保持和渲染输出一致。

这里关键的点在于任意一次渲染中的 count 常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的 count 值独立于其他渲染。

每一次渲染都有它自己的事件处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter() {
const [count, setCount] = useState(0);

function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
);
}

当我们更新状态的时候,React 会重新渲染组件。每一次渲染都能拿到独立的 count 状态,这个状态值是函数中的一个常量。对于 handleAlertClick 也是这样

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
//  react 第一次渲染
function Counter() {
const count = 0;

function handleAlertClick() {
setTimeout(() => {
// count === 0
alert('You clicked on: ' + count);
}, 3000);
}

return (...);
}

// 点击触发 react 重新渲染,整个函数组件被重新调用了
function Counter() {
const count = 01;

// 创建了新的 handleAlertClick
function handleAlertClick() {
setTimeout(() => {
// count === 1
alert('You clicked on: ' + count);
}, 3000);
}

return (...);
}

// ...

React 每次渲染都会创建新的 handleAlertClick, 而对于 handleAlertClick 来讲,每次他访问到的 count 都是一个全新的常量。

在任意一次渲染中,props 和 state 是始终保持不变的。如果 props 和 state 在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的 count 值。

每次渲染都有它自己的 Effects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(`useEffect 执行, count: ${count}`);
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}

useEffect 和事件处理函数一样,每次渲染都被重新执行,传入 useEffect 的回调函数也被重新创建,所有内部引用的所有 state 都是独属于本次渲染的。

错误设置依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Counter4() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
// count 永远是 0
console.log('Counter4 setInterval', count);
setCount(count + 1);
}, 1000);

return () => clearInterval(id);
}, []);

return <h1>count: {count}</h1>;
}

上面的代码中的 setInterval 会持续执行,但是 count 的值保持不变,原因就是第一次执行时 setCount 触发重新渲染,但是由于 useEffect 的依赖是[],effect 不会再重新运行,它后面每一秒都会调用 setCount(0 + 1)

设置依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter5() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log('Counter5 useEffect');

const id = setInterval(() => {
// 每次都重新生成新的定时器
console.log('Counter5 setInterval');
setCount(count + 1);
}, 1000);

return () => clearInterval(id);
}, [count]);

return <h1>count: {count}</h1>;
}

上面的代码已经可以正确运行了,缺点是 count 每次改变都会重置 setInterval

仅指定行为,不依赖具体的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter6() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log('Counter6 useEffect');

const id = setInterval(() => {
// 仅指定行为,不依赖具体的值
console.log('Counter6 setInterval');
setCount(c => c + 1);
}, 1000);

return () => clearInterval(id);
}, []);

return <h1>count: {count}</h1>;
}

附 useState, useEffect 的简单实现

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
import { render } from '../index';

// hooks 存放在这个数组
let memoizedState = [];
// 当前 memoizedState 下标
let cursor = 0;

export function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;

const currentCursor = cursor;
function setState(newState) {
if (typeof newState === 'function') {
memoizedState[currentCursor] = newState(memoizedState[currentCursor]);
} else {
memoizedState[currentCursor] = newState;
}

render();
// 每更新一次都需要将_index归零,才不会不断重复增加 memoizedState
cursor = 0;
}

// cursor 加 1
cursor++;

// 返回当前 state
return [memoizedState[currentCursor], setState];
}

export function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true;
if (hasNoDeps || hasChangedDeps) {
setTimeout(callback);
memoizedState[cursor] = depArray;
}
cursor++;
}

webpack的模块化实现

源文件:a.js, b.js, c.js, d.js, index.js

展开源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// a.js
const a = {
x: 1
}
module.exports = a

// b.js
const b = {
x: 1
}
export default b

// c.js
export default {
x: 1
}

// d.js
const d = {
x: 1
}
export { d }

// index.js
import b from './b'
import c from './c'
import { d } from './d'
const a = require('./a')

console.log('a:', a)
console.log('b:', b)
console.log('c:', c)
console.log('d:', d)

就是在 index.js  中引入 [a, b, c, d]  等使用不同模块化方式的模块, 来看看 webpack 是怎么处理模块引用的

下面是 webpack 打包出来的 bundle.js 文件(省略了多余注释和没用到的声明)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
(function (modules) {
// 代码模块缓存, 将执行过的代码模块缓存起来
var installedModules = {};

// 实现模块化代码的主要函数, 通过以文件名为模块id(moduleId)递归调用
function __webpack_require__(moduleId) {
// 若模块已缓存, 则直接调用
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建新的模块代码, 并把模块缓存
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
});

// 执行对应模块id的代码, 并将模块对象和方法绑定到当前module上
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

// 将模块已加载标识置为true
module.l = true;

// 返回模块对外暴露方法
return module.exports;
}

// 模块化处理函数: 用于 export { aaa, bbb } , import { aaa } from './*
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};

// 挂载 _esModule 属性到 exports 上
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};

// 封装了 Object.prototype.hasOwnProperty.call
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};

// 代码启动, 传入入口文件的模块id
return __webpack_require__((__webpack_require__.s = './src/index.js'));
})({
'./src/a.js': function (module, exports) {
/**
* commonjs 模块
* 直接将导出的对象赋值为 module.exports
*/
eval('const a = {\n x: 1\n}\n\nmodule.exports = a\n\n//# sourceURL=webpack:///./src/a.js?');
},

'./src/b.js': function (module, __webpack_exports__, __webpack_require__) {
/**
* es6 默认导出模块 export default
* 会在 module.exports 属性挂载属性 _esModule = true
* 然后将导出对象挂载到 module.exports.default 上
*/
'use strict';
eval(
'__webpack_require__.r(__webpack_exports__);\nconst b = {\n x: 1\n}\n\n/* harmony default export */ __webpack_exports__["default"] = (b);\n\n//# sourceURL=webpack:///./src/b.js?'
);
},

'./src/c.js': function (module, __webpack_exports__, __webpack_require__) {
/**
* es6 模块
* 同 模块b
*/
'use strict';
eval(
'__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__["default"] = ({\n x: 1\n});\n\n//# sourceURL=webpack:///./src/c.js?'
);
},

'./src/d.js': function (module, __webpack_exports__, __webpack_require__) {
/**
* es6 命名导出模块 export const d = {}
* 会在 module.exports 属性挂载属性 _esModule = true
* 通过 getter 在访问 module.exports[name] 时返回导出对象
*/
'use strict';
eval(
'__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "d", function() { return d; });\nconst d = {\n x: 1\n}\n\n\n\n//# sourceURL=webpack:///./src/d.js?'
);
},

'./src/index.js': function (module, __webpack_exports__, __webpack_require__) {
/**
* 入口文件
* 通过 _webpack_require__() 得到导入对象,即 编译后的 module.exports 属性
* 对不同的模块化方式做不同的引入处理
*/
'use strict';
eval(
'__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b */ "./src/b.js");\n/* harmony import */ var _c__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./c */ "./src/c.js");\n/* harmony import */ var _d__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./d */ "./src/d.js");\n\n\n\nconst a = __webpack_require__(/*! ./a */ "./src/a.js")\n\nconsole.log(\'a\', a)\nconsole.log(\'b\', _b__WEBPACK_IMPORTED_MODULE_0__["default"], _b__WEBPACK_IMPORTED_MODULE_0__)\nconsole.log(\'c\', _c__WEBPACK_IMPORTED_MODULE_1__["default"], _c__WEBPACK_IMPORTED_MODULE_1__)\nconsole.log(\'d\', _d__WEBPACK_IMPORTED_MODULE_2__["d"], _d__WEBPACK_IMPORTED_MODULE_2__)\n\n//# sourceURL=webpack:///./src/index.js?'
);
},
});

整体代码流程就是创建一个自执行函数, 根据文件名称递归调用内部函数 __webpack_require__(moduleId) , 从而把代码模块化

node常用模块

fs(文件系统) 模块

fs 模块提供了一个 API,用于以模仿标准 POSIX 函数的方式与文件系统进行交互。

所有文件系统操作都具有同步和异步的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
读取文件;
const fs = require('fs');

// 异步读取
fs.readFile('./index.txt', 'utf8', (err, data) => {
console.log(data); // Hello Nodejs
});

// 同步读取
const data = fs.readFileSync('./index.txt', 'utf8');

console.log(data); // Hello Nodejs

// 创建读取流
const stream = fs.createReadStream('./index.txt', 'utf8');

// 这里fs.createReadStream用到了前面介绍的events eventEmitter.on() 方法来监听事件
stream.on('data', data => {
console.log(data); // Hello Nodejs
});

写入/修改文件
写入文件时,如果文件不存在,则会创建并写入,如果文件存在,会覆盖文件内容.

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');
// 异步写入
fs.writeFile('./write.txt', 'Hello Nodejs', 'utf8', err => {
if (err) throw err;
});
// 同步写入
fs.writeFileSync('./writeSync.txt', 'Hello Nodejs');
// 文件流写入
const ws = fs.createWriteStream('./writeStream.txt', 'utf8');
ws.write('Hello Nodejs');
ws.end();

删除文件/文件夹
删除文件

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
// 异步删除文件
fs.unlink('./delete.txt', err => {
if (err) throw err;
});

// 同步删除文件
fs.unlinkSync('./deleteSync.txt');
删除文件夹;
// 异步删除文件夹
fs.rmdir('./rmdir', err => {
if (err) throw err;
});

// 同步删除文件夹
fs.rmdirSync('./rmdirSync');
创建文件夹;
// 异步创建文件夹
fs.mkdir('./mkdir', err => {
if (err) throw err;
});

// 同步创建文件夹
fs.mkdirSync('./mkdirSync');
重命名文件 / 文件夹;
const fs = require('fs');

// 异步重命名文件
fs.rename('./rename.txt', './rename-r.txt', err => {
if (err) throw err;
});

// 同步重命名文件夹
fs.renameSync('./renameSync', './renameSync-r');
复制文件 / 文件夹;
const fs = require('fs');

// 异步复制文件
fs.copyFile('./copy.txt', './copy-c.txt', (err, copyFiles) => {
if (err) throw err;
});

// 同步复制文件夹
fs.copyFileSync('./null', 'null-c');
文件夹状态 - 文件 / 文件夹;
const fs = require('fs');

// 异步获取文件状态
fs.stat('./dir', (err, stats) => {
if (err) throw err;
// 是否是文件类型
console.log(stats.isFile()); // false
// 是否是文件夹类型
console.log(stats.isDirectory()); // true
});

// 同步获取文件状态
const stats = fs.statSync('./stats.txt');

// 是否是文件类型
console.log(stats.isFile()); // true
// 是否是文件夹类型
console.log(stats.isDirectory()); // false

events 模块

Node 是基于事件驱动的,其就是通过核心模块 Events 实现的。 Events 模块定义了 EventEmitter 类。实际上就是发布订阅模式。所有可能触发事件的对象都是继承自 EventEmitter 类的,其主要包含下面方法:

  • addEventListener
  • on
  • once
  • removeListener
  • removeAllListeners
  • setMaxListeners
  • emit

使用 EventEmitter
其他类需要触发事件等,都需要自行继承这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
let util = require('util')
let EventEmitter = require('events')

function OtherClass(path) {
EventEmitter.call(this)
}
util.inherits(OtherClass, EventEmitter)
newListener 事件
EventEmitter 实例还可以监听一个 newListener 事件,即每次给这个实例添加新的监听函数时,都会触发 newListener 事件。

this.on('newListener', function (type, listener) {
// 每当监听一个事件时,就执行某些行为
})

stream 模块

流(stream)是 Node.js 中处理流式数据的抽象接口。 stream 模块用于构建实现了流接口的对象。

使用 stream 的场景:

使用 fs.readFile 读取文件时会将数据整个读取到内存中再处理,当处理大文件时,会极大的展示资源,这时就是使用 stream 的时候了。

Stream 不会一次性的读取文件,而是分批次的读取适量的内容到缓存区进行操作,这样对内存的占用会少很多。

流的四种基本类型

Writable - 可写入数据的流(例如 fs.createWriteStream())。

Readable - 可读取数据的流(例如 fs.createReadStream())。

Duplex - 可读又可写的流(例如 net.Socket)。

Transform - 在读写过程中可以修改或转换数据的 Duplex 流(例如 zlib.createDeflate())。

缓冲

缓冲区有大小限制,由 highWaterMark 指定字节总数(对象模式下指定对象总数)。调用 stream.push(chunk)时,数据缓冲在可读流中,等待被消费,如果缓冲区的数据大小达到了 highWaterMark 指定的阈值,流会停止读取数据,直到当前缓冲区的数据被消费。

用于消费的 api

使用继承自 events 的 emitter api 来通讯状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const file = fs.createReadStream('./test.txt', {
highWaterMark: 2,
flags: 'r',
encoding: 'utf8',
});

file.on('open', a => {
console.log('open', a);
});

file.on('data', chunk => {
console.log('data', chunk);
});

file.on('end', chunk => {
console.log('end', chunk);
});

file.on('close', a => {
console.log('close', a);
});
// open -> data -> data -> ... -> end -> close

(异步)事件触发顺序 open -> data -> data -> … -> end -> close

URL 模块

url 模块用于处理与解析 URL。

URL 字符串是结构化的字符串,包含多个含义不同的组成部分。 解析字符串后返回的 URL 对象,每个属性对应字符串的各个组成部分。

url 模块提供两种 api 来处理 url。(两种 api 的处理结果不同,详细)

const url = require(‘url’)
// WHATWG 标准
const myUrl = new url(‘https://baidu.com:8080/a/b/c?q=str#hash')
// 遗留 api
const myUrl2 = url.parse(‘https://baidu.com:8080/a/b/c?q=str#hash')
以下介绍的都是 WHATWG 标准解析出的对象

URL 对象的属性
const url = require(“url”);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const url = require("url");

const myURL = new url("https://github.com/webfansplz#hello");
console.log(myURL);
{
href: 'https://github.com/webfansplz#hello', // 序列化的 URL
origin: 'https://github.com', // 序列化的 URL 的 origin
protocol: 'https:', // URL 的协议
username: '', // URL 的用户名
password: '', // URL 的密码
host: 'github.com', // URL 的主机
hostname: 'github.com', // URL 的主机名
port: '', // URL 的端口
pathname: '/webfansplz', // URL 的路径
search: '', // URL 的序列化查询参数
searchParams: URLSearchParams {}, // URL 查询参数的 URLSearchParams 对象
hash: '#hello' // URL 的片段
}

URL 对象属性 除了 origin 和 searchParams 是只读的,其他都是可写的.

  1. 序列化 URL
1
2
3
4
5
6
7
8
9
const { URL } = require('url');

const myURL = new URL('https://github.com/webfansplz#hello');

console.log(myURL.href); // https://github.com/webfansplz#hello

console.log(myURL.toString()); // https://github.com/webfansplz#hello

console.log(myURL.toJSON()); // https://github.com/webfansplz#hello