JavaScript Advanced Notes
Iterator
Iteration Protocol
Iteration protocol:
- 一个数据结构只要实现了
[Symbol.iterator]()
接口, 便可成为可迭代数据结构 (Iterable
):- String:
StringIterator
. - Array:
ArrayIterator
. - Map:
MapIterator
. - Set:
SetIterator
. arguments
对象.- DOM collection (
NodeList
):ArrayIterator
.
- String:
- 接收可迭代对象的原生语言特性:
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 fromnext(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) => {})
.
- Return
- Simple error handling:
- Catch error:
promise.catch((err) => {})
. - Cleanup:
promise.finally(() => {})
.
- Catch error:
- Simple composition:
Promise.all
: Converts anArray
ofPromises
to aPromise
for anArray
.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 (implementsthen
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.js
的onmessage
与postMessage
相互触发.- 有两种方法可以停止 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
promise
和async/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()
可以位于条件语句中.
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
andRead-only
imports.
- Exports:
- 模块可以定义公共接口.
- 其他模块可以基于这个公共接口观察和交互.
- Local Scope:
- 模块不共享全局命名空间.
- 模块顶级
this
的值是undefined
(传统脚本中是window
). - 模块中的
var
声明不会添加到window
对象.
- Async:
- 模块在浏览器中是异步加载和执行的.
- 模块代码只在加载后执行.
- 解析到
<script type="module">
标签后会立即下载模块文件, 但执行会延迟到 HTML 文档解析完成 (<script defer>
).
- Strict:
- 模块代码默认在严格模式下执行.
- Static:
Static
andRead-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.
- Imported module is
- 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