Node.js v21

Node.js 21

  • 内置 Websocket 客户端, 使用 --experimental-websocket 标志启用

  • writeFile 文件系统函数添加 flush 可选项。

    • 启用该选项,会在成功写入数据时强制同步刷新数据到永久存储(之前不会立即刷新到永久储存,从而使读取操作获取旧数据)
    • 已添加 flush 选项的函数
      • filehandle.createWriteStream
      • fsPromises.writeFile
      • fs.createWriteStream
      • fs.writeFile
      • fs.writeFileSync
  • 添加全局 navigator, 目前 navigator.hardwareConcurrency 这一个对象

  • Object 和 Map 添加了静态方法 groupBy()

  • 使用 --experimental-default-type 将默认模块类型设置为 ESM

Node.js 21.1.0

  • 新增 --experimental-detect-module 检测没有在 packge.json 标明 type 同时文件后缀不是 .mjs/.cjs 的普通 js 文件是否是 ES 模块(推荐所有项目都添加 type 字段,即使是 commonjs 模块)
  • filesystem appendFile 添加 flush option

Next.js App Router

Next.js 13.4

  • App Router (Stable)

    • 使用文件系统作为 API

      • ```js
        // app/layout.js

        // New: App Router ✨
        // The root layout is shared for the entire application
        export default function RootLayout({ children }) {
        return (

        <html lang="en">
          <body>{children}</body>
        </html>
        

        );
        }

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16

        - 自由导入任何 CSS 文件

        - 与 React Suspense 深度集成,getServerSideProps 不再阻止交互

        - 默认所有组件都是 React Server 组件

        - ```js
        export default async function Page() {
        const res = await fetch('https://api.example.com/...');
        // The return value is *not* serialized
        // You can use Date, Map, Set, etc.
        const data = res.json();

        return '...';
        }
    • 默认将页面标记为 transitions,使路由转换能够中断

    • 自动代码分割

      • ```js
        import { getUser } from ‘./auth’;
        import { Dashboard, Landing } from ‘./components’;

        export default async function Layout() {
        const isLoggedIn = await getUser();
        // isLoggedIn 是 false 时, 不会被传输到客户端
        return isLoggedIn ? : ;
        }

        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

        - Turbopack (Beta): 使用 `next dev --turbo` 启用

        - Server Actions (Alpha)

        - [Server Action 的背后原理,没有魔法,只是简单函数](https://zenn.dev/cybozu_frontend/articles/server-actions-deep-dive)

        - 在服务端执行任何操作,和客户端代码轻松集成,不需要再写 api 代码了

        ```js
        import kv from './kv';

        // 集成到 form
        export default function Page({ params }) {
        async function increment() {
        'use server';
        await kv.incr(`post:id:${params.id}`);
        }

        return (
        <form action={increment}>
        <button type="submit">Like</button>
        </form>
        );
        }
    • 自定义调用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      'use client';

      import { useTransition } from 'react';
      import { myAction } from './action';

      export function MyComponent() {
      const [_, startTransition] = useTransition();

      const handleClick = () => {
      startTransition(async () => {
      await myAction();
      });
      };

      return <button onClick={handleClick}>Custom Invocation</button>;
      }

在 Web 中渲染 Word 文档

在 Web 中渲染 Word 文档

  • 使用 office 厂商提供的服务,微软 office online, WPS 等
  • 转为 PDF
  • 前端解析直接解析 .docx 文件并渲染

文件解析

文档格式转换

版式布局

  • 跟随 word 展示效果,采用分页渲染
  • word 单元格转为 html table
  • 形状使用 svg 模拟
  • 公式使用 XSLTProcessor 将 Office Math 转为 MathML

浏览器存储

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』引入,并且是局部变量,不会污染全局

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++;
}

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

初识MongoDB

数据库分类

关系型

SQL Server MySQL Access ORACLE

数据库 –> 表 –> 记录

非关系型

MongoDb 文档型的非关系数据库

安装

官网下载合适版本
不停的下一步,下一步
增加环境变量,以便直接在 shell 中使用
数据库 –> 集合 –> 文档 database –> collection –> document

存储格式 BSON 类似于 JSON

每一个 {}称为一条文档

{ ​ “name” : “小明”, ​ “age” : 18, ​ “hobby”: [“睡觉”, “吃饭”] }

命令操作

创建数据库

mongod:
mongod --dbpath dir # 打开或新建一个数据库 # mongod --dbpath E:\mongodb\mydb
mongo:
use dbname # 新建叫做 dbname 的数据库, 同时进入 dbname
初步操作

1
2
3
4
5
6
7
show dbs # 查看所有的数据库
show collections # 查看当前库下所有的 集合

## 在叫做 collectionName 的集合中插入一条文档,如果集合不存在,则新建该集合
db.collectionName.insert(obj)
## 查找名为 collectionName 集合的所有文档
db.collectionName.find()

导入数据

假设 C:\user\db\test.json 是 1000 条文档

1
2
3
4
5
6
7
8
mongoimport --db test --collection user --drop --file C:\user\db\test.json

## 以上操作会把 test.json 里的文档全部导入到 数据库 test -> 集合 user 中

--db 导入到哪个库
--collection 导入到哪个集合
--drop 清空集合中原有文档
--file 要导入的文件路径

CRUD

假设集合名叫 user

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
## 查询所有文档
db.user.find()

# 查询 k 的值为 v 的所有文档
db.user.find({k: v})

# and 操作
# 查询 k1 的值为 v1 且 k2 的值为 v2 的所有文档
db.user.find({k1: v1, k2: v2})

# or 操作
# 查询 k1 的值为 v1 或 k2 的值为 v2 的所有文档
db.user.find({$or: [{k1: v1}, {k2: v2}]})

# 比较(大于,小于)
db.user.find({k: {$gt: v}}) # 查询 k 的值大于 v 的文档
db.user.find({k: {$lt: v}}) # 查询 k 的值小于 v 的文档
# $gt 大于
# $lt 小于

# 组合
db.user.find({k: {$gt: v1, $lt: v2}}) # 查询 k 的值大于 v1 同时小于 v2 的文档
# 更新
db.user.update(
{k: v1}, # 查询条件
{
$set: {k: v2} # 把查到的 k 改为 v2
}
)
# 更新详细语法
db.user.update(
<query>, # 查询条件
<update>, # 更新方式,如 $set, $inc 等
{
multi: <boolean>, # (可选)默认为 false,只匹配找到的第一条, 如果为 true, 所有满足条件的都匹配
}
)
# 删除
db.dropDatabase() # 删除当前所在数据库
db.user.drop() # 删除名为 user 的集合
db.user.remove({k: v}) # 删除匹配到的所有 k 值为 v 的文档
db.user.remove({k: v}, {justOne: true}) # 删除第一个匹配到的所有 k 值为 v 的文档
db.user.remove({}) # 清空 user 集合
排序
db.user.find({查询条件}).sort({k1: 1}, {k2: -1})

# 按照 k1 来排序, 如果 k1 的值相同,按照 k2 来排序

# 1 升序, -1 降序

node 里简单封装增删改查

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
// node mongodb 版本 v3.1.13 适用
const MongoClient = require('mongodb').MongoClient;
const log = console.log.bind(console);

class Dao {
/**
* 构造函数
* @param {string} url
* @param {string} dbname
* @param {string} colname
*/
constructor(url, dbname, colname) {
this.url = url;
this.dbname = dbname;
this.colname = colname;
}

/**
* 连接数据库
*/
_connect() {
return new Promise((resolve, reject) => {
MongoClient.connect(this.url, { useNewUrlParser: true }, (err, client) => {
if (err) {
reject(err);
} else {
resolve(client);
}
});
});
}

/**
* 插入文档
* @param {arr || object} documents
* @param {boolean} insertMany
* 使用 insertMany(arr), 在 arr === [] 时,会报错
*/
insert(documents, insertMany = false) {
return new Promise((resolve, reject) => {
this._connect()
.then(client => {
let col = client.db(this.dbname).collection(this.colname);
if (insertMany) {
col
.insertMany(documents)
.then(res => {
resolve(res);
})
.catch(err => {
log(err);
});
client.close();
} else {
col
.insertOne(documents)
.then(res => {
resolve(res);
})
.catch(err => {
log(err);
});
client.close();
}
})
.catch(err => {
reject(err);
});
});
}

/**
* 查询
* @param {object} document
*/
query(document, pageConfig) {
document = document || {};
pageConfig = pageConfig || {};
let page = pageConfig.page;
let amount = pageConfig.amount;
const resData = [];

return new Promise((resolve, reject) => {
this._connect()
.then(client => {
let col = client.db(this.dbname).collection(this.colname);
let cursor = col
.find(document)
.limit(amount)
.skip((page - 1) * amount);
cursor.each((err, data) => {
if (err) {
reject(err);
client.close();
} else if (data !== null) {
resData.push(data);
} else {
resolve(resData);
client.close();
}
});
})
.catch(err => {
log(err);
});
});
}

/**
* 删除集合中的数据
* @param {object} query
* @param {boolean} deleteMany
*/
delete(query, deleteMany = false) {
return new Promise((resolve, reject) => {
this._connect()
.then(client => {
let col = client.db(this.dbname).collection(this.colname);
if (deleteMany) {
col.deleteMany(query).then(res => {
resolve(res);
client.close();
});
} else {
col.deleteOne(query).then(res => {
resolve(res);
client.close();
});
}
})
.catch(err => {
log(err);
});
});
}

/**
* 更新
* @param {obj} filter
* @param {obj} updater
*/
update(filter, updater) {
let uptaterCpy = { $set: updater };

return new Promise((resolve, reject) => {
this._connect()
.then(client => {
let col = client.db(this.dbname).collection(this.colname);
col.updateMany(filter, uptaterCpy).then(res => {
resolve(res);
client.close();
});
})
.catch(err => {
log(err);
});
});
}
}

//
let url = 'mongodb://localhost:27017';
let dbname = 'test';
let colname = 'user';
let dao = new Dao(url, dbname, colname);

let arr = [];
for (let i = 0; i < 20; i++) {
arr.push({
userid: '234',
age: i,
});
}

ts 声明文件基本写法

全局示例

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
// global-lib.js
function globalLib(option) {
console.log(option);
}
globalLib.version = '1.0.0';
globalLib.doSomething = function () {
console.log('global do something');
};
// global.d.ts
declare function globalLib(option: globalLib.Option): void;

declare namespace globalLib {
const version: string;
function doSomething(): void;
interface Option {
[key: string]: any;
}
}

// global.d.ts
declare function globalLib(option: globalLib.Option): void;

declare namespace globalLib {
const version: string;
function doSomething(): void;
interface Option {
[key: string]: any;
}
}

模块示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// module-lib.js
const version = '1.0.0'
const doSomething = function() {
console.log('module do something')
}
function moduleLib(option) {
console.log(option)
}

moduleLib.version = version
moduleLib.doSomething = doSomething

module.exports = moduleLib


// module-lib.d.ts
declare function moduleLib(option: Option): void

interface Option {
[key: string]: any
}

declare namespace moduleLib {
const version: string
function doSomething(): void
}

export = moduleLib



umd 模块示例

// umd-lib.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory)
} else if (typeof module === 'object' && module.exports) {
module.exports = factory()
} else {
root.umdLib = factory()
}
}(this, function() {
return {
version: '1.0.0',
doSomething() {
console.log('umd do something')
}
}
}))


// umd-lib.d.ts
declare namespace umdLib {
const version: string
function doSomething(): void
}

// umd 模块必须写此行
export as namespace umdLib

export = umdLib

前置知识:

declare var 声明全局变量
declare function 声明全局方法
declare class 声明全局类
declare enum 声明全局枚举类型
declare namespace 声明(含有子属性的)全局对象
interface 和 type 声明全局类型
export 导出变量
export namespace 导出(含有子属性的)对象
export default ES6 默认导出
export = commonjs 导出模块
export as namespace UMD 库声明全局变量
declare global 扩展全局变量
declare module 扩展模块
/// 三斜线指令

从 generator 到 async/await

协程

协程是一种比线程更加轻量级的存在。可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程

协程有点像函数,又有点像线程。它的运行流程大致如下。

第一步,协程 A 开始执行。

第二步,协程 A 执行到一半,进入暂停,执行权转移到协程 B。

第三步,(一段时间后)协程 B 交还执行权。

第四步,协程 A 恢复执行。

上面流程的协程 A,就是异步任务,因为它分成两段(或多段)执行。

举例来说,读取文件的协程写法如下。

1
2
3
4
5
function asnycJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}

上面代码的函数 asyncJob 是一个协程,在执行到其中的 yield 命令处时,执行权将交给其他协程。也就是说,yield 命令是异步两个阶段的分界线。

协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除 yield 命令,简直一模一样。

Generator

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

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
// generator 函数
function* foo() {
let response1 = yield fetch('http://back.avatar.movie.test.sankuai.com/');
console.log('response1');
console.log(response1);
let response2 = yield fetch('http://back.avatar.movie.test.sankuai.com/');
console.log('response2');
console.log(response2);
}

// 执行 foo 函数的代码
let gen = foo();
function getGenPromise(gen) {
return gen.next().value;
}
getGenPromise(gen)
.then(response => {
console.log('response1');
console.log(response);
return getGenPromise(gen);
})
.then(response => {
console.log('response2');
console.log(response);
});
  • 首先执行的是let gen = foo(),创建了 gen 协程。
  • 然后在父协程中通过执行 gen.next 把主线程的控制权交给 gen 协程。
  • gen 协程获取到主线程的控制权后,就调用 fetch 函数创建了一个 Promise 对象 response1,然后通过 yield 暂停 gen 协程的执行,并将 response1 返回给父协程。
  • 父协程恢复执行后,调用 response1.then 方法等待请求结果。
  • 等通过 fetch 发起的请求完成之后,会调用 then 中的回调函数,then 中的回调函数拿到结果之后,通过调用 gen.next 放弃主线程的控制权,将控制权交 gen 协程继续执行下个请求。

通过 Generator 和 Promise 相互配合执行,达到了将异步操作以同步方式书写的目的。不过通常,我们把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器

(可参考著名的 co 框架),如下面这种方式:

1
2
3
4
5
6
7
8
9
function* foo() {
let response1 = yield fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}
co(foo());

通过使用生成器配合执行器,就能实现使用同步的方式写出异步代码了,这样也大大加强了代码的可读性。

co 源码分析

Co 核心代码(删去了非核心代码)

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
function co(gen) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
onFulfilled();

function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(
new TypeError(
'You may only yield a function, promise, generator, array, or object, ' +
'but the following object was passed: "' +
String(ret.value) +
'"'
)
);
}
}

这儿,在给 co 传入一个generator函数后,co 会将其自动启动。然后调用onFulfilled函数。在onFulfilled函数内部,首先则是获取 next 的返回值。交由next函数处理。 而next函数则首先判断是否完成,如果这个 generator 函数完成了,返回最终的值。否则则将yield后的值,转换为Promise。最后,通过Promise的 then,并将onFulfilled函数作为参数传入。

1
2
3
if (value && isPromise(value)) {
return value.then(onFulfilled, onRejected);
}

而在generator中,yield句本身没有返回值,或者说总是返回undefined 而 next 方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。同时通过onFulfilled函数,则可以实现自动调用。这也就能解释为什么 co 基于Promise。且能自动执行了。

Async/await

async 到底是什么?根据 MDN 定义,async 是一个通过异步执行隐式返回 Promise 作为结果的函数。

对 async 函数的理解,需要重点关注两个词:异步执行隐式返回 Promise

1
2
3
4
5
async function foo() {
return 2;
}
// async 函数返回一个 promise
console.log(foo()); // Promise {<resolved>: 2}

await 到底是什么?

1
2
3
4
5
6
7
8
9
async function foo() {
console.log(1);
let a = await 100;
console.log(a);
console.log(2);
}
console.log(0);
foo();
console.log(3);

根据上面的代码来分析 async/await 的执行流程。

首先,执行console.log(0)这个语句,打印出来 0。

  1. 执行 console.log(0)
  2. 执行 foo 函数,foo 是 async 函数,js 引擎保留当前的调用栈等信息
  3. 执行 foo 函数中 console.log(1)
  4. 执行 await 100,当遇到 await 100语句,js 引擎默认创建一个 promise 对象,大致代码如下:
1
2
3
let promise_ = new Promise((resolve,reject){
resolve(100)
})
  1. js 引擎执行到 resolve(10)将任务提交给微任务队列
  2. 然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程
  3. 父协程拿到主线程控制权,做的一件事是调用 promise_.then 来监控 promise 状态的改变
  4. 继续执行console.log(3)
  5. 父协程将执行结束,在结束之前,检查微任务队列,然后执行微任务队列
  6. 执行resolve(100),触发 promise_.then 中的回调函数:
1
2
3
4
promise_.then(value => {
// 回调函数被激活后
// 将主线程控制权交给 foo 协程,并将 vaule 值传给协程
});
  1. 将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程
  2. foo 协程激活之后,会把刚才的 value 值赋给了变量 a
  3. 执行 foo 函数的后续语句,执行完成之后,将控制权归还给父协程。
  4. 完毕。

以上就是 await/async 的执行流程。正是因为 async 和 await 在背后为我们做了大量的工作,所以我们才能用同步的方式写出异步代码来。


Co 源码详细分析

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
/**
* 执行 generator function 或者 generator,
* 返回一个 Promise
* @param {Function} fn
* @return {Promise}
* @api public
*/

function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);

// we wrap everything in a promise to avoid promise chaining,
// 把传进来的所有东西都转为 promise
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function (resolve, reject) {
// 执行 gen 函数
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// gen函数返回的 gen 指针不存在或 gen.next 不是函数(意味着 gen 不是 Generator函数) 则返回空值
if (!gen || typeof gen.next !== 'function') return resolve(gen);

onFulfilled();

/**
* @param {Mixed} res
* @return {Promise}
* @api private
* 把每次 yield 之后的异步函数的返回结果当做参数传回 generator 函数
* eg. let a = yeild b
* 此时经过 onFulfilled 函数的处理 a === b()
*/
function onFulfilled(res) {
var ret;
try {
/**
* gen.next(res),则是向generator函数传参数,作为yield的返回值
* ret 是 gen.next() 返回的对象
* ---
* yield 语句本身没有返回值,或者说总是返回undefined。
* next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
*/
ret = gen.next(res);
} catch (e) {
return reject(e);
}
// 每完成一次 yield,把返回的对象交给 next() 处理
next(ret);
}

/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* Get the next value in the generator,
* return a promise.
* 把 gen.next() 返回对象中的 value 变为 promise
*
* @param {Object} ret
* @return {Promise}
* @api private
*/

function next(ret) {
/**
* 如果这个generator函数完成了,返回最终的值
* 假设我们写的 generator函数没有 return someValue, 在所有yield完成后,调用next()会返回{valu: undefined, done: true}
* 所以需要手动return一个值。这样最后的value才不是undefined
*/
if (ret.done) return resolve(ret.value);
// 这个generator函数还没结束, 就统一交给 toPromise() 处理
var value = toPromise.call(ctx, ret.value);
// 这里value.then(onFulfilled, onRejected),实际上已经调用并传入了 onFulfilled, onRejected 两个参数。
// 把 onFulfilled 函数传入 value
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(
new TypeError(
'You may only yield a function, promise, generator, array, or object, ' +
'but the following object was passed: "' +
String(ret.value) +
'"'
)
);
}
});
}

剩下的(thunkToPromise, arrayToPromise)这些辅助函数就不写了