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>>
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 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(new 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(new 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, () => {
throw new Error('bar')
})
// Uncaught (in promise) bar
setTimeout(console.log, 0, p10) // Promise <rejected>: bar
const p11 = p1.then(null, () => new 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(() => new 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 new Error('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(new Error('foo'))
console.log('bar')
// bar
// Uncaught (in promise) Error: foo
const p1 = new Promise((resolve, reject) => reject(new 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(new 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 (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.
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.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' })
}
}
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(new 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(new 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'], (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()
可以位于条件语句中.
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
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 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>
- 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 的this
是undefined
. - CommonJS 模块输出的是一个值的拷贝,
ES6 模块
export
分多种情况:export default xxx
输出value
:defaultThing
andanotherDefaultThing
shows ES6 export default value,export xxx
输出reference
:importedThing
andmodule.thing
shows ES6 export live reference,Destructuring
behavior create a brand new value.- 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 } from './module.js'
import { thing as otherName } from './module.js'
// Current value:
const { thing } = await import('./module.js')
// Live reference:
export { thing }
export { thing as otherName }
export { thing as default }
export default function thing() {}
// Current value:
export default thing
export default 'hello!'
Proxy and Reflect
Modify default object behavior with Proxy
and Reflect
:
new Proxy(target, handler)
.Proxy.revocable(target, handler)
.
Proxy(target, {
set(target, name, value, receiver) {
const success = Reflect.set(target, name, value, receiver)
if (success)
log(`property ${name} on ${target} set to ${value}`)
return success
},
})
Proxy
- Change original object will change proxy object.
- change proxy object will change original object via
set
related API. Proxy.prototype
为undefined
.target !== proxy
.
const target = {
id: 'target',
}
const handler = {}
const proxy = new Proxy(target, handler)
// Proxy.prototype 是 undefined
console.log(target instanceof Proxy) // TypeError
console.log(proxy instanceof Proxy) // TypeError
// 严格相等可以用来区分代理和目标
console.log(target === proxy) // false
this
binding should process carefully:
const proxy = new Proxy(new Date(), {})
proxy.getDate() // `getDate` rely on internal slots
// TypeError: `this` is not a Date object.
const handler = {
get(target, propKey, receiver) {
if (propKey === 'getDate')
return target.getDate.bind(target)
return Reflect.get(target, propKey, receiver)
},
}
const proxy = new Proxy(new Date('2020-12-24'), handler)
proxy.getDate() // 24
Reflect
Reflect.get(target, propKey)
.Reflect.set(target, propKey, value)
.Reflect.has(target, propKey)
:in
operator.Reflect.defineProperty(target, propKey, attributes)
.Reflect.getOwnPropertyDescriptor(target, propKey)
.Reflect.deleteProperty(target, propKey)
:delete
operator.Reflect.ownKeys(target)
:Object.keys()
+Object.getOwnPropertyNames()
+Object.getOwnPropertySymbols()
, all keys.Reflect.getPrototypeOf(target)
.Reflect.setPrototypeOf(target, prototype)
.Reflect.isExtensible(target)
.Reflect.preventExtensions(target)
.Reflect.apply(target, thisArgument, argumentsList)
: function call.Reflect.construct(target, argumentsList)
:new target(...argumentsList)
operator.
Proxy Behavior | Overrides Behavior |
---|---|
Reflect.get() | Reading a property value |
Reflect.set() | Writing to a property |
Reflect.has() | in operator |
Reflect.deleteProperty() | delete operator |
Reflect.getPrototypeOf() | Object.getPrototypeOf() |
Reflect.setPrototypeOf() | Object.setPrototypeOf() |
Reflect.isExtensible() | Object.isExtensible() |
Reflect.preventExtensions() | Object.preventExtensions() |
Reflect.getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor() |
Reflect.defineProperty() | Object.defineProperty() |
Reflect.ownKeys() | All Object keys methods |
Reflect.apply() | Calling a function |
Reflect.construct() | Calling a function with new |
const target = {
foo: 'bar',
}
const proxy = new Proxy(target, Reflect)
console.log(proxy.foo) // bar
console.log(target.foo) // bar
Reflect.ownKeys({ z: 3, y: 2, x: 1 }) // [ "z", "y", "x" ]
Reflect.ownKeys([]) // ["length"]
const sym = Symbol.for('comet')
const sym2 = Symbol.for('meteor')
const obj = {
[sym]: 0,
'str': 0,
'773': 0,
'0': 0,
[sym2]: 0,
'-1': 0,
'8': 0,
'second str': 0,
}
Reflect.ownKeys(obj)
// [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]
// Indexes in numeric order,
// strings in insertion order,
// symbols in insertion order.
CommonJS (CJS
) to ES Module (ESM
) exports:
const esm$1 = { exports: {} }
;(function (module, exports) {
module.exports = () => {}
exports.a = 3
exports.b = 4
})(esm$1, esm$1.exports)
const esm = esm$1.exports
export { esm as default }
Proxy Usage
Default Zero Value Protection
function withZeroValue(target, zeroValue = 0) {
return new Proxy(target, {
get: (obj, prop) => (prop in obj ? obj[prop] : zeroValue),
})
}
let pos = { x: 4, y: 19 }
console.log(pos.z) // => undefined
pos = withZeroValue(pos)
console.log(pos.z) // => 0
Hiding Properties Protection
function hide(target, prefix = '_') {
return new Proxy(target, {
has: (obj, prop) => !prop.startsWith(prefix) && prop in obj,
ownKeys: obj =>
Reflect.ownKeys(obj).filter(
prop => typeof prop !== 'string' || !prop.startsWith(prefix)
),
get: (obj, prop, rec) => (prop in rec ? obj[prop] : undefined),
})
}
const userData = hide({
firstName: 'Tom',
mediumHandle: '@bar',
_favoriteRapper: 'Drake',
})
const falsy = '_favoriteRapper' in userData // has: false
const keys = Object.keys(userData) // ownKeys: ['firstName', 'mediumHandle']
console.log(userData._favoriteRapper) // get: undefined
Read Only Object Protection
function NOPE() {
throw new Error('Can\'t modify read-only object')
}
const NOPE_HANDLER = {
set: NOPE,
defineProperty: NOPE,
deleteProperty: NOPE,
preventExtensions: NOPE,
setPrototypeOf: NOPE,
get: (obj, prop) => {
if (prop in obj)
return Reflect.get(obj, prop)
throw new ReferenceError(`Unknown prop "${prop}"`)
},
}
const readOnly = target => new Proxy(target, NODE_HANDLER)
Range Validation
in
operator capture:
function range(min, max) {
return new Proxy(Object.create(null), {
has: (_, prop) => +prop >= min && +prop <= max,
})
}
const X = 10.5
const nums = [1, 5, X, 50, 100]
if (X in range(1, 100)) {
// => true
}
nums.filter(n => n in range(1, 10))
// => [1, 5]
Property Validation
set
operator capture:
const target = {
onlyNumbersGoHere: 0,
}
const proxy = new Proxy(target, {
set(target, property, value) {
if (typeof value !== 'number')
return false
else
return Reflect.set(target, property, value)
},
})
proxy.onlyNumbersGoHere = 1
console.log(proxy.onlyNumbersGoHere) // 1
proxy.onlyNumbersGoHere = '2'
console.log(proxy.onlyNumbersGoHere) // 1
Function Parameter Validation
apply
operator capture:
function median(...nums) {
return nums.sort()[Math.floor(nums.length / 2)]
}
const proxy = new Proxy(median, {
apply(target, thisArg, argumentsList) {
for (const arg of argumentsList) {
if (typeof arg !== 'number')
throw new TypeError('Non-number argument provided')
}
return Reflect.apply(target, thisArg, argumentsList)
},
})
console.log(proxy(4, 7, 1)) // 4
console.log(proxy(4, '7', 1))
// TypeError: Non-number argument provided
class Person {
constructor(name) {
this.name = name
}
}
const PersonProxy = new Proxy(Person, {
apply(TrapTarget, thisArg, argumentList) {
return new TrapTarget(...argumentList)
},
})
const me = PersonProxy('Nicholas')
console.log(me.name) // "Nicholas"
console.log(me instanceof Person) // true
console.log(me instanceof PersonProxy) // true
Constructor Parameter Validation
new
operator capture:
class User {
constructor(id) {
this.id_ = id
}
}
const ProxyUser = new Proxy(User, {
construct(target, argumentsList, newTarget) {
if (argumentsList[0] === undefined)
throw new Error('User cannot be instantiated without id')
else
return Reflect.construct(target, argumentsList, newTarget)
},
})
const obj = new ProxyUser(1)
const throwError = new ProxyUser()
// Error: User cannot be instantiated without id
Negative Array Indices Protection
function negativeArray(els) {
return new Proxy(target, {
get: (target, propKey, receiver) =>
Reflect.get(
target,
+propKey < 0 ? String(target.length + +propKey) : propKey,
receiver
),
})
}
Array Manipulation Protection
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % 2 ** 32
}
function isArrayIndex(key) {
const numericKey = toUint32(key)
return String(numericKey) === key && numericKey < 2 ** 32 - 1
}
class MyArray {
constructor(length = 0) {
this.length = length
return new Proxy(this, {
set(trapTarget, key, value) {
const currentLength = Reflect.get(trapTarget, 'length')
// the special case
if (isArrayIndex(key)) {
const numericKey = Number(key)
if (numericKey >= currentLength)
Reflect.set(trapTarget, 'length', numericKey + 1)
} else if (key === 'length') {
if (value < currentLength) {
for (let index = currentLength - 1; index >= value; index--)
Reflect.deleteProperty(trapTarget, index)
}
}
// always do this regardless of key type
return Reflect.set(trapTarget, key, value)
},
})
}
}
const colors = new MyArray(3)
console.log(colors instanceof MyArray) // true
console.log(colors.length) // 3
colors[0] = 'red'
colors[1] = 'green'
colors[2] = 'blue'
colors[3] = 'black'
console.log(colors.length) // 4
colors.length = 2
console.log(colors.length) // 2
console.log(colors[3]) // undefined
console.log(colors[2]) // undefined
console.log(colors[1]) // "green"
console.log(colors[0]) // "red"
Exception Protection
function createExceptionProxy(target) {
return new Proxy(target, {
get: (target, prop) => {
if (!(prop in target))
return
if (typeof target[prop] === 'function')
return createExceptionZone(target, prop)
return target[prop]
},
})
}
function createExceptionZone(target, prop) {
return (...args) => {
let result
ExceptionsZone.run(() => {
result = target[prop](...args)
})
return result
}
}
class ExceptionsZone {
static exceptionHandler = new ExceptionHandler()
static run(callback) {
try {
callback()
} catch (e) {
this.exceptionHandler.handle(e)
}
}
}
class ExceptionHandler {
handle(exception) {
console.log('记录错误: ', exception.message, exception.stack)
}
}
const obj = {
name: 'obj',
say() {
console.log(`Hi, I'm ${this.name}`)
},
coding() {
// xxx.
throw new Error('bug')
},
coding2() {
// xxx.
throw new Error('bug2')
},
}
const proxy = createProxy(obj)
proxy.say()
proxy.coding()
Proxy Pattern Implementation
- Remote mock and placeholder:
- Data mock.
- Image placeholder.
- Cache:
- Database objects access caching.
- Memoized functions.
- Profiling.
- Observer and watcher: Vue 3 reactivity, ImmerJS draft state.
Proxy and DefineProperty
- Simple:
Proxy
使用上比Object.defineProperty
方便.Object.defineProperty
只能监听对象, 导致Vue 2
data
属性必须通过一个返回对象的函数方式初始化,Vue 3
更加多元化, 可以监听任意数据.
- Performant:
Proxy
代理整个对象,Object.defineProperty
只代理对象上的某个属性.Object.defineProperty
由于每次只能监听对象一个键的get
/set
, 导致需要循 环监听浪费性能.Proxy
可以一次性监听到所有属性.
- Lazy:
Proxy
性能优于Object.defineProperty
.- 如果对象内部要全部递归代理, 则
Proxy
可以只在调用时递归. Object.defineProperty
需要在一开始就全部递归.
- 如果对象内部要全部递归代理, 则
- Feature:
- 对象上定义新属性时, 只有
Proxy
可以监听到:- Vue2: 提供
Vue.set
/Vue.delete
等辅助方法. - Vue3:
Proxy
监听新属性.
- Vue2: 提供
- 数组新增删除修改时, 只有
Proxy
可以监听到:Object.defineProperty
无法监听数组,Proxy
则可以直接监听数组变化.- Vue2: 重写数组方法监听数组变化.
- Vue3:
Proxy
监听数组变化.
- 对象上定义新属性时, 只有
Proxy
不兼容 IE,Object.defineProperty
不兼容 IE8 及以下.
Error and Exception
Error
Error Type
- Error.
- EvalError.
- RangeError.
- ReferenceError.
- SyntaxError.
- TypeError.
- URIError.
- AggregateError.
- 自定义错误.
class Error {
// Instance properties
message: string
cause?: any // ES2022
stack: string // non-standard but widely supported
constructor(message = '', options?: ErrorOptions) {
this.name = 'Error'
this.message = message
this.cause = options?.cause // ES2022: error chain.
}
}
interface ErrorOptions {
cause?: string | Error // ES2022
}
class CustomError extends Error {
constructor(message, options) {
super(message, options)
this.name = 'CustomError'
}
}
Error Object
const err = {
name: 'XXError',
message: 'something wrong',
extra: 'This was rather embarrassing',
remedy: genericErrorHandler, // 处理错误的函数名.
}
try {
throwError()
} catch (e) {
console.log(e.message)
e.remedy() // genericErrorHandler.
}
Exception
Call Stack Overflow
调用栈尺寸限制异常, 应立即定位在代码中的递归实例上:
try {
recursion()
} catch (ex) {
console.error('error info')
}
Exception Handling
try catch
.Promise.catch
.window.addEventListener('error', handler, true)
.window.addEventListener('unhandledrejection', handler, true)
.- Web Worker heartbeat monitoring.
process.on('uncaughtException', handleError)
.process.on('SIGHUP', handleExit)
.process.on('SIGINT', handleExit)
.process.on('SIGQUIT', handleExit)
.process.on('SIGTERM', handleExit)
.- AOP (Aspect Oriented Programming): Middleware/Interceptor/Monkey Patch.
- 在可能失败的地方抛出异常, 对失败处做标签, 易于调试与测试.
- 修复 bug 后, 可考虑是否在此处抛出异常.
- Avoid using try-catch inside a loop:
const object = ['foo', 'bar']
try {
for (let i = 0; i < object.length; i++) {
// do something that throws an exception
}
} catch (e) {
// handle exception
}
// 监听捕获阶段的异常事件
window.addEventListener(
'error',
(error) => {
handleError(error)
error.preventDefault()
},
true
)
// Un-catch `Promise` handler
window.addEventListener(
'unhandledrejection',
(error) => {
handleError(error)
error.preventDefault()
},
true
)
const instance = axios.create({
baseURL: 'https://api.test.com',
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
})
instance.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
// 发生异常会走到这里
if (error.response) {
const response = error.response
if (response.status >= 400)
handleError(response)
} else {
handleError(null)
}
return Promise.reject(error)
}
)
globalThis.onunhandledrejection = (event) => {
console.log(event.type)
// "unhandledrejection"
console.log(event.reason.message)
// "Oops!"
console.log(rejected === event.promise)
// true
}
globalThis.onrejectionhandled = (event) => {
console.log(event.type)
// "rejectionhandled"
console.log(event.reason.message)
// "Oops!"
console.log(rejected === event.promise)
// true
}
const possiblyUnhandledRejections = new Map()
// when a rejection is unhandled, add it to the map
globalThis.onunhandledrejection = (event) => {
// prevents the console warning
event.preventDefault()
possiblyUnhandledRejections.set(event.promise, event.reason)
}
// when a rejection is handled, remove it from the map
globalThis.onrejectionhandled = (event) => {
possiblyUnhandledRejections.delete(event.promise)
}
setInterval(() => {
possiblyUnhandledRejections.forEach((reason, promise) => {
console.error('Unhandled rejection')
console.error(promise)
console.error(reason.message ? reason.message : reason)
// do something to handle these rejections
})
possiblyUnhandledRejections.clear()
}, 60000)
const rejected = Promise.reject(new Error('Oops!'))
process.on('unhandledRejection', (reason, promise) => {
console.log(reason.message)
// "Oops!"
console.log(rejected === promise)
// true
})
const rejected = Promise.reject(new Error('Oops!'))
setTimeout(() => {
// "rejectionhandled" triggered here
rejected.catch(
reason => console.error(reason.message) // "Oops!"
)
}, 500)
process.on('rejectionHandled', (promise) => {
console.log(rejected === promise) // true
})
const possiblyUnhandledRejections = new Map()
// when a rejection is unhandled, add it to the map
process.on('unhandledRejection', (reason, promise) => {
possiblyUnhandledRejections.set(promise, reason)
})
process.on('rejectionHandled', (promise) => {
possiblyUnhandledRejections.delete(promise)
})
setInterval(() => {
possiblyUnhandledRejections.forEach((reason, promise) => {
console.error('Unhandled rejection')
console.error(promise)
console.error(reason.message ? reason.message : reason)
// do something to handle these rejections
})
possiblyUnhandledRejections.clear()
}, 60000)
Regular Expression
const re = /pattern/gi
RegExp Flags
g
(global): 全局匹配.i
(ignoreCase): 大小写不敏感匹配.m
(multiline): 多行匹配.y
(sticky): 粘附模式, 修饰符号隐含了头部匹配的标志.u
(unicode): Unicode 模式.s
(dotAll): Allows.
to matchnewline
characters.
function codePointLength(text) {
const result = text.match(/[\s\S]/gu)
return result ? result.length : 0
}
const s = '𠮷𠮷'
const length = s.length // 4
codePointLength(s) // 2
RegExp Character Classes
Characters | Meaning |
---|---|
. | [^\n\r\u2020\u2029] |
\d | [0-9] |
\D | [^0-9] |
\w | [0-9a-zA-Z_] |
\W | [^0-9a-zA-Z_] |
\s | [\r\n\f\t\v] |
\S | [^\r\n\f\t\v] |
\b | start/end of word |
\B | not start/end of word |
\p{UnicodeProperty} | match unicode |
\P{UnicodeProperty} | not match unicode |
^ | start of string |
$ | end of string |
RegExp Quantifiers
Quantifiers | Repeat Times |
---|---|
* | 0+ |
+ | 1+ |
? | 0 ~ 1 |
{n} | n |
{n,} | n+ |
{n,m} | n ~ m |
Lazy Quantifiers | Repeat Times (As Less As Possible) |
---|---|
*? | 0+ |
+? | 1+ |
?? | 0 ~ 1 |
{n,}? | n+ |
{n,m}? | n ~ m |
RegExp Group and Ranges
分类 | 代码/语法 | 说明 |
---|---|---|
捕获 | (exp) | 匹配 exp,并捕获文本到自动命名的组里 |
(?<name>exp) | 匹配 exp,并捕获文本到名称为 name 的组里 | |
(?:exp) | 匹配 exp,不捕获匹配的文本, 也不给此分组分配组号 | |
零宽断言 | (?<=exp) | 匹配左侧是 exp 的位置 |
(?<!exp) | 匹配左侧不是 exp 的位置 | |
(?=exp) | 匹配右侧是 exp 的位置 | |
(?!exp) | 匹配右侧不是 exp 的位置 | |
注释 | (?#comment) | 用于提供注释让人阅读 |
(?<=\d)th
->9th
.(?<!\d)th
->health
.six(?=\d)
->six6
.hi(?!\d)
->high
.
const string = 'Favorite GitHub Repos: tc39/ecma262 v8/v8.dev'
const regex = /\b(?<owner>[a-z0-9]+)\/(?<repo>[a-z0-9.]+)\b/g
for (const match of string.matchAll(regex)) {
console.log(`${match[0]} at ${match.index} with '${match.input}'`)
console.log(`owner: ${match.groups.owner}`)
console.log(`repo: ${match.groups.repo}`)
}
RegExp Back Reference
$1 $2 $3
: 第 n 个子表达式匹配的结果字符.- 位置编号 (左括号的顺序):
\1 \2 \3
: 第 n 个子表达式匹配的结果字符. - Named capture group back reference:
\k<Name>
. - 反向引用可以解决正则表达式回溯失控的问题 (ReDoS).
const regExp = /((<\/?\w+>.*)\2)/g
const text = 'ooo111ooo222ooo333ooo123'
const regExp = /(\d)\1\1/g
const result = text.match(regExp)
console.log(result) // [111, 222, 333]
Most RegExp.XXX
/RegExp.$X
static property aren't standard.
Avoid use them in production:
RegExp.input ($_)
.RegExp.lastMatch ($&)
.RegExp.lastParen ($+)
.RegExp.leftContext
.RegExp.rightContext ($')
.RegExp.$1-$9
.
RegExp Functions
RegExp functions:
- String:
split
.match
.search
.replace
.
- RegExp:
test
.exec
.
RegExp Test
;/[a-z|0-9]/i.test(str)
const ignoreList = [
// # All
'^npm-debug\\.log$', // Error log for npm
'^\\..*\\.swp$', // Swap file for vim state
// # macOS
'^\\.DS_Store$', // Stores custom folder attributes
'^\\.AppleDouble$', // Stores additional file resources
'^\\.LSOverride$', // Contains the absolute path to the app to be used
'^Icon\\r$', // Custom Finder icon: http://superuser.com/questions/298785/icon-file-on-os-x-desktop
'^\\._.*', // Thumbnail
'^\\.Spotlight-V100(?:$|\\/)', // Directory that might appear on external disk
'\\.Trashes', // File that might appear on external disk
'^__MACOSX$', // Resource fork
// # Linux
'~$', // Backup file
// # Windows
'^Thumbs\\.db$', // Image file cache
'^ehthumbs\\.db$', // Folder config file
'^Desktop\\.ini$', // Stores custom folder attributes
'@eaDir$', // "hidden" folder where the server stores thumbnails
]
export const junkRegex = new RegExp(ignoreList.join('|'))
export function isJunk(filename) {
return junkRegex.test(filename)
}
RexExp Exec
- Search starts at substring specified by
lastIndex
property.
const input = 'A string with 3 numbers in it... 42 and 88.'
const number = /\b\d+\b/g
for (
let match = number.exec(input);
match !== null;
match = number.exec(input)
)
console.log('Found', match[0], 'at', match.index)
// Found 3 at 14
// Found 42 at 33
// Found 88 at 40
RegExp Replace
str.replace(regExp, str / func)
RegExp Replace Arguments
第二个参数若为函数式参数, replace
方法会向它传递一系列参数:
- 第一个参数: 匹配结果字符串.
- 第 n 个参数: 子表达式匹配结果字符串.
- 倒数第二个参数: 匹配文本在源字符串中的下标位置.
- 最后一个参数: 源字符串自身.
function upper(all, letter) {
return letter.toUpperCase()
}
assert(
'border-bottom-width'.replace(/-(\w)/g, upper) === 'borderBottomWidth',
'Camel cased a hyphenated string.'
)
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
console.log('1999-12-31'.replace(RE_DATE, '$<month>/$<day>/$<year>'))
// 12/31/1999
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
console.log(
'1999-12-31'.replace(
RE_DATE,
(g0, y, m, d, offset, input, { year, month, day }) =>
`${month}/${day}/${year}`
)
)
// 12/31/1999
RegExp Replace Performance
- 使用 2 个子表达式修剪字符串, 字符串总长度影响性能.
- 使用循环修剪字符串 (分别用 正/负循环 修剪 首/尾空白符), 空白字符长度影响性能.
if (!String.prototype.trim) {
// eslint-disable-next-line no-extend-native
String.prototype.trim = function () {
return this.replace(/^\s+/, '').replace(/\s+$/, '')
}
}
if (!String.prototype.trim) {
// eslint-disable-next-line no-extend-native
String.prototype.trim = function () {
const str = this.replace(/^\s+/, '')
let end = str.length - 1
const ws = /\s/
while (ws.test(str.charAt(end)))
end--
return str.slice(0, end + 1)
}
}
RegExp Best Practice
- 不使用 new RegExp(),使用正则表达式字面量
- 将正则表达式赋值给变量, 防止正则表达式重复创建
- 以简单(唯一性)字元开始, 如
^/$ x \u363A [a-z] \b
, 避免以分组表达式开始: e.g\s\s*
优于\s{1,}
. - 减少表达式的重叠匹配.
- 减少分支表达式,并将最常用的分支放在最前面.
- 无需反向引用时, 使用非捕获组:
e.g
(?:...)
优于(...)
.
RegExp Use Case
Common Pattern
/abc/
: Characters sequence./[abc]/
: Characters set./[^abc]/
: Non characters set./[0-9]/
: Characters range./x+/
: 1+ (Greedy)./x+?/
: 1+ (Lazy)./x*/
: 0+./x?/
: 0/1./x{2,4}/
: 2 ~ 4./(abc)/
: Captured group./a|b|c/
: Or patterns./\d/
: Digit character./\w/
: Alphanumeric character ("word character")./\s/
: Whitespace character././
: Character exceptnewlines
./\b/
: Word boundary./^/
: Start of input./$/
: End of input.非X捕获组
+特征字符
+非Y捕获组
:
// URLSearchParams [key, value]
const pattern = /([^&=]+)=([^&]*)/g
中英文
/^[\u4e00-\u9fa5a-zA-Z]+$/i
数字
/^[1-9]*$/i
空字符与空格字符
/[(^\s+)(\s+$)]/g
Markdown Table
/(?<=\|\w+) /g
: second place to insert |
.
Strict Mode
this
isnull
in non-method functions.- Variables must be declared (
Reference Error
). - Functions must be declared at the top level of a scope (
Syntax Error
). - Forbidden unqualified identifiers deletion (
Type Error
). - Forbidden setting and deleting immutable properties (
Type Error
). - Forbidden access
arguments.callee
/arguments.caller
(Type Error
). - Forbidden variables escaped from
eval()
(Reference Error
). - Forbidden set
eval
/arguments
to variable identifiers (Syntax Error
). - Forbidden
with
statement (Syntax Error
). - Forbidden octal numbers (
Syntax Error
). - ES6 classes are automatically in strict mode.
- ES6 modules are automatically in strict mode.
Timer
- setTimeout.
- setImmediate.
- setInterval: 完全无法保证两个 timer 执行时间间隔 (可能连续执行, 也可能间隔过长).
- requestAnimationFrame.
- requestIdleCallback.
Combine setInterval
/setTimeout
function with closure function,
implement time slicing scheduler.
function processArray(items, process, done) {
const todo = items.slice()
setTimeout(function task() {
process(todo.shift())
if (todo.length > 0)
setTimeout(task, 25)
else
done(items)
}, 25)
}
:::tips this
Binding in Timer Function
所有超时执行的代码 (函数) 都会在全局作用域中的一个匿名函数中运行,
因此函数中的 this
值在非严格模式下始终指向 window
, 在严格模式下是 undefined
.
若给 setTimeout()
提供了一个箭头函数, 则 this
会保留为定义它时所在的词汇作用域.
:::
requestAnimationFrame
game loop:
function runAnimation(frameFunc) {
let lastTime = null
function frame(time) {
if (lastTime !== null) {
const timeStep = Math.min(time - lastTime, 100) / 1000
if (frameFunc(timeStep) === false)
return
}
lastTime = time
requestAnimationFrame(frame)
}
requestAnimationFrame(frame)
}
Math
Math.max
.Math.min()
.Math.ceil()
: 向上舍入为最接近的整数.Math.floor()
: 向下舍入为最接近的整数.Math.round()
: 四舍五入.Math.fround()
: 返回数值最接近的单精度 (32 位) 浮点值表示.Math.abs(x)
: 返回 x 的绝对值.Math.exp(x)
: 返回Math.E
的 x 次幂.Math.expm1(x)
: 等于Math.exp(x) - 1
.Math.log(x)
: 返回 x 的自然对数.Math.log1p(x)
: 等于1 + Math.log(x)
.Math.pow(x, power)
: 返回 x 的 power 次幂.Math.hypot(...nums)
: 返回 nums 中每个数平方和的平方根.Math.clz32(x)
: 返回 32 位整数 x 的前置零的数量.Math.sign(x)
: 返回表示 x 符号的1
/0
/-0
/-1
.Math.trunc(x)
: 返回 x 的整数部分, 删除所有小数.Math.sqrt(x)
: 返回 x 的平方根.Math.cbrt(x)
: 返回 x 的立方根.Math.acos(x)
: 返回 x 的反余弦.Math.acosh(x)
: 返回 x 的反双曲余弦.Math.asin(x)
: 返回 x 的反正弦.Math.asinh(x)
: 返回 x 的反双曲正弦.Math.atan(x)
: 返回 x 的反正切.Math.atanh(x)
: 返回 x 的反双曲正切.Math.atan2(y, x)
: 返回y/x
的反正切.Math.cos(x)
: 返回 x 的余弦.Math.sin(x)
: 返回 x 的正弦.Math.tan(x)
: 返回 x 的正切.
const epsilon = Math.E
const log10 = Math.LN10
const log2 = Math.LN2
const log2e = Math.LOG2E
const log10e = Math.LOG10E
const pi = Math.PI
const squareRoot = Math.SQRT1_2
const squareRoot2 = Math.SQRT2
Math.abs(num)
Math.exp(num)
Math.log(num)
Math.sqrt(num)
Math.acos(x)
Math.asin(x)
Math.atan(x)
Math.atan2(y, x)
Math.cos(x)
Math.sin(x)
Math.tan(x)
console.log(Math.max(3, 54, 32, 16)) // 54
console.log(Math.min(3, 54, 32, 16)) // 3
console.log(Math.ceil(25.9)) // 26
console.log(Math.ceil(25.5)) // 26
console.log(Math.ceil(25.1)) // 26
console.log(Math.round(25.9)) // 26
console.log(Math.round(25.5)) // 26
console.log(Math.round(25.1)) // 25
console.log(Math.fround(0.4)) // 0.4000000059604645
console.log(Math.fround(0.5)) // 0.5
console.log(Math.fround(25.9)) // 25.899999618530273
console.log(Math.floor(25.9)) // 25
console.log(Math.floor(25.5)) // 25
console.log(Math.floor(25.1)) // 25
function random(a = 1, b = 0) {
const lower = Math.min(a, b)
const upper = Math.max(a, b)
return lower + Math.random() * (upper - lower)
}
function randomInt(a = 1, b = 0) {
const lower = Math.ceil(Math.min(a, b))
const upper = Math.floor(Math.max(a, b))
return Math.floor(lower + Math.random() * (upper - lower + 1))
}
-2.9 | -2.5 | -2.1 | 2.1 | 2.5 | 2.9 | |
---|---|---|---|---|---|---|
Math.floor | -3 | -3 | -3 | 2 | 2 | 2 |
Math.ceil | -2 | -2 | -2 | 3 | 3 | 3 |
Math.round | -3 | -2 | -2 | 2 | 3 | 3 |
Math.trunc | -2 | -2 | -2 | 2 | 2 | 2 |
Atomics
const sharedArrayBuffer = new SharedArrayBuffer(4)
const dataView = new Uint32Array(sharedArrayBuffer)
const index = 0
const increment = 5
Atomics.add(dataView, index, increment)
Atomics.sub(dataView, index, increment)
Atomics.or(dataView, index, 0b1111)
Atomics.and(dataView, index, 0b1100)
Atomics.xor(dataView, index, 0b1111)
Atomics.load(dataView, index)
Atomics.store(dataView, index, 3)
Atomics.exchange(dataView, index, 4)
Atomics.compareExchange(dataView, index, expect, 6)
Signals
Signals
create a universal standard for state management across frameworks.
It use the so-called pull-push
model:
push
phase: When Signal becomesdirty
(its value has changed), it recursively passesdirty
state to all dependent Signals.pull
phase: All potential recalculations are deferred until value of Signal is explicitly requested.
Lazy evaluation brings:
- Automated tracking: Eliminates complexity of manual updates and enhances reactive programming capabilities.
- Performance optimization: Calculated only when necessary avoiding unnecessary computations and updates.
- Consistency: State is updated synchronously in corresponding UI render or any dependency evaluation.
- Easy integration: Signals can be easily integrated into various JavaScript libraries and frameworks.
const counter = new Signal.State(0)
const isEven = new Signal.Computed(() => (counter.get() & 1) === 0)
const parity = new Signal.Computed(() => isEven.get() ? 'even' : 'odd')
effect(() => element.textContent = parity.get())
/**
* Class auto-accessor decorator
* @see https://github.com/tc39/proposal-decorators?tab=readme-ov-file#class-auto-accessors
*/
function signal(target) {
const { get } = target
return {
init(value) {
return new Signal.State(value)
},
get() {
return get.call(this).get()
},
set(value) {
get.call(this).set(value)
},
}
}
class Counter {
@signal
count = 0 // This will be automatically turned into a Signal state.
increment() {
this.count++ // Behind the scenes, this updates the Signal's value.
}
decrement() {
this.count-- // Similarly, this updates the Signal's value.
}
@signal
get isEven() {
return this.count % 2 === 0
}
@signal
get parity() {
return this.isEven ? 'even' : 'odd'
}
}
// Usage
const myCounter = new Counter()
console.log(myCounter.count) // Outputs: 0
myCounter.increment()
console.log(myCounter.count) // Outputs: 1
console.log(myCounter.parity) // Outputs: "odd"
myCounter.increment()
console.log(myCounter.count) // Outputs: 2
console.log(myCounter.isEven) // Outputs: true
URI and URL
encodeURI()
: 不会编码属于 URL 组件的特殊字符, 比如冒号/斜杠/问号.encodeURIComponent()
: 编码它发现的所有非标准字符.
const uri = 'http://www.foo.com/illegal value.js#start'
// "http://www.foo.com/illegal%20value.js#start"
console.log(encodeURI(uri))
// "http%3A%2F%2Fwww.foo.com%2Fillegal%20value.js%23start"
console.log(encodeURIComponent(uri))
const uri = 'http%3A%2F%2Fwww.foo.com%2Fillegal%20value.js%23start'
// http%3A%2F%2Fwww.foo.com%2Fillegal value.js%23start
console.log(decodeURI(uri))
// http:// www.foo.com/illegal value.js#start
console.log(decodeURIComponent(uri))
URL:
hash
.host
.hostname
.href
.pathname
.port
.protocol
.search
: USVString.searchParams
: URL search map.username
.password
.origin
: read only.
const href = new URL('other.mjs', 'https://example.com/code/main.mjs').href
// 'https://example.com/code/other.mjs'
const href = new URL('../other.mjs', 'https://example.com/code/main.mjs').href
// 'https://example.com/other.mjs'
const blob = new Blob(['export const itsAModule = true'], {
type: 'text/javascript',
})
const blobUrl = URL.createObjectURL(blob)
function saveTextAsFile(textToWrite, fileNameToSaveAs, fileType) {
const textFileAsBlob = new Blob([textToWrite], { type: fileType })
const downloadLink = document.createElement('a')
downloadLink.innerHTML = 'Download File'
downloadLink.download = fileNameToSaveAs
if (window.webkitURL != null) {
downloadLink.href = window.webkitURL.createObjectURL(textFileAsBlob)
} else {
downloadLink.href = window.URL.createObjectURL(textFileAsBlob)
downloadLink.onclick = destroyClickedElement // document.body.removeChild(downloadLink);
downloadLink.style.display = 'none'
document.body.appendChild(downloadLink)
}
downloadLink.click()
}
// window.location.search
const qs = '?q=javascript&num=10'
const searchParams = new URLSearchParams(qs)
for (const param of searchParams)
console.log(param)
// ["q", "javascript"]
// ["num", "10"]
alert(searchParams.toString()) // " q=javascript&num=10"
searchParams.has('num') // true
searchParams.get('num') // 10
searchParams.set('page', '3')
alert(searchParams.toString()) // " q=javascript&num=10&page=3"
searchParams.delete('q')
alert(searchParams.toString()) // " num=10&page=3"
Encoding and Decoding
const textEncoder = new TextEncoder()
const decodedText = 'foo'
const encodedText = textEncoder.encode(decodedText)
// f 的 UTF-8 编码是 0x66 (即十进制 102)
// o 的 UTF-8 编码是 0x6F (即二进制 111)
console.log(encodedText) // Uint8Array(3) [102, 111, 111]
const textEncoder = new TextEncoder()
const fooArr = new Uint8Array(3)
const fooResult = textEncoder.encodeInto('foo', fooArr)
console.log(fooArr) // Uint8Array(3) [102, 111, 111]
console.log(fooResult) // { read: 3, written: 3 }
async function* chars() {
const decodedText = 'foo'
for (const char of decodedText)
yield await new Promise(resolve => setTimeout(resolve, 1000, char))
}
const decodedTextStream = new ReadableStream({
async start(controller) {
for await (const chunk of chars())
controller.enqueue(chunk)
controller.close()
},
})
const encodedTextStream = decodedTextStream.pipeThrough(new TextEncoderStream())
const readableStreamDefaultReader = encodedTextStream.getReader()
while (true) {
const { done, value } = await readableStreamDefaultReader.read()
if (done)
break
else
console.log(value)
}
// Uint8Array[102]
// Uint8Array[111]
// Uint8Array[111]
const textDecoder = new TextDecoder()
// f 的 UTF-8 编码是 0x66 (即十进制 102)
// o 的 UTF-8 编码是 0x6F (即二进制 111)
const encodedText = Uint8Array.of(102, 111, 111)
const decodedText = textDecoder.decode(encodedText)
console.log(decodedText) // foo
const response = await fetch(url)
const stream = response.body.pipeThrough(new TextDecoderStream())
const decodedStream = stream.getReader()
for await (const decodedChunk of decodedStream)
console.log(decodedChunk)
Web Stream
- Web streams complete guide.
Internationalization
Number i18n
const nfFrench = new Intl.NumberFormat('fr')
nf.format(12345678901234567890n)
// => 12 345 678 901 234 567 890
String i18n
const lfEnglish = new Intl.ListFormat('en')
// const lfEnglish = new Intl.ListFormat('en', { type: 'disjunction' }); => 'or'
lfEnglish.format(['Ada', 'Grace', 'Ida'])
// => 'Ada, Grace and Ida'
const formatter = new Intl.ListFormat('en', {
style: 'long',
type: 'conjunction',
})
console.log(formatter.format(vehicles))
// expected output: "Motorcycle, Bus, and Car"
const formatter2 = new Intl.ListFormat('de', {
style: 'short',
type: 'disjunction',
})
console.log(formatter2.format(vehicles))
// expected output: "Motorcycle, Bus oder Car"
const formatter3 = new Intl.ListFormat('en', { style: 'narrow', type: 'unit' })
console.log(formatter3.format(vehicles))
// expected output: "Motorcycle Bus Car"
Time i18n
const rtfEnglish = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
rtf.format(-1, 'day') // 'yesterday'
rtf.format(0, 'day') // 'today'
rtf.format(1, 'day') // 'tomorrow'
rtf.format(-1, 'week') // 'last week'
rtf.format(0, 'week') // 'this week'
rtf.format(1, 'week') // 'next week'
const dtfEnglish = new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
dtfEnglish.format(new Date()) // => 'May 7, 2019'
dtfEnglish.formatRange(start, end) // => 'May 7 - 9, 2019'
Functional JavaScript
- Predictable: pure and immutable.
- Safe: pure and immutable.
- Transparent: pure and immutable.
- Modular: composite with currying and monads.
Functional programming is the art of composing higher-order functions to advance the state of a program in a pure manner:
- Pure Functions and Immutability: Pure functions ensure that functions have no side effects and return the same output for the same inputs, which is complemented by immutability that prevents data from being changed unexpectedly. Together, they ensure a predictable and stable code base.
- Currying and Memoization: Currying allows functions to be broken down into simpler, single-argument functions that are easier to manage and memoize. Memoization can then be applied to these curried functions to cache their results, optimizing the application's performance by avoiding repeated calculations.
- Monads and Pure Functions: Monads help manage side effects in a controlled manner, which allows pure functions to remain pure even when dealing with operations like I/O or state transitions. This encapsulation of side effects preserves the integrity of the functional architecture.
Functional JavaScript Pros
- Type safe and state safe.
- Explicit flow of data.
- Concurrency safety.
Functional JavaScript Cons
- Verbose.
- More object creation.
- More garbage collection.
- More memory usage.
With help of immutable.js
/immer.js
,
object creation/garbage collection/memory usage can be alleviated.
For example, in vanilla.js, map2 === map1
become false
,
but in immutable.js map2 === map1
become true
(copy free due to immutable data).
const map1 = { b: 2 }
const map2 = map1.set('b', 2)
Partial Application
function partialFromBind(fn, ...args) {
return fn.bind(null, ...args)
}
function partial(fn, ...args) {
return (...rest) => {
return fn(...args, ...rest)
}
}
Currying
Chain of multiple single argument functions:
function curry(fn, ...stored_args) {
return function (...new_args) {
const args = stored_args.concat(new_args)
return fn(...args)
}
}
const add = x => y => x + y
const addOne = curry(add, 1)
// addOne(3) === 4;
const addFive = curry(addOne, 1, 3)
// addFive(4) === 9;
Compose
function compose(...fns) {
return x =>
fns.reduceRight((promise, fn) => promise.then(fn), Promise.resolve(x))
}
const addTwo = x => x + 2
const addThree = x => x + 3
const addFive = x => x + 5
const addTen = compose(addTwo, addThree, addFive)
addTen(8).then(console.log) // 18
Flow
function flow(...fns) {
return x =>
fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}
const addTwo = x => x + 2
const addThree = x => x + 3
const addFive = x => x + 5
const addTen = flow(addTwo, addThree, addFive)
addTen(8).then(console.log) // 18
Pipe
function pipe(x, ...fns) {
return fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}
const addTwo = x => x + 2
const addThree = x => x + 3
const addFive = x => x + 5
const addTen = pipe(8, addTwo, addThree, addFive)
addTen.then(console.log) // 18
Immutable
Immutable data structure:
- Reused reference: one address represent one data.
- Quick comparison: different address represent different data.
- Fast recovery and snapshot: reuse previous data.
Immutable Array
const RE_INDEX_PROP_KEY = /^\d+$/
const ALLOWED_PROPERTIES = new Set(['length', 'constructor', 'slice', 'concat'])
function createImmutableArray(arrayLike, mapFn) {
const arr = Array.from(arrayLike, mapFn)
const handler = {
get(target, propKey, receiver) {
if (RE_INDEX_PROP_KEY.test(propKey) || ALLOWED_PROPERTIES.has(propKey))
return Reflect.get(target, propKey, receiver)
throw new TypeError(`Property "${propKey}" can’t be accessed`)
},
set(target, propKey, value, receiver) {
throw new TypeError('Setting is not allowed')
},
deleteProperty(target, propKey) {
throw new TypeError('Deleting is not allowed')
},
}
return new Proxy(arr, handler)
}
const array = createImmutableArray(['a', 'b', 'c'])
// Non-destructive operations are allowed:
assert.deepEqual(array.slice(1), ['b', 'c'])
assert.equal(array[1], 'b')
// Destructive operations are not allowed:
assert.throws(() => (array[1] = 'x'), /^TypeError: Setting is not allowed$/)
assert.throws(
() => array.shift(),
/^TypeError: Property "shift" can’t be accessed$/
)
Immutable Map
class ImmutableMap {
#map
constructor(iterable) {
this.#map = new Map(iterable)
}
static _setUpPrototype() {
// Only forward non-destructive methods to the map:
for (const methodName of ['get', 'has', 'keys', 'size']) {
ImmutableMap.prototype[methodName] = function (...args) {
return this.#map[methodName](...args)
}
}
}
}
ImmutableMap._setUpPrototype()
const map = new ImmutableMap([
[false, 'no'],
[true, 'yes'],
])
// Non-destructive operations work as usual:
assert.equal(map.get(true), 'yes')
assert.equal(map.has(false), true)
assert.deepEqual([...map.keys()], [false, true])
// Destructive operations are not available:
assert.throws(
() => map.set(false, 'never!'),
/^TypeError: map.set is not a function$/
)
assert.throws(() => map.clear(), /^TypeError: map.clear is not a function$/)
Immutable Class
Copying class instances without side effects:
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
clone() {
return new Point(this.x, this.y)
}
static from(other) {
return new Point(other.x, other.y)
}
}
class Color {
constructor(name) {
this.name = name
}
clone() {
return new Color(this.name)
}
static from(other) {
return new Color(other.name)
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y)
this.color = color
}
clone() {
return new ColorPoint(this.x, this.y, this.color.clone())
}
static from(other) {
return new ColorPoint(other.x, other.y, Color.from(other.color))
}
}
Lodash
- chunk.
- shuffle.
- take.
- difference.
- intersection.
- isEmpty.
- orderBy.
- merge.
- cloneDeep.
- debounce.
- throttle.
- startCase.
- kebabCase.
- snakeCase.
- camelCase.
RxJS
- Async iteration: pull streams and single consumers.
- Reactive programming: push streams and potentially multiple consumers.
Stream
class PushArray extends Array {
static EVENT_NAME = 'new_value'
#eventEmitter = new EventEmitter()
push(value) {
this.#eventEmitter.emit(PushArray.EVENT_NAME, value)
return super.push(value)
}
subscribe({ next }) {
this.#eventEmitter.on(PushArray.EVENT_NAME, (value) => {
next(value)
})
}
unsubscribe() {
this.#eventEmitter.removeAllListeners(PushArray.EVENT_NAME)
}
}
// Source
const pushArray = new PushArray(1, 2, 3)
// Consumer
pushArray.subscribe({
next(value) {
console.log('New value:', value)
},
})
// Producer
pushArray.push(4)
pushArray.push(5)
pushArray.unsubscribe()
pushArray.push(6)
Observable
interface Observer<T> {
next: (value: T) => void
error?: (error: Error) => void
complete?: () => void
}
interface Subscription {
unsubscribe: () => void
}
interface Observable<T> {
// eslint-disable-next-line ts/no-misused-new
new (subscriber: (observer: Observer<T>) => Subscription): Observable<T>
observable: () => this
readonly species: this
of: (...items: Array<mixed>) => Observable<T>
from: (x: Observable<T> | Iterable<T>) => Observable<T>
map: <Z>(fn: (value: T) => Z) => Observable<Z>
reduce: <Z>(
acc: (accumulator: Z, value: T, index?: number, array?: Array<T>) => Z,
startsWith?: T
) => Observable<T>
filter: (predicate: (value: T) => boolean) => Observable<T>
skip: (count: number) => Observable<T>
subscribe: (observer: Function | Observer<T>) => Subscription
}
const map = curry(
(fn, stream) =>
new Observable((observer) => {
const subs = stream.subscribe({
next(value) {
try {
observer.next(fn(value))
} catch (err) {
observer.error(err)
}
},
error(e) {
observer.error(e)
},
complete() {
observer.complete()
},
})
return () => subs.unsubscribe()
})
)
const reduce = curry((accumulator, initialValue, stream) => {
let result = initialValue ?? {}
return new Observable((observer) => {
const subs = stream.subscribe({
next(value) {
result = accumulator(result, value)
},
error(e) {
observer.error(e)
},
complete() {
observer.next(result)
observer.complete()
},
})
return () => subs.unsubscribe()
})
})
const filter = curry(
(predicate, stream) =>
new Observable((observer) => {
const subs = stream.subscribe({
next(value) {
if (predicate(value))
observer.next(value)
},
error(e) {
observer.error(e)
},
complete() {
observer.complete()
},
})
return () => subs.unsubscribe()
})
)
const skip = curry((count, stream) => {
let skipped = 0
return new Observable((observer) => {
const subs = stream.subscribe({
next(value) {
if (skipped++ >= count)
observer.next(value)
},
error(e) {
observer.error(e)
},
complete() {
observer.complete()
},
})
return () => subs.unsubscribe()
})
})
class Observable {
map(fn) {
return map(fn, this)
}
reduce(accumulator, initialValue = {}) {
return reduce(accumulator, initialValue, this)
}
filter(predicate) {
return filter(predicate, this)
}
skip(count) {
return skip(count, this)
}
}
Observable.of(1, 2, 3, 4)
.skip(1)
.filter(isEven)
.map(square)
.reduce(add, 0)
.subscribe({
next: console.log,
})
JavaScript Style Guide
ESLint
- Promise
- Import
- Unicorn
- SonarJS: Bug and Code Smell Detection
- Functional
- JSX A11Y
- Browser Compatibility
- JSDoc
- TSDoc
- Node
- Node Security
- TypeScript Import Resolver
{
"env": {
"browser": true,
"es2021": true,
"node": true,
"jest": true
},
"extends": [
"eslint:recommended",
"plugin:import/recommended",
"plugin:jsx-a11y/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:unicorn/recommended",
"plugin:promise/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"import",
"jsx-a11y",
"react",
"react-hooks",
"@typescript-eslint"
],
"settings": {
"react": {
"version": "detect"
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true,
"project": "./"
}
}
},
"rules": {
"react/prop-types": 0,
"react/jsx-props-no-spreading": 0
}
}
Naming Style
- 变量: 名词前缀.
- 方法 / 函数: 动词前缀.
_method
: 表示私有化方法.- 普通函数: 驼峰命名法 (camelCase).
- 构造函数: 帕斯卡命名法 (PascalCase).
- 缩略词和缩写都必须是全部大写 / 小写.
- 对于
jQuery
对象的变量使用$
作为前缀.
Variable Style
- No single
let/const
for multiple variables. - Sort
let/const
. - No chains assignment (create implicit global variable).
- Prefer
()
wrap multiple line.
Object Style
- Prefer literal not
Object()
constructor. - Prefer object-shorthand.
- Prefer
Object.prototype.XX
notobject.xx
. - Prefer object spread (
...
) notobject.assign
:
// very bad
const original = { a: 1, b: 2 }
const copy = Object.assign(original, { c: 3 }) // 变异的 `original` ಠ_ಠ
delete copy.a // 这....
// bad
const original = { a: 1, b: 2 }
const copy = Object.assign({}, original, { c: 3 })
// good
const original = { a: 1, b: 2 }
const copy = { ...original, c: 3 } // copy => { a: 1, b: 2, c: 3 }
const { a, ...noA } = copy // noA => { b: 2, c: 3 }
- Prefer
.
for static name, prefer[]
for variable name:
// good
const isJedi = luke.jedi
function getProp(prop) {
return luke[prop]
}
Array Style
- Prefer literal.
- Prefer
push
not[]
. - Prefer array spread (
...
) (best) orArray.from
(good):
const foo = document.querySelectorAll('.foo')
// good
const nodes = Array.from(foo)
// best
const nodes = [...foo]
Destruct Style
对于多个返回值使用对象解构, 而不是数组解构:
// bad
function processInputBad(input) {
// 处理代码...
return [left, right, top, bottom]
}
// 调用者需要考虑返回数据的顺序.
const [left, __, top] = processInputBad(input)
// good
function processInput(input) {
// 处理代码 ...
process()
return { left, right, top, bottom }
}
// 调用者只选择他们需要的数据.
const { left, top } = processInput(input)
String Style
- Prefer
'
not"
. - Prefer template literals not
'str1' + 'str2'
.
Function Style
- No reassign parameters (implicit side effect and bad performance).
- Prefer
...args
notarguments
. - Prefer ES6 default parameters not default expression pattern.
Arrow Function Style
- Prefer
()
wrap multiple line return value.
Module Style
- No duplicated export path:
// bad
// import foo from 'foo';
// import { named1, named2 } from 'foo';
// good
import foo, { named1, named2 } from 'foo'
- No export
let
:
// bad
// let foo = 3;
// export { foo };
// good
const foo = 3
export { foo }
Iterator and Generator Style
- 使用
Object.keys() / Object.values() / Object.entries()
迭代对象生成数组. - 使用
map/reduce/filter/any/every/some/find/findIndex/ ...
遍历数组. - Prefer functional style iterator:
const numbers = [1, 2, 3, 4, 5]
// bad
let sum = 0
for (const num of numbers)
sum += num
console.log(sum === 15)
// good
let sum = 0
numbers.forEach((num) => {
sum += num
})
console.log(sum === 15)
// best (use the functional force)
const sum = numbers.reduce((total, num) => total + num, 0)
console.log(sum === 15)
// bad
const increasedByOne = []
for (let i = 0; i < numbers.length; i++)
increasedByOne.push(numbers[i] + 1)
// good
const increasedByOne = []
numbers.forEach((num) => {
increasedByOne.push(num + 1)
})
// best (keeping it functional)
const increasedByOne = numbers.map(num => num + 1)
Expression Style
if
语句使用 ToBoolean 的抽象方法来计算表达式的结果:
Object
:true
.undefined
:false
.null
:false
.boolean
: 布尔值的取值.number
: 如果为+0
/-0
/NaN
值为false
, 否则为true
.string
: 如果是一个空字符串''
值为false
, 否则为true
.
对于布尔值使用简写, 但是对于字符串和数字进行显式比较:
// bad
if (isValid === true) {
// ...
}
// good
if (isValid) {
// ...
}
// bad
if (someName) {
// ...
}
// good
if (someName !== '') {
// ...
}
// bad
if (collection.length) {
// ...
}
// good
if (collection.length > 0) {
// ...
}
- Prefer
{}
warpcase
when existsconst
/let
/function
/class
declaration:
// good
switch (foo) {
case 1: {
const x = 1
break
}
case 2: {
const y = 2
break
}
case 3: {
function f() {
// ...
}
break
}
case 4:
bar()
break
default: {
class C {}
}
}
Space Style
- 键入最后一个运算符后再换行, 运算符置于行尾可使
Automatic Semicolon Insertion
机制失效. - 换行后保持 2 个缩进层次.
- Good places to use a white space include:
,
/;
后.+
,-
,*
,/
,<
,>
,=
前后.function () {}
.function foo() {}
.} if/for/while () {}
.} else {}
.- inner
{}
. - No space inner
()
[]
.
let d = 0
let a = b + 1
if (a && b && c) {
d = a % c
a += d
}
Comments Style
JSDoc
best practice:
- 插入空行与统一缩进.
- Write a concise summary: First paragraph comment should be concise description helping quick understanding.
- Provide good type information.
- Use tags:
Tags like
@param
,@returns
, and@typeParam
provide more information. - Add examples:
Examples
@example
help users quickly understand how to use your library. - Document everything: Document every symbol exposing to users.
- Link internally:
Use
@link
,@linkcode
, and@linkplain
to link to other parts of documentation. - Test documentation:
Use
deno test --doc
to type check, anddeno doc --lint
to check for rest issues.
/**
* comments
* comments
*/
/**
* @module app
* @namespace APP
*/
/**
* @class mathStuff
*/
/**
* @property propertyName description.
* @type {import('@jest/types').Config}
*/
/**
* @constructor
* @method sum
* @param {number} id
* @param {string} instructions
* @returns {number} result
*/
JavaScript Engine
Variables Lifecycle
- Creation phase (Hoisting)
- Declaration phase: 在作用域中注册变量
- Initialization phase: 分配内存, 在作用域中绑定变量 (
undefined
)
- Execution phase/Assignment phase
Execution Context
Global Execution Context
- Create global object (
window
). - Create
this
object (refer towindow
). - Declare and initialize variable/function (
undefined
), setup memory space for them.
Function Execution Context
- Create arguments object.
- Create
this
object. - Declare and initialize variable(
undefined
)/function, store them into memory.
如果 JavaScript 引擎在函数执行上下文中找不到变量, 它会在最近的父级执行上下文中查找该变量. 这个查找链将会一直持续, 直到引擎查找到全局执行上下文. 这种情况下, 如果全局执行上下文也没有该变量, 那么将会抛出引用错误 (Reference Error). 子函数包含它父级函数的变量环境, 把这个概念称为闭 包(Closure), 即使父级函数执行环境已经从执行栈弹出了, 子函数还是可以访问父级函数变量 x (通过作用域链).
Event Loop
Browser Event Loop
The job of the event loop is to look into the call stack and determine if the call stack is empty or not. If the call stack is empty, it looks into the ES6 job queue and message queue to see if there’s any pending call back waiting to be executed:
- ES6 job queue: used by
Promises
(higher priority). - Message queue: used by
setTimeout
,DOM events
. - 微任务 MicroTask (Jobs), 有特权, 可以插队:
process.nextTick
.Promises.then
(Promise 构造函数是同步函数).Object.observer
,MutationObserver
.catch finally
.
- 宏任务 MacroTask (Tasks), 没有特权:
scripts
: 整体脚本视作一个宏任务.MessageChannel
,postMessage
.setImmediate
,I/O
.setTimeout
,setInterval
.XHR
callback function.requestAnimationFrame
.- UI interaction
events
callback function. - UI rendering.
- MicroTask 优先于 MacroTask.
- 浏览器为了能够使得 JS 内部 (macro)task 与 DOM 任务能够有序的执行, 会在一个 (macro)task 执行结束后, 在下一个 (macro)task 执行开始前, 对页面进行重新渲染. 当 JS 引擎从任务队列中取出一个宏任务来执行, 如果执行过程中有遇到微任务, 那么执行完该宏任务就会去执行宏任务内的所有微任务, 然后更新 UI. 后面就是再从任务队列中取出下一个宏任务来继续执行, 以此类推.
Event Loop
与Call Stack
有且仅有一个,Task/Job/Message Queue
可以有多个.
宏任务队列取宏任务 -> 执行 1 个宏任务 -> 检查微任务队列并执行所有微任务 -> requestAnimationFrame -> 浏览器重排/重绘 -> requestIdleCallback -> 宏任务队列取宏任务
Event Loop
simple model:
for (const macroTask of macroTaskQueue) {
// 1. Handle current MacroTask.
runTask(macroTask)
// 2. Handle all MicroTasks.
for (const microTask of microTaskQueue)
runTask(microTask)
// 3. Handle Animation Frame.
if (shouldRepaint()) {
if (!animationFrameCallbackQueue.isEmpty()) {
const animationTasks = animationFrameCallbackQueue.copyTasks()
for (const animationTask of animationTasks)
runTask(animationTask)
}
repaint()
}
}
Using setTimeout
with 0
seconds timer
helps to defer execution of Promise
and bar
until the stack is empty.
function bar() {
console.log('bar')
}
function baz() {
console.log('baz')
}
function foo() {
console.log('foo')
setTimeout(bar, 0)
new Promise((resolve, reject) => {
resolve('Promise resolved')
})
.then(res => console.log(res))
.catch(err => console.log(err))
baz()
}
foo()
// foo
// baz
// Promised resolved
// bar
process.nextTick()
run before Promise.then()
:
console.log('1')
setTimeout(() => {
console.log(2)
Promise.resolve().then(() => {
console.log(3)
process.nextTick(() => {
console.log(4)
})
})
})
Promise.resolve().then(() => {
console.log(5)
setTimeout(() => {
console.log(6)
})
Promise.resolve().then(() => {
console.log(7)
})
})
process.nextTick(() => {
console.log(8)
process.nextTick(() => {
console.log(9)
})
})
console.log('10')
// 1 10 8 9 5 7 2 3 4 6
Promise 构造函数本身是同步函数:
console.log('script start')
const promise1 = new Promise((resolve) => {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(() => {
console.log('promise2')
})
setTimeout(() => {
console.log('setTimeout')
})
console.log('script end')
// 输出顺序:
// script start
// promise1
// promise1 end
// script end
// promise2
// setTimeout.
await a(); b()
等价于 Promise(a()).then(b())
,
a
是同步执行, b
是 MicroTask:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(() => {
console.log('promise2')
})
console.log('script end')
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
当调用栈没有同步函数时, 清空 MicroTask 任务队列里的函数, 再从 MacroTask 任务队列里取出一个函数执行 (第二次 Event Loop):
function test() {
console.log('start')
setTimeout(() => {
console.log('children2')
Promise.resolve().then(() => {
console.log('children2-1')
})
}, 0)
setTimeout(() => {
console.log('children3')
Promise.resolve().then(() => {
console.log('children3-1')
})
}, 0)
Promise.resolve().then(() => {
console.log('children1')
})
console.log('end')
}
test()
// start
// end
// children1
// children2
// children2-1
// children3
// children3-1
Node Event Loop
Node.js can run I/O operations in a non-blocking way, meaning other code (and even other I/O operations) can be executed while an I/O operation is in progress.
Instead of having to wait for an I/O operation to complete (and essentially waste CPU cycles sitting idle), Node.js can use the time to execute other tasks.
When the I/O operation completes, event loop give back control to the piece of code that is waiting for the result of that I/O operation.
The Node.js execution model was designed to cater to the needs of most web servers, which tend to be I/O-intensive (due to non-blocking I/O).
console.log('glob1')
setTimeout(() => {
console.log('timeout1')
process.nextTick(() => {
console.log('timeout1_nextTick')
})
new Promise((resolve) => {
console.log('timeout1_promise')
resolve()
}).then(() => {
console.log('timeout1_then')
})
})
setImmediate(() => {
console.log('immediate1')
process.nextTick(() => {
console.log('immediate1_nextTick')
})
new Promise((resolve) => {
console.log('immediate1_promise')
resolve()
}).then(() => {
console.log('immediate1_then')
})
})
process.nextTick(() => {
console.log('glob1_nextTick')
})
new Promise((resolve) => {
console.log('glob1_promise')
resolve()
}).then(() => {
console.log('glob1_then')
})
setTimeout(() => {
console.log('timeout2')
process.nextTick(() => {
console.log('timeout2_nextTick')
})
new Promise((resolve) => {
console.log('timeout2_promise')
resolve()
}).then(() => {
console.log('timeout2_then')
})
})
process.nextTick(() => {
console.log('glob2_nextTick')
})
new Promise((resolve) => {
console.log('glob2_promise')
resolve()
}).then(() => {
console.log('glob2_then')
})
setImmediate(() => {
console.log('immediate2')
process.nextTick(() => {
console.log('immediate2_nextTick')
})
new Promise((resolve) => {
console.log('immediate2_promise')
resolve()
}).then(() => {
console.log('immediate2_then')
})
})
console.log('glob2')
// glob1
// glob1_promise
// glob2_promise
// glob2
// glob1_nextTick
// glob2_nextTick
// glob1_then
// glob2_then
// timeout1
// timeout1_promise
// timeout1_nextTick
// timeout1_then
// timeout2
// timeout2_promise
// timeout2_nextTick
// timeout2_then
// immediate1
// immediate1_promise
// immediate1_nextTick
// immediate1_then
// immediate2
// immediate2_promise
// immediate2_nextTick
// immediate2_then
V8
- source code (parser) AST (interpreter) bytecode
- send profiling data from bytecode to optimizing compiler, generate optimized code
- Ignition interpreter
- TurboFan optimizing compiler (2 for SpiderMonkey/Edge, 3 for Safari)
- JavaScript implementation list.
V8 Object Shape
V8 object shapes:
// o1 and o2 have the same shape
// JSObject(1, 2) => Shape('x', 'y')
// JSObject(3, 4) => Shape('x', 'y')
// 'x' => 0 Offset, Writable, Enumerable, Configurable
// 'y' => 1 Offset, Writable, Enumerable, Configurable
const o1 = { x: 1, y: 2 }
const o2 = { x: 3, y: 4 }
Shape transform:
// Shape chain: Shape(empty) => Shape(x) => Shape(x, y)
const o = {}
o.x = 1
o.y = 2
// Shape chain: Shape(empty) => Shape(y) => Shape(y, x)
const o = {}
o.y = 2
o.x = 1
// Shape chain: Shape(x)
const o = { x: 1 }
Array shape: Shape('length'), 'length' => 0 Offset, Writable
.
V8 Inline Cache
V8 use ICs to memorize information (same shape) where to find properties on objects:
- always initialize objects in the same way (generate the same shape)
- don't add property to objects dynamically (invalid inline cache)
- don't mess with property attributes of array elements
V8 为了提高对象属性的访问效率, 将属性分为多种类型:
- 排序属性 (elements): 符合数组索引类型的属性 (正整数), 存储为线性结构, 可快速访问.
- 常规属性 (properties): 字符串类型的属性 (包括负数/ 浮点数/Symbol), 存储为线性结构, 可快速访问.
- 慢属性: 属性需要反复增删时, 会将属性存储类型从线性结构 (快属性) 改为字典结构 (慢属性).
V8 Object Structure
Basic object structure:
- Every object in JavaScript has its main internal class and a hidden class that describes its shape.
- Hidden classes
inherit from each other and are organized into class trees.
The shape of an object
{ a: 1 }
will be the parent for the shape of an object{ a: 1, b: 2 }
. - The order of properties matters.
Objects
{ a: 1, b: 2 }
and{ b: 2, a: 1 }
will have two different shapes. - A subclass holds a reference to the superclass and information about what has changed (transition).
- In the class tree of each object, the number of levels is not less than the number of properties in the object.
- The fastest properties of an object will be those declared at initialization, in-object property faster than external property.
- Atypical changes in the object's structure, such as property removal (
delete
), can lead to a change in the storage type of properties to a slower one.
const obj1 = { a: undefined }
obj1.a = 1 // <- "a" - in-object property.
const obj2 = {}
obj2.a = 1 // <- "a" - external property.
// Change storage type to NameDictionary.
const obj1 = { a: 1, b: 2 }
delete obj1.a
// Storage type is not changed.
const obj2 = { a: 1, b: 2 }
obj2.a = undefined
An array is a regular class whose structure looks like { length: [W__] }
.
The elements of the array are stored in special structures,
and references to these structures are placed inside the object:
- Adding or removing elements from the array does not lead to an increase in the class tree.
- The use of atypical keys in an array,
such as non-numeric keys or keys outside the range
[0 .. 2**32 - 2]
, leads to the creation of new shapes in the class tree. - Attempting to modify an array element's attribute will result in a switch to a slower storage type.
// 1. New element of the array doesn't extends the shapes tree.
const arr = []
arr[0] = 1
// 2. Leads to the shapes tree generation:
// { length } => { length, [-1] } => { length, [-1], [2**32 - 1] }.
const arr = []
arr[-1] = 1
arr[2 ** 32 - 1] = 2
// 3. Modify element's attribute result in switch to slower storage type.
const arr = [1, 2, 3]
// { elements: {
// #0: 1,
// #1: 2,
// #2: 3
// }}
Object.defineProperty(arr, '0', { writable: false })
// { elements: {
// #0: { value: 1, attrs: [_EC] },
// #1: { value: 2, attrs: [WEC] },
// #2: { value: 3, attrs: [WEC] }
// }}
V8 Garbage Collection
V8 garbage collection 分代垃圾回收算法, 将堆分为两个空间:
- 新生代: 存放短周期对象, 空间小, 使用
Scavenge
回收算法, 副垃圾回收器. - 老生代: 存放长周期对象, 空间大, 使用
Mark-Sweep-Compact
回收算法, 主垃圾回收器.
V8 Scavenge GC Algorithm
- 空间换时间算法 (复制算法).
- 标记活动对象和非活动对象.
- 复制 from-space 中的活动对象到 to-space 中并进行排序.
- 清除 from-space 中的所有对象.
- 将 from-space 和 to-space 进行角色互换, 等待下一次 GC.
- 新生代对象晋升 (计算存活周期, 初始为 nursery 子代): allocation -> nursery 子代 -> intermediate 子代 -> 老生代.
V8 Mark-Sweep-Compact GC Algorithm
- 老生代空间大, 无法使用空间换时间 (复制) 算法.
- 标记阶段: 对老生代对象进行第一次扫描, 对活动对象进行标记.
- 清理阶段: 对老生代对象进行第二次扫描, 清除未标记的对象.
- 压缩阶段: 每次清理完非活动对象, 把剩下活动对象整理到内存的一侧, 回收掉边界上的内存 (以备后续大对象老生代).
V8 GC Stop-The-World
垃圾回收优先于代码执行, 会先停止代码的执行, 等到垃圾回收完毕, 再执行 JS 代码, 成为全停顿.
Orinoco 优化 (优化全停顿现象):
- Incremental marking (time slice):
JS + Mark + JS + Mark ...
. - Lazy sweeping.
- Concurrent GC.
- Parallel GC.
V8 Performance Tools
Browser Engine
- Chrome: Blink (fork of
WebCore
component fromWebKit
) + V8. - Edge: Trident/EdgeHTML + Chakra -> Chromium.
- Firefox: Gecko + SpiderMonkey.
- Safari: Webkit (fork of
KHTML
andKJS
fromKDE
) + JavaScriptCore (Nitro).
Browser Process Architecture
- 浏览器进程:
- Singleton.
- 浏览器界面显示, 提供用户交互, e.g 前进/后退.
- 子进程管理.
- 网络资源管理.
- 存储管理.
- 网络进程:
- Singleton.
- 主要负责页面的网络资源加载.
Viz
process:- Singleton.
- 实现 3D CSS, 绘制 UI 界面.
- GPU main thread: rasters display lists and video frames into GPU texture tiles, draws compositor frames to screen using GPU, Out-of-Process display compositor, raster and iframes (OOP-D, OOP-R, OOPIFs).
- Display compositor thread: aggregates and optimizes compositing from render processes and browser process.
- 渲染进程:
- 核心任务是将 HTML/CSS 和 JavaScript 转换为用户可以与之交互的网页 (页面渲染, 脚本执行, 事件处理).
- 排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中.
- 默认情况下, Chrome 会为每个 Tab 标签创建一个渲染进程.
- Within a single browser tab,
frames (
<iframe>
) from different sites are always in different render processes (OOPIFs, Out-of-Process iframes), but frames (<iframe>
) from same site are always in same render process. - 出于安全考虑, 渲染进程都是运行在沙箱模式下.
- 插件进程: 主要是负责插件的运行, 因插件易崩溃, 所以需要通过隔离以保证插件进程崩溃不会对浏览器和页面造成影响.
Chromium Rendering Engine
RenderingNG Architecture
RenderingNG
key goals:
RenderingNG
key changes:
- Uniform point of entry: always enter the pipeline at the beginning (no longer possible to enter the pipeline starting at an intermediate phase).
- Functional stages: each stage should have well-defined inputs and outputs, and its behavior should be functional.
- Constant inputs: inputs should be effectively constant while the stage is running.
- Immutable outputs: outputs should be immutable for the remainder of the rendering update.
- Checkpoint consistency: rendering data produced thus far should be in a self-consistent state at the end of each stage.
- De-duplication of work: only compute each thing once.
RenderingNG Pipeline
RenderingNG
pipeline
(green main
thread -> yellow compositor
thread -> orange Viz
process):
- Animate.
- Style.
- Layout.
- Pre-paint.
- Scroll.
- Paint.
- Commit.
- Layerize.
- Raster, decode and paint worklet (GPU hardware acceleration).
- Activate.
- Aggregate (GPU hardware acceleration).
- Draw (GPU hardware acceleration).
Stages of the rendering pipeline can be skipped if they aren't needed:
Scrolling and visual effects animation can skip layout
, pre-paint
and paint
.
If layout
, pre-paint
, and paint
can be skipped for visual effects,
they can be run entirely on compositor
thread and skip main
thread.
RenderingNG Render Process
Render process:
- Download HTML.
- Parser/Script.
- DOM and CSSOM Construction.
- Render Tree = DOM Tree + Styled Tree.
- Layout.
- Paint.
- Composite.
- GUI 渲染线程 (
main
thread):- Parse
HTML
/CSS
. - Construct
DOM
tree,CSSOM
tree andRenderObject
tree. - Layout render tree.
- Paint render tree.
- Send information to GPU (Composite render tree):
separating
main
andcompositor
threads is critically important forperformance isolation
of animation and scrolling from main thread work.
- Parse
- JS 引擎线程:
- JS 内核运行线程, 负责解析
JavaScript
脚本, 运行代码. - 一个 Tab 页 (渲染进程) 中只有一个 JS 引擎线程在运行 JS 程序.
- JS 引擎一直等待着任务队列中任务的到来, 然后加以 处理.
- JS 内核运行线程, 负责解析
- 事件触发线程:
- 负责控制事件循环.
- JS 引擎线程触发 MacroTask/MicroTask, 事件线程将任务添加到任务队列, 等待 JS 引擎线程执行.
- 定时触发器线程:
setInterval
与setTimeout
执行线程.- 此线程计时并触发定时器任务, 计时完毕后, 事件线程将定时器任务添加到任务队列, 等待 JS 引擎线程执行.
- 异步 HTTP 请求线程:
XMLHttpRequest
连接后, 通过浏览器产生一个新的异步 HTTP 请求线程.- 当检测到 HTTP 请求状态变更时, 此线程产生状态变更事件, 并将 HTTP 响应处理函数 (用户回调函数) 添加到任务队列, 等待 JS 引擎线程执行.
GUI 渲染线程与 JS 引擎线程互斥.
Threads help achieve:
- Performance isolation and responsiveness in spite of slow tasks.
- Helper threads: sending long-running subtasks off to additional threads, to keep parent thread responsive to other requests happening simultaneously.
- Multiple buffering: showing previously rendered content while rendering new content, to hide latency of rendering.
- Pipeline parallelization: running rendering pipeline in multiple places simultaneously (fast scrolling and animation).
RenderingNG HTML Parser
DTD is context-sensitive grammar. Use State Machine pattern to implement a tokenizer:
Data -> Tag Open -> Tag Name -> Tag Close -> Data.
tokenizer send tokens to constructor, constructing DOM tree:
initial -> before HTML -> before head -> in head -> after head -> in body -> after body -> after after body -> EOF token.
HTML parser performance:
<= 1500
DOM nodes.<= 60
children nodes.<= 32
levels.
RenderingNG CSS Parser
CSS is context-free grammar. Webkit use flex/bison (bottom-to-up), Gecko use up-to-bottom.
ruleSet
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
selector
: simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
;
simple_selector
: element_name [ HASH | class | attrib | pseudo ]*
| [ HASH | class | attrib | pseudo ]+
;
class
: '.' IDENT
;
element_name
: IDENT | '*'
;
attrib
: '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
[ IDENT | STRING ] S* ] ']'
;
pseudo
: ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
;
RenderingNG Layout Engine
为避免对所有细小更改都进行整体布局, 浏览器采用了一种 dirty bit
系统.
如果某个呈现器发生了更改, 或者将自身及其子代标注为 dirty
, 则需要进行布局:
- 父呈现器确定自己的宽度.
- 父呈现器依次处理子呈现器, 并且:
- 放置子呈现器 (设置
(x, y)
坐标). - 如果有必要, 调用子呈现器的布局 (如果子呈现器是 dirty 的, 或者这是全局布局, 或出于其他某些原因), 这会计算子呈现器的高度.
- 放置子呈现器 (设置
- 父呈现器根据子呈现器的累加高度以及边距和补白的高度来设置自身高度, 此值也可供父呈现器的父呈现器使用.
- 将其
dirty 位
设置为false
.
RenderingNG
layout engine
generate
immutable fragment tree
and
immutable flat list.
It helps mitigate layout bugs including:
RenderingNG Paint Engine
Paint order:
- 背景颜色.
- 背景图片.
- 边框.
- 子代.
- 轮廓.
Property trees are data structures that explain how visual and scrolling effects apply to DOM elements. Every web document has four separate property trees:
- Transform tree represents CSS transforms and scrolling.
- Clip tree represents overflow clips.
- Effect tree represents all other visual effects: opacity, filters, masks, blend modes, and other kinds of clips such as clip-path.
- Scroll tree represents information about scrolling,. such as how scrolls chain together
JavaScript 阻塞渲染:
- 渲染进程 (
Render
process) 中主线程 (main
thread)Layout
阶段与Paint
阶段不负责实际的绘制操作:Layout
阶段执行Update Layer Tree
, 更新每层信息,Paint
阶段整理每层页面的绘制信息, 构建绘制列表. - 以上阶段的数据最终会交给
渲染进程 (
Render
process) 中合成线程 (compositor
thread) 与Viz
process 执行实际的绘制操作. - JavaScript 阻塞了同在主线程的
Layout
阶段与Paint
阶段, 间接阻塞了compositor
thread 与Viz
process 的绘制操作.
RenderingNG Video Engine
- Each video talks directly to
Viz
process: video rendering is largely decoupled fromRenderingNG
main rendering pipeline.
RenderingNG Scrolling Performance
- Cached GPU textures and display lists: help battery life and animation frame rate for scrolling.
- Every possible scroll is threaded: don't have to depend on the JavaScript and layout thread.
RenderingNG Animation Performance
CSS transform animation only runs on compositor
thread and Viz
process.
Browser Engine Reference
- Chromium rendering engine: RenderingNG.
- Chromium
RenderingNG
architecture. - Chromium
RenderingNG
key data structures. - Chromium video engine architecture.
- Chromium layout engine architecture.
- Chromium
Blink
architecture. - Web platform tests.
- Google Chrome platform feature status.
- Mozilla Firefox platform feature status.