May 08, 2021 ES6
1. 1. Problems with the sync traverser
2. 2. The interface that is asynchronously traversed
As the Traverser chapter says, the Iterator interface is a protocol for data traversal, and whenever you call
next
you get an object that represents the information at that location where the current traversal pointer is located.
next
of the object returned by the next method is
{value, done}
where
value
of the current data, and done
done
a boolean value that indicates whether the traversal is over or not.
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
// ...
In the above code, the variable
it
is a
(iterator)
Each time the
it.next()
called, an object is returned that represents information about the current traversal location.
There is an implicit rule that the
it.next()
be synchronized and must return a value as soon as it is called. T
hat is, once the
it.next()
executed, the value and
done
value
must be obtained simultaneously.
If the traversal pointer points exactly to the synchronization operation, of course, there is no problem, but for asynchronous operations, it is not appropriate.
function idMaker() {
let index = 0;
return {
next: function() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
resolve({ value: index++, done: false });
}, 1000);
});
}
};
}
In the code above,
next()
returns a
Promise
object, which is not possible and does not comply with
Iterator
as long as the code contains asynchronous operations.
That is, the
next()
method in the
Iterator
protocol can only contain synchronization operations.
The current workaround is to wrap the asynchronous operation as
Thunk
function or
Promise
next()
of the
value
Promise
a value that
Thunk
a Promise object, waiting for the real value to be
done
still produced synchronously.
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
// ...
In the code above,
value
value of the value property is
Promise
object that places asynchronous operations.
But writing like this is cumbersome, not intuitive, and semantic.
ES2018
introduces the Async Iterator, which provides a native traverser interface for asynchronous
value
and
done
properties are asynchronous.
The greatest syntax feature of the asynchronous traverser is that it calls the
next
of the traverser and returns
Promise
object.
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
In the code above,
asyncIterator
an asynchronous traverser
next
Promise object after calling the
Promise
method. T
herefore, you
then
the then method to specify that the state of this
Promise
resolve
a callback function after resolve.
The argument to the callback function is an object
value
and
done
which is the same as the synchronous traverser.
We know that the interface of an object's synchronous traverser is
Symbol.iterator
property. S
imilarly, the object's asynchronous traverser interface is
Symbol.asyncIterator
property.
Whatever the object, as long as
Symbol.asyncIterator
has a value, it should be asynchronously traversed.
The following is an example of an asynchronous traverser.
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 }
});
In the above code, the asynchronous traverser actually returns the value twice. O
n the first call, a
Promise
object is returned, and when the Promise
resolve
an object representing the current data member information is returned.
Promise
This means that the asynchronous traverser is consistent with the final behavior of the synchronous traverser, but returns the Promise object as a mediation first.
Because of the next method of the
next
a Promise object
Promise
returned.
Therefore, you can put it
await
command.
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 }
}
In the code above,
next
does not have to be used after it is
then
with
await
The whole process is very close to synchronous processing.
Note that the next method of the asynchronous
next
can be called continuously, without having to wait for
Promise
in the previous
resolve
to be called later. I
n this case,
next
method accumulates and automatically runs in the order of each step.
Here's an example of putting
next
Promise.all
method.
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
Another use is to call all the next methods
next
and
await
the last step.
async function runner() {
const writer = openFile('someFile.txt');
writer.next('hello');
writer.next('world');
await writer.return();
}
runner();
As mentioned earlier,
for...of
loop is used to traverse
Iterator
interface. N
ewly introduced
for await...of
loop, is the Iterator interface for
Iterator
`
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b
In the above code,
createAsyncIterable()
returns an object with an asynchronous traverser interface,
for...of
loop automatically calls the next method of the object's
next
and gets a
Promise
object.
await
is used to handle
Promise
object, and
resolve
the resulting
( x )
in
for...of
loop body.
for await...of
use of the loop is to deploy an asynchronous interface for
asyncIterable
operation, which can be placed directly into the loop.
let body = '';
async function f() {
for await(const data of req) body += data;
const parsed = JSON.parse(body);
console.log('got', parsed);
}
In the code above,
req
is an
asyncIterable
that reads data asynchronously. A
s you can see,
for await...of
loop, the code will be very concise.
If
next
method returns a
Promise
object that is
reject
for await...of
will report an error, to use
try...catch
snap.
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}
Note,
for await...of
loop can also be used to synchronize the traverser.
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
// a
// b
Node v10
asynchronous traversers,
Stream
deployed this interface.
Here's the difference between traditional and asynchronous traverser writing for reading files.
// 传统写法
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 ###');
}
Just as the
Generator
function returns a synchronous traverser object, the
Generator
function is used to return an asynchronous traverser object.
In syntax, the
Generator
function is the combination of the
async
function and the
Generator
function.
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
In the code above,
gen
is an asynchronous
Generator
function that returns an asynchronous
Iterator
execution.
Call the
next
the object and return a
Promise
object.
One of the design purposes of the asynchronous traverser
Generator
function can use the same set of interfaces when handling synchronization and asynchronous operations.
// 同步 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);
}
}
In the above code,
map
is a
Generator
function, the first argument is the
iterable
and the second argument is a callback
func
map
role of map is
iterable
value returned by iterable at each
func
There are two versions
map
the previous dealing with the synchronous traverser, and the latest dealing with the asynchronous traverser, and you can see that the two versions are written in much the same way.
Here is another example of an
Generator
function.
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
In the code above, the asynchronous operation is preceded by
await
keyword indicating that the action after
await
should return the
Promise
object.
Wherever the
yield
is used, it is where the
next
stops, and the value of the expression that follows it
await file.readLine()
next()
the object as
value
which is
Generator
function.
Inside the
Generator
function, you can use both
await
and
yield
As you can understand,
await
command is used to input values generated by external operations into the inside of the
yield
command is used to output the values inside the function.
The asynchronous Generator function defined
Generator
code above is used as follows.
(async function () {
for await (const line of readLines(filePath)) {
console.log(line);
}
})()
Asynchronous
Generator
can be associated
for await...of
are used together.
async function* prefixLines(asyncIterable) {
for await (const line of asyncIterable) {
yield '> ' + line;
}
}
The return value of the asynchronous
Generator
function is an
Iterator
that is,
next
method, a
Promise
object is
yield
be a Promise object after the
Promise
command.
If, as in the example above,
yield
command is followed by a string that is automatically wrapped as a
Promise
object.
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);
})
In the code above,
ag
is
asyncGenerator
object returned by the asyncGenerator function.
After
ag.next()
above code is executed in the following order.
ag.next()
a Promise object
Promise
asyncGenerator
function starts executing and prints
Start
await
command returns a
Promise
object, where
asyncGenerator
function stops.
fulfilled
the resulting value is
result
variable, and the
asyncGenerator
function continues to execute down.
B
at
yield
execution, and
yield
command is valued, the Promise object
Promise
ag.next()
fulfilled
state.
then
ag.next()
starts executing.
The parameter of the callback function is an
{value, done}
where
value
value of the expression after the
yield
command, and the value of
done
is
false
Lines A and B behave like the following code.
return new Promise((resolve, reject) => {
fetchRandom()
.then(result => result.text())
.then(result => {
resolve({
value: 'Result: ' + result,
done: false,
});
});
});
If an asynchronous
Generator
throws an error, the state of
Promise
object
reject
and then the thrown error is
catch
the catch method.
async function* asyncGenerator() {
throw new Error('Problem!');
}
asyncGenerator()
.next()
.catch(err => console.log(err)); // Error: Problem!
Note that the
async
function returns a
Promise
object, while the
Generator
function returns an
Iterator
object. I
t is understood that
async
function and the
Generator
are two methods of encapsulating asynchronous operations, both for the same purpose. T
he difference is that the former brings its own executor, and the latter
for await...of
or write the executor yourself.
Below is an executor
Generator
asynchronous Generator function.
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;
}
In the above code, the
Generator
generated
while
Generator function is executed
await iterator.next()
it goes into the next cycle.
Once the
done
true
it jumps out of the loop and the asynchronous traverser execution ends.
The following is an example of the use of this auto-executor.
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']
})
After the
Generator
function appeared,
JavaScript
had four function forms: normal
async
Generator
function, and asynchronous
Generator
function. N
otice the differences between each function.
Basically, you can use the async function if it is a series of asynchronous operations that are performed sequentially
async
new, and then depositing it on a hard disk), or if it is a series of asynchronous operations that produce the same data structure, such as reading a file one line at a time, you can use an
Generator
function.
The asynchronous
Generator
function can also
next
passed data through the parameters of the next method.
const writer = openFile('someFile.txt');
writer.next('hello'); // 立即执行
writer.next('world'); // 立即执行
await writer.return(); // 等待写入结束
In the code above,
openFile
is an asynchronous
Generator
function.
next
of the next method, which incomings data to the operation inside the function.
Each
next
is executed synchronously, and the last await
await
is used to wait for the entire write operation to end.
Finally, synchronized data structures can also use asynchronous
Generator
async function* createAsyncIterable(syncIterable) {
for (const elem of syncIterable) {
yield elem;
}
}
In the code above, the await keyword is not used because there are no
await
yield*
can also be followed by
异步遍历器
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}
async function* gen2() {
// result 最终会等于 2
const result = yield* gen1();
}
In the code above,
gen2
in the
result
function, the last value is 2.
Like the
Generator
function,
for await...of
loop
yield*
(async function () {
for await (const x of gen2()) {
console.log(x);
}
})();
// a
// b