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>>
throw?: (e?: any) => Promise<IteratorResult<T>>
// Closable iterator
return?: (value?: T | PromiseLike<T>) => 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, () => {
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(() => {
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 = []

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

function 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) => {
//
})

async function loadData() {
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 = Array.from({ length: iterable.length })

for (const promise of iterable) {
const currentIndex = index
promise.then(

(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 = Array.from({ length: iterable.length })

for (const promise of iterable) {
const currentIndex = index
promise.then(
(value) => {
resolve(value)
},

(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 = Array.from({ length: 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.
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',
(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' })
}
}

function 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'], (box) => {
// console.log(box);
})

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

Sandbox('dom', 'event', (box) => {
// work with dom and event
Sandbox('ajax', (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'], (moduleB) => {
return {
stuff: moduleB.doStuff(),
}
})
define('moduleA', ['require', 'exports'], (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([], () => {
return factory(root)
})
} else if (typeof exports === 'object') {
module.exports = factory(root)
} else {
root.myPlugin = factory(root)
}
})(
typeof globalThis !== 'undefined'
? globalThis
: typeof window !== 'undefined'
? window
: this,
(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'
export const year = 1958
export function foo() {}
export function* bar() {}
export class Foo {}
// profile.js
const firstName = 'Michael'
const lastName = 'Jackson'
const year = 1958

export { firstName, lastName, year }
// 接口改名
export { foo as myFoo } from 'node:module'
export { default as Article } from './Article'

// 整体输出
export * from 'utils'

ES6 Module Imports

Import meta import.meta:

// index.mjs
import './index2.mjs?someURLInfo=5'

// index2.mjs
new URL(import.meta.url).searchParams.get('someURLInfo') // 5
const urlOfData = new URL('data.txt', import.meta.url)

Import assertion:

import data from './data.json' assert { type: 'json' }

console.log(data)

Import map importmap:

<script type="importmap">
{
"imports": {
"ms": "https://cdn.skypack.dev/ms"
"lodash": "https://cdn.skypack.dev/lodash",
"lodash": "https://cdn.skypack.dev/lodash/",
}
}
</script>
<script type="module">
import get from 'lodash/get.js'
import lodash from 'lodash'
import('lodash').then((_) => {})
</script>
Imports Order
  • Polyfills: import 'reflect-metadata';.
  • Node builtin modules: import fs from 'node:fs';.
  • External modules: import { motion } from 'framer-motion';.
  • Internal modules: import { UserService } from 'src/services/userService';.
  • Parent directory modules: import foo from '../foo'; import qux from '../../foo/qux';.
  • Same/Sibling directory modules: import bar from './bar'; import baz from './bar/baz';.

ES6 Module Exports

  • CommonJS 模块是运行时加载, ES6 模块是编译时输出接口.
  • CommonJS 是单个值导出, ES6 Module 可以导出多个.
  • CommonJS 是动态语法可以写在判断里, ES6 Module 是静态语法只能写在顶层.
  • CommonJS 的 this 是当前模块, ES6 Module 的 thisundefined.
  • CommonJS 模块输出的是一个值的拷贝, ES6 模块 export 分多种情况:
    1. export default xxx 输出 value: defaultThing and anotherDefaultThing shows ES6 export default value,
    2. export xxx 输出 reference: importedThing and module.thing shows ES6 export live reference,
    3. Destructuring behavior create a brand new value.
    4. function/class special case: export default function/class thing() {}; // function/class expressions export live reference, function/class thing() {}; export default thing; // function/class statements export default value.

Export default value:

// module.js
let thing = 'initial'

export { thing }
export default thing

setTimeout(() => {
thing = 'changed'
}, 500)
// main.js
import { default as defaultThing, thing } from './module.js'
import anotherDefaultThing from './module.js'

setTimeout(() => {
console.log(thing) // "changed"
console.log(defaultThing) // "initial"
console.log(anotherDefaultThing) // "initial"
}, 1000)

Export live reference:

// module.js
export let thing = 'initial'

setTimeout(() => {
thing = 'changed'
}, 500)
// main.js
import { thing as importedThing } from './module.js'
const module = await import('./module.js')
let { thing } = await import('./module.js') // Destructuring behavior

setTimeout(() => {
console.log(importedThing) // "changed"
console.log(module.thing) // "changed"
console.log(thing) // "initial"
}, 1000)

To sum up:

// Live reference:
import { thing }