Skip to main content

JavaScript Advanced Notes

Iterator

Iteration Protocol

Iteration protocol:

  • 一个数据结构只要实现了 [Symbol.iterator]() 接口, 便可成为可迭代数据结构 (Iterable):
    • String: StringIterator.
    • Array: ArrayIterator.
    • Map: MapIterator.
    • Set: SetIterator.
    • arguments 对象.
    • DOM collection (NodeList): ArrayIterator.
  • 接收可迭代对象的原生语言特性:
    • for...in/for...of.
    • Destructing: 数组解构.
    • ...: 扩展操作符 (Spread Operator).
    • Array.from().
    • new Map().
    • new Set().
    • Promise.all().
    • Promise.race().
    • yield * 操作符.
  • for...in/for...of 隐形调用迭代器的方式, 称为内部迭代器, 使用方便, 不可自定义迭代过程.
  • { next, done, value } 显式调用迭代器的方式, 称为外部迭代器, 使用复杂, 可以自定义迭代过程.
  • All built-in ES6 iterators are Self Iterable Iterator.
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}

interface Iterator<T> {
next(...args: []): IteratorResult<T>;
return?(value?: T): IteratorResult<T>; // Closable iterator
throw?(e?: any): IteratorResult<T>;
}

interface IterableIterator<T> extends Iterator<T> {
[Symbol.iterator](): IterableIterator<T>;
}

interface AsyncIterable<T> {
[Symbol.asyncIterator](): AsyncIterator<T>;
}

interface AsyncIterator<T> {
next(...args: []): Promise<IteratorResult<T>>;
return?(value?: T | PromiseLike<T>): Promise<IteratorResult<T>>; // Closable iterator
throw?(e?: any): Promise<IteratorResult<T>>;
}

interface AsyncIterableIterator<T> extends AsyncIterator<T> {
[Symbol.asyncIterator](): AsyncIterableIterator<T>;
}

interface IteratorResult<T> {
done: boolean;
value: T;
}

Synchronous Iterator

Iterable Object

function methodsIterator() {
let index = 0;
const methods = Object.keys(this)
.filter(key => {
return typeof this[key] === 'function';
})
.map(key => this[key]);

// iterator object
return {
next: () => ({
// Conform to Iterator protocol
done: index >= methods.length,
value: methods[index++],
}),
};
}

const myMethods = {
toString() {
return '[object myMethods]';
},
sumNumbers(a, b) {
return a + b;
},
numbers: [1, 5, 6],
[Symbol.iterator]: methodsIterator, // Conform to Iterable Protocol
};

for (const method of myMethods) {
console.log(method); // logs methods `toString` and `sumNumbers`
}
function zip(...iterables) {
const iterators = iterables.map(i => i[Symbol.iterator]());
let done = false;

return {
[Symbol.iterator]() {
return this;
},
next() {
if (!done) {
const items = iterators.map(i => i.next());
done = items.some(item => item.done);

if (!done) {
return { value: items.map(i => i.value) };
}

// Done for the first time: close all iterators
for (const iterator of iterators) {
if (typeof iterator.return === 'function') {
iterator.return();
}
}
}

// We are done
return { done: true };
},
};
}

const zipped = zip(['a', 'b', 'c'], ['d', 'e', 'f', 'g']);

for (const x of zipped) {
console.log(x);
}
// Output:
// ['a', 'd']
// ['b', 'e']
// ['c', 'f']

Iterable Class

class Counter {
constructor(limit) {
this.limit = limit;
}

[Symbol.iterator]() {
let count = 1;
const limit = this.limit;

return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true };
}
},
return() {
console.log('Exiting early');
return { done: true };
},
};
}
}

const counter1 = new Counter(5);
for (const i of counter1) {
if (i > 2) {
break;
}
console.log(i);
}
// 1
// 2
// Exiting early

const counter2 = new Counter(5);
try {
for (const i of counter2) {
if (i > 2) {
throw new Error('err');
}

console.log(i);
}
} catch (e) {}
// 1
// 2
// Exiting early

const counter3 = new Counter(5);
const [a, b] = counter3;
// Exiting early

Class Iterator

// Class Iterator:
class MatrixIterator {
constructor(matrix) {
this.x = 0;
this.y = 0;
this.matrix = matrix;
}

next() {
if (this.y === this.matrix.height) return { done: true };

const value = {
x: this.x,
y: this.y,
value: this.matrix.get(this.x, this.y),
};

this.x++;

if (this.x === this.matrix.width) {
this.x = 0;
this.y++;
}

return { value, done: false };
}
}

// Iterable Class:
class Matrix {
constructor(width, height, element = (x, y) => undefined) {
this.width = width;
this.height = height;
this.content = [];

for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
this.content[y * width + x] = element(x, y);
}
}
}

get(x, y) {
return this.content[y * this.width + x];
}

set(x, y, value) {
this.content[y * this.width + x] = value;
}

[Symbol.iterator]() {
return new MatrixIterator(this);
}
}

const matrix = new Matrix(2, 2, (x, y) => `value ${x},${y}`);

for (const { x, y, value } of matrix) {
console.log(x, y, value);
}
// → 0 0 value 0, 0
// → 1 0 value 1, 0
// → 0 1 value 0, 1
// → 1 1 value 1, 1

Asynchronous Iterator

const AsyncIterable = {
[Symbol.asyncIterator]() {
return AsyncIterator;
},
};

const AsyncIterator = {
next() {
return Promise.resolve(IteratorResult);
},
return() {
return Promise.resolve(IteratorResult);
},
throw(e) {
return Promise.reject(e);
},
};

const IteratorResult = {
value: any,
done: boolean,
};

// Tasks will chained:
ait
.next()
.then(({ value, done }) => ait.next())
.then(({ value, done }) => ait.next())
.then(({ done }) => done);

// Tasks will run in parallel:
ait.next().then();
ait.next().then();
ait.next().then();
function remotePostsAsyncIteratorsFactory() {
let i = 1;
let done = false;

const asyncIterableIterator = {
// the next method will always return a Promise
async next() {
// do nothing if we went out-of-bounds
if (done) {
return Promise.resolve({
done: true,
value: undefined,
});
}

const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${i++}`
).then(r => r.json());

// the posts source is ended
if (Object.keys(res).length === 0) {
done = true;
return Promise.resolve({
done: true,
value: undefined,
});
} else {
return Promise.resolve({
done: false,
value: res,
});
}
},
[Symbol.asyncIterator]() {
return this;
},
};

return asyncIterableIterator;
}

(async () => {
const ait = remotePostsAsyncIteratorsFactory();

await ait.next(); // { done:false, value:{id: 1, ...} }
await ait.next(); // { done:false, value:{id: 2, ...} }
await ait.next(); // { done:false, value:{id: 3, ...} }
// ...
await ait.next(); // { done:false, value:{id: 100, ...} }
await ait.next(); // { done:true, value:undefined }
})();

Closable Iterator

  • An iterator is closable if it has a method return().
interface ClosableIterator {
next(): IteratorResult;
return(value?: any): IteratorResult;
}
  • Not all iterators are closable: e.g Array Iterator.
const iterable = ['a', 'b', 'c'];
const iterator = iterable[Symbol.iterator]();
console.log('return' in iterator);
// => false
  • If an iterator is not closable, you can continue iterating over it after an abrupt exit.
  • If an iterator is closable, you can't continue iterating over it after an abrupt exit.
function* elements() {
yield 'a';
yield 'b';
yield 'c';
}

function twoLoops(iterator) {
// eslint-disable-next-line no-unreachable-loop
for (const x of iterator) {
console.log(x);
break;
}

for (const x of iterator) {
console.log(x);
}
}

class PreventReturn {
constructor(iterator) {
this.iterator = iterator;
}

[Symbol.iterator]() {
return this;
}

next() {
return this.iterator.next();
}

return(value = undefined) {
return { done: false, value };
}
}

twoLoops(elements());
// Output:
// a

twoLoops(new PreventReturn(elements()));
// Output:
// a
// b
// c

twoLoops(['a', 'b', 'c'][Symbol.iterator]());
// Output:
// a
// b
// c
  • Manually call iterator.return():
function take(n, iterable) {
const iter = iterable[Symbol.iterator]();

return {
[Symbol.iterator]() {
return this;
},
next() {
if (n > 0) {
n--;
return iter.next();
} else {
iter?.return();
return { done: true };
}
},
return() {
n = 0;
iter?.return();
},
};
}

Generator

Generator Definition

  • 函数名称前面加一个星号 (*) 表示它是一个生成器函数.
  • 箭头函数不能用来定义生成器函数.
  • 调用生成器函数会产生一个生成器对象, 其是一个自引用可迭代对象: 其本身是一个迭代器, 同时实现了 Iterable 接口 (返回 this).
interface GeneratorFunction {
(...args: any[]): Generator;
readonly length: number;
readonly name: string;
readonly prototype: Generator;
}

interface Generator<T> extends Iterator<T> {
next(...args: []): IteratorResult<T>;
return(value: T): IteratorResult<T>; // Required
throw(e: any): IteratorResult<T>; // Required
[Symbol.iterator](): Generator<T>;
}

interface AsyncGeneratorFunction {
(...args: any[]): AsyncGenerator;
readonly length: number;
readonly name: string;
readonly prototype: AsyncGenerator;
}

interface AsyncGenerator<T> extends AsyncIterator<T> {
next(...args: []): Promise<IteratorResult<T>>;
return(value: T | PromiseLike<T>): Promise<IteratorResult<T>>; // Required
throw(e: any): Promise<IteratorResult<T>>; // Required
[Symbol.asyncIterator](): AsyncGenerator<T>;
}
function* generatorFn() {}
console.log(generatorFn);
// f* generatorFn() {}

console.log(generatorFn()[Symbol.iterator]);
// f [Symbol.iterator]() {native code}

console.log(generatorFn());
// generatorFn {<suspended>}

console.log(generatorFn()[Symbol.iterator]());
// generatorFn {<suspended>}

const g = generatorFn(); // IterableIterator
console.log(g === g[Symbol.iterator]());
// true

Generator Roles

Generators can play 3 roles:

  • Iterators (data producers): generators can produce sequences of values via loops and recursion.
  • Observers (data consumers): generators become data consumers that pause until a new value is pushed into them via next(value) (yield can receive a value from next(value)).
  • Coroutines (data producers and consumers): generators are pauseable and can be both data producers and data consumers, generators can be coroutines (cooperatively multi-tasked tasks).

Generator Basic Usage

function* gen() {
yield 1;
yield 2;
yield 3;
}

const g = gen();

g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.next(); // { value: 3, done: false }
g.next(); // { value: undefined, done: true }
g.return(); // { value: undefined, done: true }
g.return(1); // { value: 1, done: true }

Default Iterator Generator

生成器函数和默认迭代器被调用之后都产生迭代器 (生成器对象是自引用可迭代对象, 自身是一个迭代器), 所以生成器适合作为默认迭代器:

const users = {
james: false,
andrew: true,
alexander: false,
daisy: false,
luke: false,
clare: true,

*[Symbol.iterator]() {
// this === 'users'
for (const key in this) {
if (this[key]) yield key;
}
},
};

for (const key of users) {
console.log(key);
}
// andrew
// clare

class Foo {
constructor() {
this.values = [1, 2, 3];
}

*[Symbol.iterator]() {
yield* this.values;
}
}

const f = new Foo();

for (const x of f) {
console.log(x);
}
// 1
// 2
// 3

Early Return Generator

  • return() 方法会强制生成器进入关闭状态.
  • 提供给 return() 的值, 就是终止迭代器对象的值.
function* gen() {
yield 1;
yield 2;
yield 3;
}

const g = gen();

g.next(); // { value: 1, done: false }
g.return('foo'); // { value: "foo", done: true }
g.next(); // { value: undefined, done: true }

Error Handling Generator

  • throw() 方法会在暂停的时候将一个提供的错误注入到生成器对象中. 如果错误未被处理, 生成器就会关闭.
  • 假如生成器函数内部处理了这个错误, 那么生成器就不会关闭, 可以恢复执行. 错误处理会跳过对应的 yield (跳过一个值).
function* generator() {
try {
yield 1;
} catch (e) {
console.log(e);
}

yield 2;
yield 3;
yield 4;
yield 5;
}

const it = generator();

it.next(); // {value: 1, done: false}

// the error will be handled and printed ("Error: Handled!"),
// then the flow will continue, so we will get the
// next yielded value as result.
it.throw(Error('Handled!')); // {value: 2, done: false}

it.next(); // {value: 3, done: false}

// now the generator instance is paused on the
// third yield that is not inside a try-catch.
// the error will be re-thrown out
it.throw(Error('Not handled!')); // !!! Uncaught Error: Not handled! !!!

// now the iterator is exhausted
it.next(); // {value: undefined, done: true}

Generator Advanced Usage

Next Value Generator

当为 next 传递值进行调用时, 传入的值会被当作上一次生成器函数暂停时 yield 关键字的返回值处理. 第一次调用 g.next() 传入参数是毫无意义, 因为首次调用 next 函数时, 生成器函数并没有在 yield 关键字处暂停:

function* lazyCalculator(operator) {
const firstOperand = yield;
const secondOperand = yield;

switch (operator) {
case '+':
yield firstOperand + secondOperand;
return;
case '-':
yield firstOperand - secondOperand;
return;
case '*':
yield firstOperand * secondOperand;
return;
case '/':
yield firstOperand / secondOperand;
return;
default:
throw new Error('Unsupported operation!');
}
}

const g = gen('*');
g.next(); // { value: undefined, done: false }
g.next(10); // { value: undefined, done: false }
g.next(2); // { value: 20, done: false }
g.next(); // { value: undefined, done: true }

Default Asynchronous Iterator Generator

Default asynchronous iterator:

const asyncSource = {
async *[Symbol.asyncIterator]() {
yield await new Promise(resolve => setTimeout(resolve, 1000, 1));
},
};

for await (const chunk of asyncSource) {
console.log(chunk);
}

Asynchronous Generator

async function* remotePostsAsyncGenerator() {
let i = 1;

while (true) {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${i++}`
).then(r => r.json());

// when no more remote posts will be available,
// it will break the infinite loop.
// the async iteration will end
if (Object.keys(res).length === 0) {
break;
}

yield res;
}
}

for await (const chunk of remotePostsAsyncGenerator()) {
console.log(chunk);
}

Asynchronous Events Stream

Asynchronous UI events stream (RxJS):

class Observable {
constructor() {
this.promiseQueue = [];
// 保存用于队列下一个 promise 的 resolve 方法
this.resolve = null;
// 把最初的 promise 推到队列, 该 promise 会 resolve 为第一个观察到的事件
this.enqueue();
}

// 创建新 promise, 保存其 resolve 方法, 并把它保存到队列中
enqueue() {
this.promiseQueue.push(new Promise(resolve => (this.resolve = resolve)));
}

// 从队列前端移除 promise, 并返回它
dequeue() {
return this.promiseQueue.shift();
}

async *fromEvent(element, eventType) {
// 在有事件生成时, 用事件对象来 resolve 队列头部的 promise
// 同时把另一个 promise 加入队列
element.addEventListener(eventType, event => {
this.resolve(event);
this.enqueue();
});

// 每次 resolve 队列头部的 promise 后, 都会向异步迭代器返回相应的事件对象
while (true) {
yield await this.dequeue();
}
}
}

const observable = new Observable();
const button = document.querySelector('button');
const mouseClickIterator = observable.fromEvent(button, 'click');

for await (const clickEvent of mouseClickIterator) {
console.log(clickEvent);
}

Generator based asynchronous control flow goodness for nodejs and the browser, using promises, letting you write non-blocking code in a nice-ish way (just like tj/co).

function coroutine(generatorFunc) {
const generator = generatorFunc();

function nextResponse(value) {
const response = generator.next(value);

if (response.done) {
return;
}

if (value instanceof Promise) {
value.then(nextResponse);
} else {
nextResponse(response.value);
}
}

nextResponse();
}

coroutine(function* bounce() {
yield bounceUp;
yield bounceDown;
});

利用 async/await 可以实现相同效果:

function co(gen) {
return new Promise((resolve, reject) => {
const g = gen();

function next(param) {
const { done, value } = g.next(param);

if (!done) {
// Resolve chain.
Promise.resolve(value).then(res => next(res));
} else {
resolve(value);
}
}

// First invoke g.next() without params.
next();
});
}

function promise1() {
return new Promise(resolve => {
setTimeout(() => {
resolve('1');
}, 1000);
});
}

function promise2(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(`value:${value}`);
}, 1000);
});
}

function* readFileGenerator() {
const value = yield promise1();
const result = yield promise2(value);
return result;
}

async function readFile() {
const value = await promise1();
const result = await promise2(value);
return result;
}

co(readFileGenerator).then(res => console.log(res));
// const g = readFileGenerator();
// const value = g.next();
// const result = g.next(value);
// resolve(result);

readFile().then(res => console.log(res));

Delegating Generator

yield * 能够迭代一个可迭代对象 (yield* iterable):

  • 可以迭代标准库提供的 Iterable 集合.
  • 生成器函数产生的生成器对象是一个自引用可迭代对象, 可以使用 yield * 聚合生成器 (Delegating Generator).
function* generatorFn() {
console.log('iter value:', yield* [1, 2, 3]);
}

for (const x of generatorFn()) {
console.log('value:', x);
}
// value: 1
// value: 2
// value: 3
// iter value: undefined
function* innerGeneratorFn() {
yield 'foo';
return 'bar';
}

function* outerGeneratorFn(genObj) {
console.log('iter value:', yield* innerGeneratorFn());
}

for (const x of outerGeneratorFn()) {
console.log('value:', x);
}
// value: foo
// iter value: bar
function* chunkify(array, n) {
yield array.slice(0, n);
array.length > n && (yield* chunkify(array.slice(n), n));
}

async function* getRemoteData() {
let hasMore = true;
let page;

while (hasMore) {
const { next_page, results } = await fetch(URL, { params: { page } }).then(
r => r.json()
);

// Return 5 elements with each iteration.
yield* chunkify(results, 5);

hasMore = next_page !== null;
page = next_page;
}
}

for await (const chunk of getRemoteData()) {
console.log(chunk);
}

Recursive Generator

在生成器函数内部, 用 yield * 去迭代自身产生的生成器对象, 实现递归算法.

Tree traversal:

// Tree traversal
class BinaryTree {
constructor(value, left = null, right = null) {
this.value = value;
this.left = left;
this.right = right;
}

*[Symbol.iterator]() {
yield this.value;

if (this.left) {
// Short for: yield* this.left[Symbol.iterator]()
yield* this.left;
}

if (this.right) {
// Short for: yield* this.right[Symbol.iterator]()
yield* this.right;
}
}
}

const tree = new BinaryTree(
'a',
new BinaryTree('b', new BinaryTree('c'), new BinaryTree('d')),
new BinaryTree('e')
);

for (const x of tree) {
console.log(x);
}
// Output:
// a
// b
// c
// d
// e

Graph traversal:

// Graph traversal
function* graphTraversal(nodes) {
for (const node of nodes) {
if (!visitedNodes.has(node)) {
yield node;
yield* graphTraversal(node.neighbors);
}
}
}

DOM traversal:

function* domTraversal(element) {
yield element;
element = element.firstElementChild;

while (element) {
yield* domTraversal(element);
element = element.nextElementSibling;
}
}

for (const element of domTraversal(document.getElementById('subTree'))) {
console.log(element.nodeName);
}

结合 Promise/async/await 可以实现异步递归算法:

import { promises as fs } from 'node:fs';
import { basename, dirname, join } from 'node:path';

async function* walk(dir: string): AsyncGenerator<string> {
for await (const d of await fs.opendir(dir)) {
const entry = join(dir, d.name);

if (d.isDirectory()) {
yield* walk(entry);
} else if (d.isFile()) {
yield entry;
}
}
}

async function run(arg = '.') {
if ((await fs.lstat(arg)).isFile()) {
return runTestFile(arg);
}

for await (const file of walk(arg)) {
if (
!dirname(file).includes('node_modules') &&
(basename(file) === 'test.js' || file.endsWith('.test.js'))
) {
console.log(file);
await runTestFile(file);
}
}
}

Promise

Callback style asynchronous programming:

  • Callback hell.
  • Complicated error handling.
  • Complicated composition.

Promise style asynchronous programming:

  • Avoid callback hell:
    • Return new Promise()/Promise.resolve().
    • Return promise.then((value) => {}).
  • Simple error handling:
    • Catch error: promise.catch((err) => {}).
    • Cleanup: promise.finally(() => {}).
  • Simple composition:
    • Promise.all: Converts an Array of Promises to a Promise for an Array.
    • Promise.race.

Promise Resolve

Resolve only accept one value:

return new Promise(resolve => resolve([a, b]));
const thenable = {
then(resolve, reject) {
resolve(42);
},
};
const promise = Promise.resolve(thenable);
promise.then(value => {
console.log(value); // 42
});

Promise.resolve 是一个幂等方法 (状态机幂等):

const p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p));
// true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p)));
// true

const p = new Promise(() => {});
setTimeout(console.log, 0, p);
// Promise <pending>
setTimeout(console.log, 0, Promise.resolve(p));
// Promise <pending>
setTimeout(console.log, 0, p === Promise.resolve(p));
// true

Promise Reject

let p1 = Promise.resolve('foo');
let p2 = p1.then();
stetTimeout(console.log, 0, p2); // Promise <resolved>: foo

// eslint-disable-next-line prefer-promise-reject-errors
p1 = Promise.reject('foo');
p2 = p1.then();
// Uncaught (in promise) foo
setTimeout(console.log, 0, p2); // Promise <rejected>: foo

const p3 = p1.then(null, () => undefined);
const p4 = p1.then(null, () => {});
const p5 = p1.then(null, () => Promise.resolve());
setTimeout(console.log, 0, p3); // Promise <resolved>: undefined
setTimeout(console.log, 0, p4); // Promise <resolved>: undefined
setTimeout(console.log, 0, p5); // Promise <resolved>: undefined

const p6 = p1.then(null, () => 'bar');
const p7 = p1.then(null, () => Promise.resolve('bar'));
setTimeout(console.log, 0, p6); // Promise <resolved>: bar
setTimeout(console.log, 0, p7); // Promise <resolved>: bar

const p8 = p1.then(null, () => new Promise(() => {}));
// eslint-disable-next-line prefer-promise-reject-errors
const p9 = p1.then(null, () => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined

const p10 = p1.then(null, () => {
// eslint-disable-next-line no-throw-literal
throw 'bar';
});
// Uncaught (in promise) bar
setTimeout(console.log, 0, p10); // Promise <rejected>: bar

const p11 = p1.then(null, () => Error('bar'));
setTimeout(console.log, 0, p11); // Promise <resolved>: Error: bar

Promise Catch

// eslint-disable-next-line prefer-promise-reject-errors
const p = Promise.reject();
const onRejected = function (e) {
setTimeout(console.log, 0, 'rejected');
};
// 语法糖:
p.then(null, onRejected); // rejected
p.catch(onRejected); // rejected
const p1 = new Promise(() => {});
const p2 = p1.catch();
setTimeout(console.log, 0, p1); // Promise <pending>
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false

Promise Finally

const p1 = new Promise(() => {});
const p2 = p1.finally();
setTimeout(console.log, 0, p1); // Promise <pending>
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false
const p1 = Promise.resolve('foo');

// 原样后传:
const p2 = p1.finally();
const p3 = p1.finally(() => undefined);
const p4 = p1.finally(() => {});
const p5 = p1.finally(() => Promise.resolve());
const p6 = p1.finally(() => 'bar');
const p7 = p1.finally(() => Promise.resolve('bar'));
const p8 = p1.finally(() => Error('bar'));
setTimeout(console.log, 0, p2); // Promise <resolved>: foo
setTimeout(console.log, 0, p3); // Promise <resolved>: foo
setTimeout(console.log, 0, p4); // Promise <resolved>: foo
setTimeout(console.log, 0, p5); // Promise <resolved>: foo
setTimeout(console.log, 0, p6); // Promise <resolved>: foo
setTimeout(console.log, 0, p7); // Promise <resolved>: foo
setTimeout(console.log, 0, p8); // Promise <resolved>: foo

// 特殊处理:
const p9 = p1.finally(() => new Promise(() => {}));
setTimeout(console.log, 0, p9); // Promise <pending>
// eslint-disable-next-line prefer-promise-reject-errors
const p10 = p1.finally(() => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p10); // Promise <rejected>: undefined
const p11 = p1.finally(() => {
// eslint-disable-next-line no-throw-literal
throw 'bar';
});
// Uncaught (in promise) baz
setTimeout(console.log, 0, p11); // Promise <rejected>: bar

Any value or resolved promises returned from finally() is ignored:

const promise = Promise.resolve(42);

promise
.finally(() => {
// Settlement handler
return 43; // Ignored!
})
.then(value => {
// Fulfillment handler
console.log(value); // 42
});

promise
.finally(() => {
// Settlement handler
return Promise.resolve(44); // Ignored!
})
.then(value => {
// Fulfillment handler
console.log(value); // 42
});

Returning rejected promise from finally() equivalent to throwing an error:

const promise = Promise.resolve(42);

promise
.finally(() => {
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject(43);
})
.catch(reason => {
console.error(reason); // 43
});
// eslint-disable-next-line prefer-promise-reject-errors
const promise = Promise.reject(43);

promise
.finally(() => {
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject(45);
})
.catch(reason => {
console.log(reason); // 45
});

Promise Thenable and Catch

The main difference between the forms promise.then(success, error) and promise.then(success).catch(error): in case if success callback returns a rejected promise, then only the second form is going to catch that rejection.

正常情况下, 在通过 throw() 关键字抛出错误时, JavaScript 运行时的错误处理机制会停止执行抛出错误之后的任何指令. 但在 Promise 中抛出错误时, 因为错误实际上是从消息队列中异步抛出的, 所以并不会阻止运行时继续执行同步指令 (Node.js 中仍然会停止执行任何指令).

throw new Error('foo');
console.log('bar'); // 这一行不会执行
// Uncaught Error: foo
Promise.reject(Error('foo'));
console.log('bar');
// bar
// Uncaught (in promise) Error: foo

const p1 = new Promise((resolve, reject) => reject(Error('foo'))); // 1.
const p2 = new Promise((resolve, reject) => {
throw new Error('foo'); // 2.
});
const p3 = Promise.resolve().then(() => {
throw new Error('foo'); // 4.
});
const p4 = Promise.reject(Error('foo')); // 3.
// Uncaught (in promise) Error: foo
// at Promise (test.html:1)
// at new Promise (<anonymous>)
// at test.html:1
// Uncaught (in promise) Error: foo
// at Promise (test.html:2)
// at new Promise (<anonymous>)
// at test.html:2
// Uncaught (in promise) Error: foo
// at test.html:4
// Uncaught (in promise) Error: foo
// at Promise.resolve.then (test.html:3)

Promise Chain

  • Promises on the same chain execute orderly.
  • Promises on two separate chains execute in random order.
const users = ['User1', 'User2', 'User3', 'User4'];

const response = [];

const getUser = user => () => {
return axios.get(`/users/userId=${user}`).then(res => response.push(res));
};

const getUsers = users => {
const [getFirstUser, getSecondUser, getThirdUser, getFourthUser] =
users.map(getUser);

getFirstUser()
.then(getSecondUser)
.then(getThirdUser)
.then(getFourthUser)
.catch(console.log);
};
const users = ['User1', 'User2', 'User3', 'User4'];

let response = [];

function getUsers(users) {
const promises = [];
promises[0] = axios.get(`/users/userId=${users[0]}`);
promises[1] = axios.get(`/users/userId=${users[1]}`);
promises[2] = axios.get(`/users/userId=${users[2]}`);
promises[3] = axios.get(`/users/userId=${users[3]}`);

Promise.all(promises)
.then(userDataArr => (response = userDataArr))
.catch(err => console.log(err));
}

Promise Combinator Array Functions

  • Promise.all(iterable) fail-fast: If at least one promise in the promises array rejects, then the promise returned rejects too. Short-circuits when an input value is rejected.
  • Promise.any(iterable): Resolves if any of the given promises are resolved. Short-circuits when an input value is fulfilled.
  • Promise.race(iterable): Short-circuits when an input value is settled (fulfilled or rejected).
  • Promise.allSettled(iterable): Returns when all given promises are settled (fulfilled or rejected).
Promise.all(urls.map(fetch))
.then(responses => Promise.all(responses.map(res => res.text())))
.then(texts => {
//
});

const loadData = async () => {
try {
const urls = ['...', '...'];

const results = await Promise.all(urls.map(fetch));
const dataPromises = await results.map(result => result.json());
const finalData = Promise.all(dataPromises);

return finalData;
} catch (err) {
console.log(err);
}
};

const data = loadData().then(data => console.log(data));

Promise Polyfill

class Promise {
// `executor` takes 2 parameters, `resolve()` and `reject()`. The executor
// function is responsible for calling `resolve()` or `reject()` to say that
// the async operation succeeded (resolved) or failed (rejected).
constructor(executor) {
if (typeof executor !== 'function') {
throw new TypeError('Executor must be a function');
}

// Internal state. `$state` is the state of the promise, and `$chained` is
// an array of the functions we need to call once this promise is settled.
this.$state = 'PENDING';
this.$chained = [];

// Implement `resolve()` and `reject()` for the executor function to use
const resolve = res => {
// A promise is considered "settled" when it is no longer
// pending, that is, when either `resolve()` or `reject()`
// was called once. Calling `resolve()` or `reject()` twice
// or calling `reject()` after `resolve()` was already called
// are no-ops.
if (this.$state !== 'PENDING') {
return;
}

// If `res` is a "thenable", lock in this promise to match the
// resolved or rejected state of the thenable.
const then = res !== null ? res.then : null;
if (typeof then === 'function') {
// In this case, the promise is "resolved", but still in the 'PENDING'
// state. This is what the ES6 spec means when it says "A resolved promise
// may be pending, fulfilled or rejected" in
// http://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects
return then(resolve, reject);
}

this.$state = 'FULFILLED';
this.$internalValue = res;

// If somebody called `.then()` while this promise was pending, need
// to call their `onFulfilled()` function
for (const { onFulfilled } of this.$chained) {
onFulfilled(res);
}

return res;
};

const reject = err => {
if (this.$state !== 'PENDING') {
return;
}

this.$state = 'REJECTED';
this.$internalValue = err;

for (const { onRejected } of this.$chained) {
onRejected(err);
}
};

// Call the executor function with `resolve()` and `reject()` as in the spec.
try {
// If the executor function throws a sync exception, we consider that
// a rejection. Keep in mind that, since `resolve()` or `reject()` can
// only be called once, a function that synchronously calls `resolve()`
// and then throws will lead to a fulfilled promise and a swallowed error
executor(resolve, reject);
} catch (err) {
reject(err);
}
}

// `onFulfilled` is called if the promise is fulfilled, and `onRejected`
// if the promise is rejected. For now, you can think of 'fulfilled' and
// 'resolved' as the same thing.
then(onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
// Ensure that errors in `onFulfilled()` and `onRejected()` reject the
// returned promise, otherwise they'll crash the process. Also, ensure
// that the promise
const _onFulfilled = res => {
try {
// If `onFulfilled()` returns a promise, trust `resolve()` to handle
// it correctly.
// store new value to new Promise
resolve(onFulfilled(res));
} catch (err) {
reject(err);
}
};

const _onRejected = err => {
try {
// store new value to new Promise
reject(onRejected(err));
} catch (_err) {
reject(_err);
}
};

switch (this.$state) {
case 'FULFILLED':
_onFulfilled(this.$internalValue);
break;
case 'REJECTED':
_onRejected(this.$internalValue);
break;
default:
this.$chained.push({
onFulfilled: _onFulfilled,
onRejected: _onRejected,
});
}
});
}

catch(onRejected) {
return this.then(null, onRejected);
}

finally(callback) {
return this.then(
value => {
return Promise.resolve(callBack()).then(() => value);
},
reason => {
return Promise.resolve(callBack()).then(() => {
throw reason;
});
}
);
}

static all(iterable) {
return new Promise((resolve, reject) => {
let index = 0;
let pendingCount = 0;
const result = new Array(iterable.length);

for (const promise of iterable) {
const currentIndex = index;
promise.then(
// eslint-disable-next-line no-loop-func
value => {
result[currentIndex] = value;
pendingCount++;

if (pendingCount === iterable.length) {
resolve(result);
}
},
err => {
reject(err);
}
);
index++;
}

if (index === 0) {
resolve([]);
}
});
}

static any(iterable) {
return new Promise((resolve, reject) => {
let index = 0;
let pendingCount = 0;
const error = new Error('All promise were rejected');
error.errors = new Array(iterable.length);

for (const promise of iterable) {
const currentIndex = index;
promise.then(
value => {
resolve(value);
},
// eslint-disable-next-line no-loop-func
err => {
error.errors[currentIndex] = err;
pendingCount++;

if (pendingCount === iterable.length) {
reject(error);
}
}
);
index++;
}

if (index === 0) {
resolve([]);
}
});
}

static race(iterable) {
return new Promise((resolve, reject) => {
for (const promise of iterable) {
promise.then(
value => {
resolve(value);
},
err => {
reject(err);
}
);
}
});
}

static allSettled(iterable) {
return new Promise((resolve, reject) => {
let index = 0;
let pendingCount = 0;
let result;

function addElementToResult(i, elem) {
result[i] = elem;
pendingCount++;

if (pendingCount === result.length) {
resolve(result);
}
}

for (const promise of iterable) {
const currentIndex = index;
promise.then(
value =>
addElementToResult(currentIndex, {
status: 'fulfilled',
value,
}),
reason =>
addElementToResult(currentIndex, {
status: 'rejected',
reason,
})
);
index++;
}

if (index === 0) {
resolve([]);
return;
}

result = new Array(index);
});
}
}

Memorize Async Function

const memo = {};
const progressQueues = {};

function memoProcessData(key) {
return new Promise((resolve, reject) => {
if (Object.prototype.hasOwnProperty.call(memo, key)) {
resolve(memo[key]);
return;
}

if (!Object.prototype.hasOwnProperty.call(progressQueues, key)) {
// Called for a new key
// Create an entry for it in progressQueues
progressQueues[key] = [[resolve, reject]];
} else {
// Called for a key that's still being processed
// Enqueue it's handlers and exit.
progressQueues[key].push([resolve, reject]);
return;
}

processData(key)
.then(data => {
memo[key] = data;
for (const [resolver] of progressQueues[key]) resolver(data);
})
.catch(error => {
for (const [, rejector] of progressQueues[key]) rejector(error);
})
.finally(() => {
delete progressQueues[key];
});
});
}

Async and Await

Await Features

  • async 异步函数如果不包含 await 关键字, 其执行 (除返回值外) 基本上跟普通函数没有什么区别.
  • JavaScript 运行时在碰到 await 关键字时, 会记录在哪里暂停执行.
  • 等到 await 右边的值可用了, JavaScript 运行时会向消息队列中推送一个任务, 这个任务会恢复异步函数的执行.
  • 即使 await 后面跟着一个立即可用的值, 函数的其余部分也会被异步求值.
async function foo() {
console.log(2);
}

console.log(1);
foo();
console.log(3);
// 1
// 2
// 3

async function bar() {
console.log(2);
await null;
console.log(4);
}

console.log(1);
bar();
console.log(3);
// 1
// 2
// 3
// 4
  • Await thenable object (implements then interface):
async function bar() {
const thenable = {
then(callback) {
callback('bar');
},
};
return thenable;
}

bar().then(console.log);
// bar

async function baz() {
const thenable = {
then(callback) {
callback('baz');
},
};
console.log(await thenable);
}

baz();
// baz
  • async/await implement generator based asynchronous control flow:
const fetchJson = co.wrap(function* (url) {
try {
const response = yield fetch(url);
const text = yield response.text();
return JSON.parse(text);
} catch (error) {
console.log(`ERROR: ${error.stack}`);
}
});

async function fetchJson(url) {
try {
const response = await fetch(url);
const text = await response.text();
return JSON.parse(text);
} catch (error) {
console.log(`ERROR: ${error.stack}`);
}
}
  • async 函数自动将返回值包装为 Promise:
// BAD.
async function downloadContent(urls) {
const promiseArray = urls.map(fetch);
return await Promise.all(promiseArray);
}

// GOOD.
async function downloadContent(urls) {
const promiseArray = urls.map(fetch);
return Promise.all(promiseArray);
}

Await Arrays

  • If you want to execute await calls in series, use a for-loop (or any loop without a callback).
  • Don't ever use await with forEach (forEach is not promise-aware), use a for-loop (or any loop without a callback) instead.
  • Don't await inside filter and reduce, always await an array of promises with map, then filter or reduce accordingly.
  • Avoid wrong parallel logic (too sequential):
// Wrong:
const books = await bookModel.fetchAll();
const author = await authorModel.fetch(authorId);

// Correct:
const bookPromise = bookModel.fetchAll();
const authorPromise = authorModel.fetch(authorId);
const book = await bookPromise;
const author = await authorPromise;

async function getAuthors(authorIds) {
// WRONG, this will cause sequential calls
// const authors = authorIds.map(id => await authorModel.fetch(id));
// CORRECT:
const promises = authorIds.map(id => authorModel.fetch(id));
const authors = await Promise.all(promises);
}
async function randomDelay(id) {
const delay = Math.random() * 1000;
return new Promise(resolve =>
setTimeout(() => {
console.log(`${id} finished`);
resolve(id);
}, delay)
);
}

async function sequential() {
const t0 = Date.now();

for (let i = 0; i < 5; ++i) {
await randomDelay(i);
}

console.log(`${Date.now() - t0}ms elapsed`);
}

sequential();
// 0 finished
// 1 finished
// 2 finished
// 3 finished
// 4 finished
// 2877ms elapsed

async function parallel() {
const t0 = Date.now();
const promises = Array(5)
.fill(null)
.map((_, i) => randomDelay(i));

for (const p of promises) {
console.log(`awaited ${await p}`);
}

console.log(`${Date.now() - t0}ms elapsed`);
}

parallel();
// 4 finished
// 2 finished
// 1 finished
// 0 finished
// 3 finished
// awaited 0
// awaited 1
// awaited 2
// awaited 3
// awaited 4
// 645ms elapsed

Asynchronous JavaScript

Sleep Function

function sleep(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
sleep(2000).then(() => {
// do something after 2000 milliseconds
console.log('resolved');
});

async function add(n1, n2) {
await sleep(2222);
console.log(n1 + n2);
}

add(1, 2);

Race Condition

  • Keep latest updates.
  • Recover from failures.
  • Online and offline sync (PouchDB).
  • Tools: redux-saga.
// eslint-disable-next-line import/no-anonymous-default-export
export default {
data() {
return {
text: '',
results: [],
nextRequestId: 1,
displayedRequestId: 0,
};
},
watch: {
async text(value) {
const requestId = this.nextRequestId++;
const results = await search(value);

// guarantee display latest search results (when input keep changing)
if (requestId < this.displayedRequestId) {
return;
}

this.displayedRequestId = requestId;
this.results = results;
},
},
};

Web Worker

  • 多线程并行执行.
  • 利用 BroadcastChannel API 可以创建 Shared Worker, 即共享 Workers 在同一源 (origin) 下面的各种进程都可以访问它, 包括: iframe/浏览器中的不同 Tab 页 (Browsing Context).
  • Use Case:
    • Graphic App (Ray Tracing).
    • Encryption.
    • Prefetching Data.
    • PWA (Service Worker).
    • Spell Checking.
<button onclick="startComputation()">Start computation</button>

<script>
const worker = new Worker('worker.js');

worker.addEventListener(
'message',
function (e) {
console.log(e.data);
},
false
);

function startComputation() {
worker.postMessage({ cmd: 'average', data: [1, 2, 3, 4] });
}
</script>
// worker.js
// eslint-disable-next-line no-restricted-globals
self.addEventListener(
'message',
function (e) {
const data = e.data;
switch (data.cmd) {
case 'average': {
const result = calculateAverage(data);
// eslint-disable-next-line no-restricted-globals
self.postMessage(result);
break;
}
default:
// eslint-disable-next-line no-restricted-globals
self.postMessage('Unknown command');
}
},
false
);

Web Worker Runtime

  • Web Worker 无法访问一些非常关键的 JavaScript 特性: DOM (线程不安全), window 对象, document 对象, parent 对象.
  • self 上可用的属性是 window 对象上属性的严格子集, WorkerGlobalScope:
    • navigation 对象: appName, appVersion, userAgent, platform.
    • location 对象: 所有属性只读.
    • ECMAScript 对象: Object/Array/Date.
    • console 对象.
    • setTimeout/setInterval 方法.
    • XMLHttpRequest 方法.
    • fetch 方法.
    • caches 对象: ServicerWorker CacheStorage 对象.
    • self 对象: 指向全局 worker 对象.
    • close 方法: 停止 worker.
    • importScripts 方法: 加载外部依赖.
    • MessagePort 方法: postMessage/onmessage/onmessageerror.
  • 工作者线程的脚本文件只能从与父页面相同的源加载, 从其他源加载工作者线程的脚本文件会导致错误. 在工作者线程内部可以使用 importScripts() 可以加载其他源的脚本.

Web Worker Basic Usage

  • on, 后 post.
  • main.js/worker.jsonmessagepostMessage 相互触发.
  • 有两种方法可以停止 Worker: 从主页调用 worker.terminate() 或在 worker 内部调用 self.close().
/*
* JSONParser.js
*/
// eslint-disable-next-line no-restricted-globals
self.onmessage = function (event) {
const jsonText = event.data;
const jsonData = JSON.parse(jsonText);

// eslint-disable-next-line no-restricted-globals
self.postMessage(jsonData);
};
/*
* main.js
*/
const worker = new Worker('JSONParser.js');

worker.onmessage = function (event) {
const jsonData = event.data;
evaluateData(jsonData);
};

worker.postMessage(jsonText);
// main.js
function work() {
onmessage = ({ data: { jobId, message } }) => {
console.log(`I am worker, I receive:-----${message}`);
postMessage({ jobId, result: 'message from worker' });
};
}

const makeWorker = f => {
const pendingJobs = {};
const workerScriptBlobUrl = URL.createObjectURL(
new Blob([`(${f.toString()})()`])
);
const worker = new Worker(workerScriptBlobUrl);

worker.onmessage = ({ data: { result, jobId } }) => {
// 调用 resolve, 改变 Promise 状态
pendingJobs[jobId](result);
delete pendingJobs[jobId];
};

return (...message) =>
new Promise(resolve => {
const jobId = String(Math.random());
pendingJobs[jobId] = resolve;
worker.postMessage({ jobId, message });
});
};

const testWorker = makeWorker(work);

testWorker('message from main thread').then(message => {
console.log(`I am main thread, I receive:-----${message}`);
});

Web Worker Pool

class TaskWorker extends Worker {
constructor(notifyAvailable, ...workerArgs) {
super(...workerArgs);

// 初始化为不可用状态
this.available = false;
this.resolve = null;
this.reject = null;

// 线程池会传递回调
// 以便工作者线程发出它需要新任务的信号
this.notifyAvailable = notifyAvailable;

// 线程脚本在完全初始化之后
// 会发送一条"ready"消息
this.onmessage = () => this.setAvailable();
}

// 由线程池调用, 以分派新任务
dispatch({ resolve, reject, postMessageArgs }) {
this.available = false;
this.onmessage = ({ data }) => {
resolve(data);
this.setAvailable();
};
this.onerror = e => {
reject(e);
this.setAvailable();
};
this.postMessage(...postMessageArgs);
}

setAvailable() {
this.available = true;
this.resolve = null;
this.reject = null;
this.notifyAvailable();
}
}

class WorkerPool {
constructor(poolSize, ...workerArgs) {
this.taskQueue = [];
this.workers = [];

// 初始化线程池
for (let i = 0; i < poolSize; ++i) {
this.workers.push(
new TaskWorker(() => this.dispatchIfAvailable(), ...workerArgs)
);
}
}

// 把任务推入队列
enqueue(...postMessageArgs) {
return new Promise((resolve, reject) => {
this.taskQueue.push({ resolve, reject, postMessageArgs });
this.dispatchIfAvailable();
});
}

// 把任务发送给下一个空闲的线程
dispatchIfAvailable() {
if (!this.taskQueue.length) {
return;
}

for (const worker of this.workers) {
if (worker.available) {
const a = this.taskQueue.shift();
worker.dispatch(a);
break;
}
}
}

// 终止所有工作者线程
close() {
for (const worker of this.workers) {
worker.terminate();
}
}
}
// worker.js
self.onmessage = ({ data }) => {
const view = new Float32Array(data.arrayBuffer);
let sum = 0;
// 求和
for (let i = data.startIdx; i < data.endIdx; ++i) {
// 不需要原子操作, 因为只需要读
sum += view[i];
}
// 把结果发送给工作者线程
self.postMessage(sum);
};
// 发送消息给 TaskWorker
// 通知工作者线程准备好接收任务了
self.postMessage('ready');

// main.js
const totalFloats = 1e8;
const numTasks = 20;
const floatsPerTask = totalFloats / numTasks;
const numWorkers = 4;

// 创建线程池
const pool = new WorkerPool(numWorkers, './worker.js');

// 填充浮点值数组
const arrayBuffer = new SharedArrayBuffer(4 * totalFloats);
const view = new Float32Array(arrayBuffer);

for (let i = 0; i < totalFloats; ++i) {
view[i] = Math.random();
}

const partialSumPromises = [];

for (let i = 0; i < totalFloats; i += floatsPerTask) {
partialSumPromises.push(
pool.enqueue({
startIdx: i,
endIdx: i + floatsPerTask,
arrayBuffer,
})
);
}

// 求和
Promise.all(partialSumPromises)
.then(partialSums => partialSums.reduce((x, y) => x + y))
.then(console.log);
// (在这个例子中, 和应该约等于 1E8/2)
// 49997075.47203197

Web Worker Performance

  • Web Worker performance guide.

Abort Controller

Abort Fetching

import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';

interface Post {
id: number;
title: string;
body: string;
}

function usePostLoading() {
const { postId } = useParams<{ postId: string }>();
const [isLoading, setIsLoading] = useState(false);
const [post, setPost] = useState<Post | null>(null);

useEffect(() => {
const abortController = new AbortController();

setIsLoading(true);
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
signal: abortController.signal,
})
.then(response => {
if (response.ok) {
return response.json();
}

return Promise.reject(Error('The request failed.'));
})
.then((fetchedPost: Post) => {
setPost(fetchedPost);
})
.catch(err => {
if (abortController.signal.aborted) {
console.log('The user aborted the request');
} else {
console.error(err.message);
}
})
.finally(() => {
setIsLoading(false);
});

return () => {
abortController.abort();
};
}, [postId]);

return {
post,
isLoading,
};
}

export default usePostLoading;

Abort Promise

function wait(time: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
const timeoutId = setTimeout(() => {
resolve();
}, time);
signal?.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(Error('Aborted.'));
});
});
}

const abortController = new AbortController();

setTimeout(() => {
abortController.abort();
}, 1000);

wait(5000, abortController.signal)
.then(() => {
console.log('5 seconds passed');
})
.catch(() => {
console.log('Waiting was interrupted');
});

Abort Controller Helpers

Abort controller helpers polyfill:

if (!timeout in AbortSignal) {
AbortSignal.timeout = function abortTimeout(ms) {
const controller = new AbortController();
setTimeout(() => controller.abort(), ms);
return controller.signal;
};
}

if (!any in AbortSignal) {
AbortSignal.any = function abortAny(signals) {
const controller = new AbortController();
signals.forEach(signal => {
if (signal.aborted) {
controller.abort();
} else {
signal.addEventListener('abort', () => controller.abort());
}
});
return controller.signal;
};
}

Asynchronous API Comparison

  • promiseasync/await 专门用于处理异步操作.
  • generator 并不是专门为异步设计, 它还有其他功能 (对象迭代/控制输出/Iterator Interface/etc).
  • promise 编写代码相比 generator/async/await 更为复杂化, 且可读性也稍差.
  • generator/async/await 需要与 promise 对象搭配处理异步情况.
  • async/await 使用上更为简洁, 将异步代码以同步的形式进行编写, 是处理异步编程的最终方案.

Module

CRUST Principles

  • Consistent: ES6 API design Array.XXX(fn).
  • Resilient: jQuery sizzle API design $(element)/$(selector)/$(selector, context).
  • Unambiguous.
  • Simple: Simple fetch API design.
  • Tiny: Tiny surface areas.

Namespace Module Pattern

Namespace Module Constructor

  • 命名空间.
  • 依赖模式.
  • 私有属性/特权方法.
  • 初始化模式.
  • 揭示模式: 公共接口.
  • 即时函数模式.
APP.namespace = function (namespaceString) {
let parts = namespaceString.split('.');
let parent = APP;
let i;
// strip redundant leading global
if (parts[0] === 'APP') {
// remove leading global
parts = parts.slice(1);
}
for (i = 0; i < parts.length; i += 1) {
// create a property if it doesn't exist
if (typeof parent[parts[i]] === 'undefined') {
parent[parts[i]] = {};
}
// 关键: 向内嵌套
parent = parent[parts[i]];
}
// 返回最内层模块名
return parent;
};
// assign returned value to a local var
const module2 = APP.namespace('APP.modules.module2');
const truthy = module2 === APP.modules.module2; // true
// skip initial `APP`
APP.namespace('modules.module51');
// long namespace
APP.namespace('once.upon.a.time.there.was.this.long.nested.property');

Namespace Module Usage

通过传参匿名函数, 创建命名空间, 进行模块包裹:

const app = {};

(function (exports) {
(function (exports) {
const api = {
moduleExists: function test() {
return true;
},
};
// 闭包式继承,扩展exports对象为api对象
$.extend(exports, api);
})(typeof exports === 'undefined' ? window : exports);
// 将api对象绑定至app对象上
})(app);
// global object
const APP = {};
// constructors
APP.Parent = function () {};
APP.Child = function () {};
// a variable
APP.some_var = 1;
// an object container
APP.modules = {};
// nested objects
APP.modules.module1 = {};
APP.modules.module1.data = { a: 1, b: 2 };
APP.modules.module2 = {};
// 命名空间模式
APP.namespace('APP.utilities.array');

// 形参: 导入全局变量
APP.utilities.array = (function (app, global) {
// 依赖模式
const uObj = app.utilities.object;
const uLang = app.utilities.lang;

// 私有属性
const arrStr = '[object Array]';
const toStr = Object.prototype.toString;

// 私有方法
const inArray = function (haystack, needle) {
for (let i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}

return -1;
};
const isArray = function (a) {
return toStr.call(a) === arrayString;
};

// 初始化模式:
// 初始化代码, 只执行一次.

// 揭示公共接口.
return {
isArray,
indexOf: inArray,
};
})(APP, this);

Sandbox Module Pattern

Sandbox Module Constructor

  • 私有属性绑定至 this/prototype.
  • 特权方法绑定至 modules/prototype.
function Sandbox(...args) {
// the last argument is the callback
const callback = args.pop();
// modules can be passed as an array or as individual parameters
let modules = args[0] && typeof args[0] === 'string' ? args : args[0];

// make sure the function is called
// as a constructor
if (!(this instanceof Sandbox)) {
return new Sandbox(modules, callback);
}

// add properties to `this` as needed:
this.a = 1;
this.b = 2;

// now add modules to the core `this` object
// no modules or "*" both mean "use all modules"
if (!modules || modules === '*') {
modules = [];
for (const i in Sandbox.modules) {
if (Object.prototype.hasOwnProperty.call(Sandbox.modules, i)) {
modules.push(i);
}
}
}

// initialize the required modules
for (let i = 0; i < modules.length; i += 1) {
Sandbox.modules[modules[i]](this);
}

// call the callback
callback(this);
}
// any prototype properties as needed
Sandbox.prototype = {
name: 'My Application',
version: '1.0',
getName() {
return this.name;
},
};

静态属性: 使用添加的方法/模块:

Sandbox.modules = {};
Sandbox.modules.dom = function (box) {
box.getElement = function () {};
box.getStyle = function () {};
box.foo = 'bar';
};
Sandbox.modules.event = function (box) {
// access to the Sandbox prototype if needed:
// box.constructor.prototype.m = "mmm";
box.attachEvent = function () {};
box.detachEvent = function () {};
};
Sandbox.modules.ajax = function (box) {
box.makeRequest = function () {};
box.getResponse = function () {};
};

Sandbox Module Usage

Sandbox(['ajax', 'event'], function (box) {
// console.log(box);
});

Sandbox('*', function (box) {
// console.log(box);
});
Sandbox(function (box) {
// console.log(box);
});

Sandbox('dom', 'event', function (box) {
// work with dom and event
Sandbox('ajax', function (box) {
// another "box" object
// this "box" is not the same as
// the "box" outside this function
// ...
// done with Ajax
});
// no trace of Ajax module here
});

CommonJS Pattern

  • 无论一个模块在 require() 中被引用多少次, 模块永远是单例, 只会被加载一次.
  • 模块第一次加载后会被缓存, 后续加载会取得缓存的模块.
  • 模块加载是模块系统执行的同步操作, require() 可以位于条件语句中.

Minimal CJS bundler:

require.cache = Object.create(null);

// Construct 'require', 'module' and 'exports':
function require(moduleId) {
if (!(moduleId in require.cache)) {
const code = readFile(moduleId);
const module = { exports: {} };
require.cache[moduleId] = module;
// eslint-disable-next-line no-new-func
const wrapper = Function('require, exports, module', code);
// Bind code to module.exports:
wrapper(require, module.exports, module);
}
return require.cache[moduleId].exports;
}

AMD Pattern

Asynchronous module definition:

// ID 为 'moduleA' 的模块定义:
// moduleA 依赖 moduleB.
// moduleB 会异步加载.
define('moduleA', ['moduleB'], function (moduleB) {
return {
stuff: moduleB.doStuff(),
};
});
define('moduleA', ['require', 'exports'], function (require, exports) {
const moduleB = require('moduleB');

if (condition) {
const moduleC = require('moduleC');
}

exports.stuff = moduleB.doStuff();
});

UMD Pattern

Universal module definition:

  • 判断是否支持 AMD (define), 存在则使用 AMD 方式加载模块.
  • 判断是否支持 Node.js 的模块 (exports), 存在则使用 Node.js 模块模式.
/**
* UMD Boilerplate.
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], function () {
return factory(root);
});
} else if (typeof exports === 'object') {
module.exports = factory(root);
} else {
root.myPlugin = factory(root);
}
})(
typeof global !== 'undefined'
? global
: typeof window !== 'undefined'
? window
: this,
function (window) {
'use strict';

// Module code goes here...
return {};
}
);

ES6 Module

ES6 Module Features

  • Singleton:
    • 模块是单例.
    • 模块只能加载一次: 同一个模块无论在一个页面中被加载多少次, 也不管它是如何加载的, 实际上都只会加载一次.
  • Imports:
    • 模块可以请求加载其他模块.
    • 模块支持循环依赖.
    • Static and Read-only imports.
  • Exports:
    • 模块可以定义公共接口.
    • 其他模块可以基于这个公共接口观察和交互.
  • Local Scope:
    • 模块不共享全局命名空间.
    • 模块顶级 this 的值是 undefined (传统脚本中是 window).
    • 模块中的 var 声明不会添加到 window 对象.
  • Async:
    • 模块在浏览器中是异步加载和执行的.
    • 模块代码只在加载后执行.
    • 解析到 <script type="module"> 标签后会立即下载模块文件, 但执行会延迟到 HTML 文档解析完成 (<script defer>).
  • Strict:
    • 模块代码默认在严格模式下执行.
  • Static:
    • Static and Read-only imports: 模块是静态结构.
      • Imported module is Pre-parsed: imported modules get run first, code which imports module gets executed after.
      • Imported module is Read-only: code which imports module cannot modify imported module, only module which exports them can change its value.
    • Static analysis.
    • Tree shaking.
    • Compact bundling.
    • Faster imports lookup.
<!-- 支持模块的浏览器会执行这段脚本 -->
<!-- 不支持模块的浏览器不会执行这段脚本 -->
<script type="module" src="module.js"></script>

<!-- 支持模块的浏览器不会执行这段脚本 -->
<!-- 不支持模块的浏览器会执行这段脚本 -->
<script nomodule src="script.js"></script>

ES6 Module Syntax

import { lastName as surname } from './profile.js';
import module from './module.js';
import * as Bar from './bar.js'; // Object.freeze(Bar)
import './foo.js'; // Load effects
export const firstName = 'Michael';
export const lastName = 'Jackson'