diff --git a/docs/array.md b/docs/array.md index 0aed4e4..08dd118 100644 --- a/docs/array.md +++ b/docs/array.md @@ -546,7 +546,7 @@ function ArrayOf(){ ## 数组实例的 copyWithin() -数组实例的`copyWithin`方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。 +数组实例的`copyWithin()`方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。 ```javascript Array.prototype.copyWithin(target, start = 0, end = this.length) @@ -555,8 +555,8 @@ Array.prototype.copyWithin(target, start = 0, end = this.length) 它接受三个参数。 - target(必需):从该位置开始替换数据。如果为负值,表示倒数。 -- start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。 -- end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。 +- start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。 +- end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。 这三个参数都应该是数值,如果不是,会自动转为数值。 diff --git a/docs/arraybuffer.md b/docs/arraybuffer.md index cf883c3..9eb73ee 100644 --- a/docs/arraybuffer.md +++ b/docs/arraybuffer.md @@ -34,11 +34,11 @@ 很多浏览器操作的 API,用到了二进制数组操作二进制数据,下面是其中的几个。 -- File API -- XMLHttpRequest -- Fetch API -- Canvas -- WebSockets +- [Canvas](#canvas) +- [Fetch API](#fetch-api) +- [File API](#file-api) +- [WebSockets](#websocket) +- [XMLHttpRequest](#ajax) ## ArrayBuffer 对象 @@ -448,37 +448,42 @@ Float64Array.BYTES_PER_ELEMENT // 8 ### ArrayBuffer 与字符串的互相转换 -`ArrayBuffer`转为字符串,或者字符串转为`ArrayBuffer`,有一个前提,即字符串的编码方法是确定的。假定字符串采用 UTF-16 编码(JavaScript 的内部编码方式),可以自己编写转换函数。 +`ArrayBuffer` 和字符串的相互转换,使用原生 `TextEncoder` 和 `TextDecoder` 方法。为了便于说明用法,下面的代码都按照 TypeScript 的用法,给出了类型签名。 ```javascript -// ArrayBuffer 转为字符串,参数为 ArrayBuffer 对象 -function ab2str(buf) { - // 注意,如果是大型二进制数组,为了避免溢出, - // 必须一个一个字符地转 - if (buf && buf.byteLength < 1024) { - return String.fromCharCode.apply(null, new Uint16Array(buf)); - } - - const bufView = new Uint16Array(buf); - const len = bufView.length; - const bstr = new Array(len); - for (let i = 0; i < len; i++) { - bstr[i] = String.fromCharCode.call(null, bufView[i]); - } - return bstr.join(''); +/** + * Convert ArrayBuffer/TypedArray to String via TextDecoder + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder + */ +function ab2str( + input: ArrayBuffer | Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array, + outputEncoding: string = 'utf8', +): string { + const decoder = new TextDecoder(outputEncoding) + return decoder.decode(input) } -// 字符串转为 ArrayBuffer 对象,参数为字符串 -function str2ab(str) { - const buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节 - const bufView = new Uint16Array(buf); - for (let i = 0, strLen = str.length; i < strLen; i++) { - bufView[i] = str.charCodeAt(i); - } - return buf; +/** + * Convert String to ArrayBuffer via TextEncoder + * + * @see https://developer.mozilla.org/zh-CN/docs/Web/API/TextEncoder + */ +function str2ab(input: string): ArrayBuffer { + const view = str2Uint8Array(input) + return view.buffer +} + +/** Convert String to Uint8Array */ +function str2Uint8Array(input: string): Uint8Array { + const encoder = new TextEncoder() + const view = encoder.encode(input) + return view } ``` +上面代码中,`ab2str()`的第二个参数`outputEncoding`给出了输出编码的编码,一般保持默认值(`utf-8`),其他可选值参见[官方文档](https://encoding.spec.whatwg.org)或 [Node.js 文档](https://nodejs.org/api/util.html#util_whatwg_supported_encodings)。 + ### 溢出 不同的视图类型,所能容纳的数值范围是确定的。超出这个范围,就会出现溢出。比如,8 位视图只能容纳一个 8 位的二进制值,如果放入一个 9 位的值,就会溢出。 @@ -574,7 +579,7 @@ v3.byteOffset // 2 ### TypedArray.prototype.length -`length`属性表示 TypedArray 数组含有多少个成员。注意将`byteLength`属性和`length`属性区分,前者是字节长度,后者是成员长度。 +`length`属性表示 `TypedArray` 数组含有多少个成员。注意将 `length` 属性和 `byteLength` 属性区分,前者是成员长度,后者是字节长度。 ```javascript const a = new Int16Array(8); diff --git a/docs/async-iterator.md b/docs/async-iterator.md new file mode 100644 index 0000000..6e5d70a --- /dev/null +++ b/docs/async-iterator.md @@ -0,0 +1,475 @@ +# 异步遍历器 + +## 同步遍历器的问题 + +《遍历器》一章说过,Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的`next`方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。`next`方法返回的对象的结构是`{value, done}`,其中`value`表示当前的数据的值,`done`是一个布尔值,表示遍历是否结束。 + +```javascript +function idMaker() { + let index = 0; + + return { + next: function() { + return { value: index++, done: false }; + } + }; +} + +const it = idMaker(); + +it.next().value // 0 +it.next().value // 1 +it.next().value // 2 +// ... +``` + +上面代码中,变量`it`是一个遍历器(iterator)。每次调用`it.next()`方法,就返回一个对象,表示当前遍历位置的信息。 + +这里隐含着一个规定,`it.next()`方法必须是同步的,只要调用就必须立刻返回值。也就是说,一旦执行`it.next()`方法,就必须同步地得到`value`和`done`这两个属性。如果遍历指针正好指向同步操作,当然没有问题,但对于异步操作,就不太合适了。 + +```javascript +function idMaker() { + let index = 0; + + return { + next: function() { + return new Promise(function (resolve, reject) { + setTimeout(() => { + resolve({ value: index++, done: false }); + }, 1000); + }); + } + }; +} +``` + +上面代码中,`next()`方法返回的是一个 Promise 对象,这样就不行,不符合 Iterator 协议,只要代码里面包含异步操作都不行。也就是说,Iterator 协议里面`next()`方法只能包含同步操作。 + +目前的解决方法是,将异步操作包装成 Thunk 函数或者 Promise 对象,即`next()`方法返回值的`value`属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而`done`属性则还是同步产生的。 + +```javascript +function idMaker() { + let index = 0; + + return { + next: function() { + return { + value: new Promise(resolve => setTimeout(() => resolve(index++), 1000)), + done: false + }; + } + }; +} + +const it = idMaker(); + +it.next().value.then(o => console.log(o)) // 1 +it.next().value.then(o => console.log(o)) // 2 +it.next().value.then(o => console.log(o)) // 3 +// ... +``` + +上面代码中,`value`属性的返回值是一个 Promise 对象,用来放置异步操作。但是这样写很麻烦,不太符合直觉,语义也比较绕。 + +ES2018 [引入](https://github.com/tc39/proposal-async-iteration)了“异步遍历器”(Async Iterator),为异步操作提供原生的遍历器接口,即`value`和`done`这两个属性都是异步产生。 + +## 异步遍历的接口 + +异步遍历器的最大的语法特点,就是调用遍历器的`next`方法,返回的是一个 Promise 对象。 + +```javascript +asyncIterator + .next() + .then( + ({ value, done }) => /* ... */ + ); +``` + +上面代码中,`asyncIterator`是一个异步遍历器,调用`next`方法以后,返回一个 Promise 对象。因此,可以使用`then`方法指定,这个 Promise 对象的状态变为`resolve`以后的回调函数。回调函数的参数,则是一个具有`value`和`done`两个属性的对象,这个跟同步遍历器是一样的。 + +我们知道,一个对象的同步遍历器的接口,部署在`Symbol.iterator`属性上面。同样地,对象的异步遍历器接口,部署在`Symbol.asyncIterator`属性上面。不管是什么样的对象,只要它的`Symbol.asyncIterator`属性有值,就表示应该对它进行异步遍历。 + +下面是一个异步遍历器的例子。 + +```javascript +const asyncIterable = createAsyncIterable(['a', 'b']); +const asyncIterator = asyncIterable[Symbol.asyncIterator](); + +asyncIterator +.next() +.then(iterResult1 => { + console.log(iterResult1); // { value: 'a', done: false } + return asyncIterator.next(); +}) +.then(iterResult2 => { + console.log(iterResult2); // { value: 'b', done: false } + return asyncIterator.next(); +}) +.then(iterResult3 => { + console.log(iterResult3); // { value: undefined, done: true } +}); +``` + +上面代码中,异步遍历器其实返回了两次值。第一次调用的时候,返回一个 Promise 对象;等到 Promise 对象`resolve`了,再返回一个表示当前数据成员信息的对象。这就是说,异步遍历器与同步遍历器最终行为是一致的,只是会先返回 Promise 对象,作为中介。 + +由于异步遍历器的`next`方法,返回的是一个 Promise 对象。因此,可以把它放在`await`命令后面。 + +```javascript +async function f() { + const asyncIterable = createAsyncIterable(['a', 'b']); + const asyncIterator = asyncIterable[Symbol.asyncIterator](); + console.log(await asyncIterator.next()); + // { value: 'a', done: false } + console.log(await asyncIterator.next()); + // { value: 'b', done: false } + console.log(await asyncIterator.next()); + // { value: undefined, done: true } +} +``` + +上面代码中,`next`方法用`await`处理以后,就不必使用`then`方法了。整个流程已经很接近同步处理了。 + +注意,异步遍历器的`next`方法是可以连续调用的,不必等到上一步产生的 Promise 对象`resolve`以后再调用。这种情况下,`next`方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把所有的`next`方法放在`Promise.all`方法里面。 + +```javascript +const asyncIterable = createAsyncIterable(['a', 'b']); +const asyncIterator = asyncIterable[Symbol.asyncIterator](); +const [{value: v1}, {value: v2}] = await Promise.all([ + asyncIterator.next(), asyncIterator.next() +]); + +console.log(v1, v2); // a b +``` + +另一种用法是一次性调用所有的`next`方法,然后`await`最后一步操作。 + +```javascript +async function runner() { + const writer = openFile('someFile.txt'); + writer.next('hello'); + writer.next('world'); + await writer.return(); +} + +runner(); +``` + +## for await...of + +前面介绍过,`for...of`循环用于遍历同步的 Iterator 接口。新引入的`for await...of`循环,则是用于遍历异步的 Iterator 接口。 + +```javascript +async function f() { + for await (const x of createAsyncIterable(['a', 'b'])) { + console.log(x); + } +} +// a +// b +``` + +上面代码中,`createAsyncIterable()`返回一个拥有异步遍历器接口的对象,`for...of`循环自动调用这个对象的异步遍历器的`next`方法,会得到一个 Promise 对象。`await`用来处理这个 Promise 对象,一旦`resolve`,就把得到的值(`x`)传入`for...of`的循环体。 + +`for await...of`循环的一个用途,是部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。 + +```javascript +let body = ''; + +async function f() { + for await(const data of req) body += data; + const parsed = JSON.parse(body); + console.log('got', parsed); +} +``` + +上面代码中,`req`是一个 asyncIterable 对象,用来异步读取数据。可以看到,使用`for await...of`循环以后,代码会非常简洁。 + +如果`next`方法返回的 Promise 对象被`reject`,`for await...of`就会报错,要用`try...catch`捕捉。 + +```javascript +async function () { + try { + for await (const x of createRejectingIterable()) { + console.log(x); + } + } catch (e) { + console.error(e); + } +} +``` + +注意,`for await...of`循环也可以用于同步遍历器。 + +```javascript +(async function () { + for await (const x of ['a', 'b']) { + console.log(x); + } +})(); +// a +// b +``` + +Node v10 支持异步遍历器,Stream 就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异。 + +```javascript +// 传统写法 +function main(inputFilePath) { + const readStream = fs.createReadStream( + inputFilePath, + { encoding: 'utf8', highWaterMark: 1024 } + ); + readStream.on('data', (chunk) => { + console.log('>>> '+chunk); + }); + readStream.on('end', () => { + console.log('### DONE ###'); + }); +} + +// 异步遍历器写法 +async function main(inputFilePath) { + const readStream = fs.createReadStream( + inputFilePath, + { encoding: 'utf8', highWaterMark: 1024 } + ); + + for await (const chunk of readStream) { + console.log('>>> '+chunk); + } + console.log('### DONE ###'); +} +``` + +## 异步 Generator 函数 + +就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。 + +在语法上,异步 Generator 函数就是`async`函数与 Generator 函数的结合。 + +```javascript +async function* gen() { + yield 'hello'; +} +const genObj = gen(); +genObj.next().then(x => console.log(x)); +// { value: 'hello', done: false } +``` + +上面代码中,`gen`是一个异步 Generator 函数,执行后返回一个异步 Iterator 对象。对该对象调用`next`方法,返回一个 Promise 对象。 + +异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。 + +```javascript +// 同步 Generator 函数 +function* map(iterable, func) { + const iter = iterable[Symbol.iterator](); + while (true) { + const {value, done} = iter.next(); + if (done) break; + yield func(value); + } +} + +// 异步 Generator 函数 +async function* map(iterable, func) { + const iter = iterable[Symbol.asyncIterator](); + while (true) { + const {value, done} = await iter.next(); + if (done) break; + yield func(value); + } +} +``` + +上面代码中,`map`是一个 Generator 函数,第一个参数是可遍历对象`iterable`,第二个参数是一个回调函数`func`。`map`的作用是将`iterable`每一步返回的值,使用`func`进行处理。上面有两个版本的`map`,前一个处理同步遍历器,后一个处理异步遍历器,可以看到两个版本的写法基本上是一致的。 + +下面是另一个异步 Generator 函数的例子。 + +```javascript +async function* readLines(path) { + let file = await fileOpen(path); + + try { + while (!file.EOF) { + yield await file.readLine(); + } + } finally { + await file.close(); + } +} +``` + +上面代码中,异步操作前面使用`await`关键字标明,即`await`后面的操作,应该返回 Promise 对象。凡是使用`yield`关键字的地方,就是`next`方法停下来的地方,它后面的表达式的值(即`await file.readLine()`的值),会作为`next()`返回对象的`value`属性,这一点是与同步 Generator 函数一致的。 + +异步 Generator 函数内部,能够同时使用`await`和`yield`命令。可以这样理解,`await`命令用于将外部操作产生的值输入函数内部,`yield`命令用于将函数内部的值输出。 + +上面代码定义的异步 Generator 函数的用法如下。 + +```javascript +(async function () { + for await (const line of readLines(filePath)) { + console.log(line); + } +})() +``` + +异步 Generator 函数可以与`for await...of`循环结合起来使用。 + +```javascript +async function* prefixLines(asyncIterable) { + for await (const line of asyncIterable) { + yield '> ' + line; + } +} +``` + +异步 Generator 函数的返回值是一个异步 Iterator,即每次调用它的`next`方法,会返回一个 Promise 对象,也就是说,跟在`yield`命令后面的,应该是一个 Promise 对象。如果像上面那个例子那样,`yield`命令后面是一个字符串,会被自动包装成一个 Promise 对象。 + +```javascript +function fetchRandom() { + const url = 'https://www.random.org/decimal-fractions/' + + '?num=1&dec=10&col=1&format=plain&rnd=new'; + return fetch(url); +} + +async function* asyncGenerator() { + console.log('Start'); + const result = await fetchRandom(); // (A) + yield 'Result: ' + await result.text(); // (B) + console.log('Done'); +} + +const ag = asyncGenerator(); +ag.next().then(({value, done}) => { + console.log(value); +}) +``` + +上面代码中,`ag`是`asyncGenerator`函数返回的异步遍历器对象。调用`ag.next()`以后,上面代码的执行顺序如下。 + +1. `ag.next()`立刻返回一个 Promise 对象。 +1. `asyncGenerator`函数开始执行,打印出`Start`。 +1. `await`命令返回一个 Promise 对象,`asyncGenerator`函数停在这里。 +1. A 处变成 fulfilled 状态,产生的值放入`result`变量,`asyncGenerator`函数继续往下执行。 +1. 函数在 B 处的`yield`暂停执行,一旦`yield`命令取到值,`ag.next()`返回的那个 Promise 对象变成 fulfilled 状态。 +1. `ag.next()`后面的`then`方法指定的回调函数开始执行。该回调函数的参数是一个对象`{value, done}`,其中`value`的值是`yield`命令后面的那个表达式的值,`done`的值是`false`。 + +A 和 B 两行的作用类似于下面的代码。 + +```javascript +return new Promise((resolve, reject) => { + fetchRandom() + .then(result => result.text()) + .then(result => { + resolve({ + value: 'Result: ' + result, + done: false, + }); + }); +}); +``` + +如果异步 Generator 函数抛出错误,会导致 Promise 对象的状态变为`reject`,然后抛出的错误被`catch`方法捕获。 + +```javascript +async function* asyncGenerator() { + throw new Error('Problem!'); +} + +asyncGenerator() +.next() +.catch(err => console.log(err)); // Error: Problem! +``` + +注意,普通的 async 函数返回的是一个 Promise 对象,而异步 Generator 函数返回的是一个异步 Iterator 对象。可以这样理解,async 函数和异步 Generator 函数,是封装异步操作的两种方法,都用来达到同一种目的。区别在于,前者自带执行器,后者通过`for await...of`执行,或者自己编写执行器。下面就是一个异步 Generator 函数的执行器。 + +```javascript +async function takeAsync(asyncIterable, count = Infinity) { + const result = []; + const iterator = asyncIterable[Symbol.asyncIterator](); + while (result.length < count) { + const {value, done} = await iterator.next(); + if (done) break; + result.push(value); + } + return result; +} +``` + +上面代码中,异步 Generator 函数产生的异步遍历器,会通过`while`循环自动执行,每当`await iterator.next()`完成,就会进入下一轮循环。一旦`done`属性变为`true`,就会跳出循环,异步遍历器执行结束。 + +下面是这个自动执行器的一个使用实例。 + +```javascript +async function f() { + async function* gen() { + yield 'a'; + yield 'b'; + yield 'c'; + } + + return await takeAsync(gen()); +} + +f().then(function (result) { + console.log(result); // ['a', 'b', 'c'] +}) +``` + +异步 Generator 函数出现以后,JavaScript 就有了四种函数形式:普通函数、async 函数、Generator 函数和异步 Generator 函数。请注意区分每种函数的不同之处。基本上,如果是一系列按照顺序执行的异步操作(比如读取文件,然后写入新内容,再存入硬盘),可以使用 async 函数;如果是一系列产生相同数据结构的异步操作(比如一行一行读取文件),可以使用异步 Generator 函数。 + +异步 Generator 函数也可以通过`next`方法的参数,接收外部传入的数据。 + +```javascript +const writer = openFile('someFile.txt'); +writer.next('hello'); // 立即执行 +writer.next('world'); // 立即执行 +await writer.return(); // 等待写入结束 +``` + +上面代码中,`openFile`是一个异步 Generator 函数。`next`方法的参数,向该函数内部的操作传入数据。每次`next`方法都是同步执行的,最后的`await`命令用于等待整个写入操作结束。 + +最后,同步的数据结构,也可以使用异步 Generator 函数。 + +```javascript +async function* createAsyncIterable(syncIterable) { + for (const elem of syncIterable) { + yield elem; + } +} +``` + +上面代码中,由于没有异步操作,所以也就没有使用`await`关键字。 + +## yield\* 语句 + +`yield*`语句也可以跟一个异步遍历器。 + +```javascript +async function* gen1() { + yield 'a'; + yield 'b'; + return 2; +} + +async function* gen2() { + // result 最终会等于 2 + const result = yield* gen1(); +} +``` + +上面代码中,`gen2`函数里面的`result`变量,最后的值是`2`。 + +与同步 Generator 函数一样,`for await...of`循环会展开`yield*`。 + +```javascript +(async function () { + for await (const x of gen2()) { + console.log(x); + } +})(); +// a +// b +``` + diff --git a/docs/async.md b/docs/async.md index 8e707a3..3c845b8 100644 --- a/docs/async.md +++ b/docs/async.md @@ -242,13 +242,34 @@ class Sleep { } (async () => { - const actualTime = await new Sleep(1000); - console.log(actualTime); + const sleepTime = await new Sleep(1000); + console.log(sleepTime); })(); +// 1000 ``` 上面代码中,`await`命令后面是一个`Sleep`对象的实例。这个实例不是 Promise 对象,但是因为定义了`then`方法,`await`会将其视为`Promise`处理。 +这个例子还演示了如何实现休眠效果。JavaScript 一直没有休眠的语法,但是借助`await`命令就可以让程序停顿指定的时间。下面给出了一个简化的`sleep`实现。 + +```javascript +function sleep(interval) { + return new Promise(resolve => { + setTimeout(resolve, interval); + }) +} + +// 用法 +async function one2FiveInAsync() { + for(let i = 1; i <= 5; i++) { + console.log(i); + await sleep(1000); + } +} + +one2FiveInAsync(); +``` + `await`命令后面的 Promise 对象如果变为`reject`状态,则`reject`的参数会被`catch`方法的回调函数接收到。 ```javascript @@ -486,24 +507,6 @@ async function dbFuc(db) { } ``` -目前,[`esm`](https://www.npmjs.com/package/esm)模块加载器支持顶层`await`,即`await`命令可以不放在 async 函数里面,直接使用。 - -```javascript -// async 函数的写法 -const start = async () => { - const res = await fetch('google.com'); - return res.text(); -}; - -start().then(console.log); - -// 顶层 await 的写法 -const res = await fetch('google.com'); -console.log(await res.text()); -``` - -上面代码中,第二种写法的脚本必须使用`esm`加载器,才会生效。 - 第四点,async 函数可以保留运行堆栈。 ```javascript @@ -702,410 +705,155 @@ async function logInOrder(urls) { 上面代码中,虽然`map`方法的参数是`async`函数,但它是并发执行的,因为只有`async`函数内部是继发执行,外部不受影响。后面的`for..of`循环内部使用了`await`,因此实现了按顺序输出。 -## 异步遍历器 +## 顶层 await -《遍历器》一章说过,Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的`next`方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。`next`方法返回的对象的结构是`{value, done}`,其中`value`表示当前的数据的值,`done`是一个布尔值,表示遍历是否结束。 - -这里隐含着一个规定,`next`方法必须是同步的,只要调用就必须立刻返回值。也就是说,一旦执行`next`方法,就必须同步地得到`value`和`done`这两个属性。如果遍历指针正好指向同步操作,当然没有问题,但对于异步操作,就不太合适了。目前的解决方法是,Generator 函数里面的异步操作,返回一个 Thunk 函数或者 Promise 对象,即`value`属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而`done`属性则还是同步产生的。 - -ES2018 [引入](https://github.com/tc39/proposal-async-iteration)了“异步遍历器”(Async Iterator),为异步操作提供原生的遍历器接口,即`value`和`done`这两个属性都是异步产生。 - -### 异步遍历的接口 - -异步遍历器的最大的语法特点,就是调用遍历器的`next`方法,返回的是一个 Promise 对象。 +根据语法规格,`await`命令只能出现在 async 函数内部,否则都会报错。 ```javascript -asyncIterator - .next() - .then( - ({ value, done }) => /* ... */ - ); +// 报错 +const data = await fetch('https://api.example.com'); ``` -上面代码中,`asyncIterator`是一个异步遍历器,调用`next`方法以后,返回一个 Promise 对象。因此,可以使用`then`方法指定,这个 Promise 对象的状态变为`resolve`以后的回调函数。回调函数的参数,则是一个具有`value`和`done`两个属性的对象,这个跟同步遍历器是一样的。 +上面代码中,`await`命令独立使用,没有放在 async 函数里面,就会报错。 -我们知道,一个对象的同步遍历器的接口,部署在`Symbol.iterator`属性上面。同样地,对象的异步遍历器接口,部署在`Symbol.asyncIterator`属性上面。不管是什么样的对象,只要它的`Symbol.asyncIterator`属性有值,就表示应该对它进行异步遍历。 - -下面是一个异步遍历器的例子。 +目前,有一个[语法提案](https://github.com/tc39/proposal-top-level-await),允许在模块的顶层独立使用`await`命令。这个提案的目的,是借用`await`解决模块异步加载的问题。 ```javascript -const asyncIterable = createAsyncIterable(['a', 'b']); -const asyncIterator = asyncIterable[Symbol.asyncIterator](); +// awaiting.js +let output; +async function main() { + const dynamic = await import(someMission); + const data = await fetch(url); + output = someProcess(dynamic.default, data); +} +main(); +export { output }; +``` -asyncIterator -.next() -.then(iterResult1 => { - console.log(iterResult1); // { value: 'a', done: false } - return asyncIterator.next(); -}) -.then(iterResult2 => { - console.log(iterResult2); // { value: 'b', done: false } - return asyncIterator.next(); -}) -.then(iterResult3 => { - console.log(iterResult3); // { value: undefined, done: true } +上面代码中,模块`awaiting.js`的输出值`output`,取决于异步操作。我们把异步操作包装在一个 async 函数里面,然后调用这个函数,只有等里面的异步操作都执行,变量`output`才会有值,否则就返回`undefined`。 + +上面的代码也可以写成立即执行函数的形式。 + +```javascript +// awaiting.js +let output; +(async function main() { + const dynamic = await import(someMission); + const data = await fetch(url); + output = someProcess(dynamic.default, data); +})(); +export { output }; +``` + +下面是加载这个模块的写法。 + +```javascript +// usage.js +import { output } from "./awaiting.js"; + +function outputPlusValue(value) { return output + value } + +console.log(outputPlusValue(100)); +setTimeout(() => console.log(outputPlusValue(100), 1000); +``` + +上面代码中,`outputPlusValue()`的执行结果,完全取决于执行的时间。如果`awaiting.js`里面的异步操作没执行完,加载进来的`output`的值就是`undefined`。 + +目前的解决方法,就是让原始模块输出一个 Promise 对象,从这个 Promise 对象判断异步操作有没有结束。 + +```javascript +// awaiting.js +let output; +export default (async function main() { + const dynamic = await import(someMission); + const data = await fetch(url); + output = someProcess(dynamic.default, data); +})(); +export { output }; +``` + +上面代码中,`awaiting.js`除了输出`output`,还默认输出一个 Promise 对象(async 函数立即执行后,返回一个 Promise 对象),从这个对象判断异步操作是否结束。 + +下面是加载这个模块的新的写法。 + +```javascript +// usage.js +import promise, { output } from "./awaiting.js"; + +function outputPlusValue(value) { return output + value } + +promise.then(() => { + console.log(outputPlusValue(100)); + setTimeout(() => console.log(outputPlusValue(100), 1000); }); ``` -上面代码中,异步遍历器其实返回了两次值。第一次调用的时候,返回一个 Promise 对象;等到 Promise 对象`resolve`了,再返回一个表示当前数据成员信息的对象。这就是说,异步遍历器与同步遍历器最终行为是一致的,只是会先返回 Promise 对象,作为中介。 +上面代码中,将`awaiting.js`对象的输出,放在`promise.then()`里面,这样就能保证异步操作完成以后,才去读取`output`。 -由于异步遍历器的`next`方法,返回的是一个 Promise 对象。因此,可以把它放在`await`命令后面。 +这种写法比较麻烦,等于要求模块的使用者遵守一个额外的使用协议,按照特殊的方法使用这个模块。一旦你忘了要用 Promise 加载,只使用正常的加载方法,依赖这个模块的代码就可能出错。而且,如果上面的`usage.js`又有对外的输出,等于这个依赖链的所有模块都要使用 Promise 加载。 + +顶层的`await`命令,就是为了解决这个问题。它保证只有异步操作完成,模块才会输出值。 ```javascript -async function f() { - const asyncIterable = createAsyncIterable(['a', 'b']); - const asyncIterator = asyncIterable[Symbol.asyncIterator](); - console.log(await asyncIterator.next()); - // { value: 'a', done: false } - console.log(await asyncIterator.next()); - // { value: 'b', done: false } - console.log(await asyncIterator.next()); - // { value: undefined, done: true } +// awaiting.js +const dynamic = import(someMission); +const data = fetch(url); +export const output = someProcess((await dynamic).default, await data); +``` + +上面代码中,两个异步操作在输出的时候,都加上了`await`命令。只有等到异步操作完成,这个模块才会输出值。 + +加载这个模块的写法如下。 + +```javascript +// usage.js +import { output } from "./awaiting.js"; +function outputPlusValue(value) { return output + value } + +console.log(outputPlusValue(100)); +setTimeout(() => console.log(outputPlusValue(100), 1000); +``` + +上面代码的写法,与普通的模块加载完全一样。也就是说,模块的使用者完全不用关心,依赖模块的内部有没有异步操作,正常加载即可。 + +这时,模块的加载会等待依赖模块(上例是`awaiting.js`)的异步操作完成,才执行后面的代码,有点像暂停在那里。所以,它总是会得到正确的`output`,不会因为加载时机的不同,而得到不一样的值。 + +下面是顶层`await`的一些使用场景。 + +```javascript +// import() 方法加载 +const strings = await import(`/i18n/${navigator.language}`); + +// 数据库操作 +const connection = await dbConnector(); + +// 依赖回滚 +let jQuery; +try { + jQuery = await import('https://cdn-a.com/jQuery'); +} catch { + jQuery = await import('https://cdn-b.com/jQuery'); } ``` -上面代码中,`next`方法用`await`处理以后,就不必使用`then`方法了。整个流程已经很接近同步处理了。 - -注意,异步遍历器的`next`方法是可以连续调用的,不必等到上一步产生的 Promise 对象`resolve`以后再调用。这种情况下,`next`方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把所有的`next`方法放在`Promise.all`方法里面。 +注意,如果加载多个包含顶层`await`命令的模块,加载命令是同步执行的。 ```javascript -const asyncIterable = createAsyncIterable(['a', 'b']); -const asyncIterator = asyncIterable[Symbol.asyncIterator](); -const [{value: v1}, {value: v2}] = await Promise.all([ - asyncIterator.next(), asyncIterator.next() -]); +// x.js +console.log("X1"); +await new Promise(r => setTimeout(r, 1000)); +console.log("X2"); -console.log(v1, v2); // a b +// y.js +console.log("Y"); + +// z.js +import "./x.js"; +import "./y.js"; +console.log("Z"); ``` -另一种用法是一次性调用所有的`next`方法,然后`await`最后一步操作。 +上面代码有三个模块,最后的`z.js`加载`x.js`和`y.js`,打印结果是`X1`、`Y`、`X2`、`Z`。这说明,`z.js`并没有等待`x.js`加载完成,再去加载`y.js`。 -```javascript -async function runner() { - const writer = openFile('someFile.txt'); - writer.next('hello'); - writer.next('world'); - await writer.return(); -} +顶层的`await`命令有点像,交出代码的执行权给其他的模块加载,等异步操作完成后,再拿回执行权,继续向下执行。 -runner(); -``` - -### for await...of - -前面介绍过,`for...of`循环用于遍历同步的 Iterator 接口。新引入的`for await...of`循环,则是用于遍历异步的 Iterator 接口。 - -```javascript -async function f() { - for await (const x of createAsyncIterable(['a', 'b'])) { - console.log(x); - } -} -// a -// b -``` - -上面代码中,`createAsyncIterable()`返回一个拥有异步遍历器接口的对象,`for...of`循环自动调用这个对象的异步遍历器的`next`方法,会得到一个 Promise 对象。`await`用来处理这个 Promise 对象,一旦`resolve`,就把得到的值(`x`)传入`for...of`的循环体。 - -`for await...of`循环的一个用途,是部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。 - -```javascript -let body = ''; - -async function f() { - for await(const data of req) body += data; - const parsed = JSON.parse(body); - console.log('got', parsed); -} -``` - -上面代码中,`req`是一个 asyncIterable 对象,用来异步读取数据。可以看到,使用`for await...of`循环以后,代码会非常简洁。 - -如果`next`方法返回的 Promise 对象被`reject`,`for await...of`就会报错,要用`try...catch`捕捉。 - -```javascript -async function () { - try { - for await (const x of createRejectingIterable()) { - console.log(x); - } - } catch (e) { - console.error(e); - } -} -``` - -注意,`for await...of`循环也可以用于同步遍历器。 - -```javascript -(async function () { - for await (const x of ['a', 'b']) { - console.log(x); - } -})(); -// a -// b -``` - -Node v10 支持异步遍历器,Stream 就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异。 - -```javascript -// 传统写法 -function main(inputFilePath) { - const readStream = fs.createReadStream( - inputFilePath, - { encoding: 'utf8', highWaterMark: 1024 } - ); - readStream.on('data', (chunk) => { - console.log('>>> '+chunk); - }); - readStream.on('end', () => { - console.log('### DONE ###'); - }); -} - -// 异步遍历器写法 -async function main(inputFilePath) { - const readStream = fs.createReadStream( - inputFilePath, - { encoding: 'utf8', highWaterMark: 1024 } - ); - - for await (const chunk of readStream) { - console.log('>>> '+chunk); - } - console.log('### DONE ###'); -} -``` - -### 异步 Generator 函数 - -就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。 - -在语法上,异步 Generator 函数就是`async`函数与 Generator 函数的结合。 - -```javascript -async function* gen() { - yield 'hello'; -} -const genObj = gen(); -genObj.next().then(x => console.log(x)); -// { value: 'hello', done: false } -``` - -上面代码中,`gen`是一个异步 Generator 函数,执行后返回一个异步 Iterator 对象。对该对象调用`next`方法,返回一个 Promise 对象。 - -异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。 - -```javascript -// 同步 Generator 函数 -function* map(iterable, func) { - const iter = iterable[Symbol.iterator](); - while (true) { - const {value, done} = iter.next(); - if (done) break; - yield func(value); - } -} - -// 异步 Generator 函数 -async function* map(iterable, func) { - const iter = iterable[Symbol.asyncIterator](); - while (true) { - const {value, done} = await iter.next(); - if (done) break; - yield func(value); - } -} -``` - -上面代码中,`map`是一个 Generator 函数,第一个参数是可遍历对象`iterable`,第二个参数是一个回调函数`func`。`map`的作用是将`iterable`每一步返回的值,使用`func`进行处理。上面有两个版本的`map`,前一个处理同步遍历器,后一个处理异步遍历器,可以看到两个版本的写法基本上是一致的。 - -下面是另一个异步 Generator 函数的例子。 - -```javascript -async function* readLines(path) { - let file = await fileOpen(path); - - try { - while (!file.EOF) { - yield await file.readLine(); - } - } finally { - await file.close(); - } -} -``` - -上面代码中,异步操作前面使用`await`关键字标明,即`await`后面的操作,应该返回 Promise 对象。凡是使用`yield`关键字的地方,就是`next`方法停下来的地方,它后面的表达式的值(即`await file.readLine()`的值),会作为`next()`返回对象的`value`属性,这一点是与同步 Generator 函数一致的。 - -异步 Generator 函数内部,能够同时使用`await`和`yield`命令。可以这样理解,`await`命令用于将外部操作产生的值输入函数内部,`yield`命令用于将函数内部的值输出。 - -上面代码定义的异步 Generator 函数的用法如下。 - -```javascript -(async function () { - for await (const line of readLines(filePath)) { - console.log(line); - } -})() -``` - -异步 Generator 函数可以与`for await...of`循环结合起来使用。 - -```javascript -async function* prefixLines(asyncIterable) { - for await (const line of asyncIterable) { - yield '> ' + line; - } -} -``` - -异步 Generator 函数的返回值是一个异步 Iterator,即每次调用它的`next`方法,会返回一个 Promise 对象,也就是说,跟在`yield`命令后面的,应该是一个 Promise 对象。如果像上面那个例子那样,`yield`命令后面是一个字符串,会被自动包装成一个 Promise 对象。 - -```javascript -function fetchRandom() { - const url = 'https://www.random.org/decimal-fractions/' - + '?num=1&dec=10&col=1&format=plain&rnd=new'; - return fetch(url); -} - -async function* asyncGenerator() { - console.log('Start'); - const result = await fetchRandom(); // (A) - yield 'Result: ' + await result.text(); // (B) - console.log('Done'); -} - -const ag = asyncGenerator(); -ag.next().then(({value, done}) => { - console.log(value); -}) -``` - -上面代码中,`ag`是`asyncGenerator`函数返回的异步遍历器对象。调用`ag.next()`以后,上面代码的执行顺序如下。 - -1. `ag.next()`立刻返回一个 Promise 对象。 -1. `asyncGenerator`函数开始执行,打印出`Start`。 -1. `await`命令返回一个 Promise 对象,`asyncGenerator`函数停在这里。 -1. A 处变成 fulfilled 状态,产生的值放入`result`变量,`asyncGenerator`函数继续往下执行。 -1. 函数在 B 处的`yield`暂停执行,一旦`yield`命令取到值,`ag.next()`返回的那个 Promise 对象变成 fulfilled 状态。 -1. `ag.next()`后面的`then`方法指定的回调函数开始执行。该回调函数的参数是一个对象`{value, done}`,其中`value`的值是`yield`命令后面的那个表达式的值,`done`的值是`false`。 - -A 和 B 两行的作用类似于下面的代码。 - -```javascript -return new Promise((resolve, reject) => { - fetchRandom() - .then(result => result.text()) - .then(result => { - resolve({ - value: 'Result: ' + result, - done: false, - }); - }); -}); -``` - -如果异步 Generator 函数抛出错误,会导致 Promise 对象的状态变为`reject`,然后抛出的错误被`catch`方法捕获。 - -```javascript -async function* asyncGenerator() { - throw new Error('Problem!'); -} - -asyncGenerator() -.next() -.catch(err => console.log(err)); // Error: Problem! -``` - -注意,普通的 async 函数返回的是一个 Promise 对象,而异步 Generator 函数返回的是一个异步 Iterator 对象。可以这样理解,async 函数和异步 Generator 函数,是封装异步操作的两种方法,都用来达到同一种目的。区别在于,前者自带执行器,后者通过`for await...of`执行,或者自己编写执行器。下面就是一个异步 Generator 函数的执行器。 - -```javascript -async function takeAsync(asyncIterable, count = Infinity) { - const result = []; - const iterator = asyncIterable[Symbol.asyncIterator](); - while (result.length < count) { - const {value, done} = await iterator.next(); - if (done) break; - result.push(value); - } - return result; -} -``` - -上面代码中,异步 Generator 函数产生的异步遍历器,会通过`while`循环自动执行,每当`await iterator.next()`完成,就会进入下一轮循环。一旦`done`属性变为`true`,就会跳出循环,异步遍历器执行结束。 - -下面是这个自动执行器的一个使用实例。 - -```javascript -async function f() { - async function* gen() { - yield 'a'; - yield 'b'; - yield 'c'; - } - - return await takeAsync(gen()); -} - -f().then(function (result) { - console.log(result); // ['a', 'b', 'c'] -}) -``` - -异步 Generator 函数出现以后,JavaScript 就有了四种函数形式:普通函数、async 函数、Generator 函数和异步 Generator 函数。请注意区分每种函数的不同之处。基本上,如果是一系列按照顺序执行的异步操作(比如读取文件,然后写入新内容,再存入硬盘),可以使用 async 函数;如果是一系列产生相同数据结构的异步操作(比如一行一行读取文件),可以使用异步 Generator 函数。 - -异步 Generator 函数也可以通过`next`方法的参数,接收外部传入的数据。 - -```javascript -const writer = openFile('someFile.txt'); -writer.next('hello'); // 立即执行 -writer.next('world'); // 立即执行 -await writer.return(); // 等待写入结束 -``` - -上面代码中,`openFile`是一个异步 Generator 函数。`next`方法的参数,向该函数内部的操作传入数据。每次`next`方法都是同步执行的,最后的`await`命令用于等待整个写入操作结束。 - -最后,同步的数据结构,也可以使用异步 Generator 函数。 - -```javascript -async function* createAsyncIterable(syncIterable) { - for (const elem of syncIterable) { - yield elem; - } -} -``` - -上面代码中,由于没有异步操作,所以也就没有使用`await`关键字。 - -### yield\* 语句 - -`yield*`语句也可以跟一个异步遍历器。 - -```javascript -async function* gen1() { - yield 'a'; - yield 'b'; - return 2; -} - -async function* gen2() { - // result 最终会等于 2 - const result = yield* gen1(); -} -``` - -上面代码中,`gen2`函数里面的`result`变量,最后的值是`2`。 - -与同步 Generator 函数一样,`for await...of`循环会展开`yield*`。 - -```javascript -(async function () { - for await (const x of gen2()) { - console.log(x); - } -})(); -// a -// b -``` diff --git a/docs/module.md b/docs/module.md index 42482fe..9dba64d 100644 --- a/docs/module.md +++ b/docs/module.md @@ -89,7 +89,7 @@ var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; -export {firstName, lastName, year}; +export { firstName, lastName, year }; ``` 上面代码在`export`命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在`var`语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。 @@ -190,7 +190,7 @@ foo() ```javascript // main.js -import {firstName, lastName, year} from './profile.js'; +import { firstName, lastName, year } from './profile.js'; function setName(element) { element.textContent = firstName + ' ' + lastName; diff --git a/docs/number.md b/docs/number.md index 27e384d..677114d 100644 --- a/docs/number.md +++ b/docs/number.md @@ -424,7 +424,7 @@ Math.cbrt = Math.cbrt || function(x) { ### Math.clz32() -`Math.clz32()`方法将参数转为 32 位无符号整数的形式,然后这个 32 位值里面有多少个前导 0。 +`Math.clz32()`方法将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。 ```javascript Math.clz32(0) // 32 diff --git a/docs/reflect.md b/docs/reflect.md index 5f21611..0ffc943 100644 --- a/docs/reflect.md +++ b/docs/reflect.md @@ -40,7 +40,7 @@ Reflect.has(Object, 'assign') // true ```javascript Proxy(target, { set: function(target, name, value, receiver) { - var success = Reflect.set(target,name, value, receiver); + var success = Reflect.set(target, name, value, receiver); if (success) { console.log('property ' + name + ' on ' + target + ' set to ' + value); } diff --git a/docs/string-methods.md b/docs/string-methods.md index 07b2938..318d0f7 100644 --- a/docs/string-methods.md +++ b/docs/string-methods.md @@ -43,6 +43,8 @@ String.raw`Hi\u000A!`; ```javascript String.raw`Hi\\n` // 返回 "Hi\\\\n" + +String.raw`Hi\\n` === "Hi\\\\n" // true ``` `String.raw()`方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。 @@ -123,6 +125,17 @@ for (let ch of s) { // 61 ``` +另一种方法也可以,使用扩展运算符(`...`)进行展开运算。 + +```javascript +let arr = [...'𠮷a']; // arr.length === 2 +arr.forEach( + ch => console.log(ch.codePointAt(0).toString(16)) +); +// 20bb7 +// 61 +``` + `codePointAt()`方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。 ```javascript diff --git a/sidebar.md b/sidebar.md index ce32f80..8d3579f 100644 --- a/sidebar.md +++ b/sidebar.md @@ -32,6 +32,7 @@ 1. [Module 的加载实现](#docs/module-loader) 1. [编程风格](#docs/style) 1. [读懂规格](#docs/spec) +1. [异步遍历器](#docs/async-iterator) 1. [ArrayBuffer](#docs/arraybuffer) 1. [最新提案](#docs/proposals) 1. [Decorator](#docs/decorator)