Original link: https://www.iyouhun.com/post-256.html
iterator
Iterators are an ordered, sequential, pull-based organization for consuming data to control behavior one step at a time.
To put it simply, we iterate and loop an iterable object. Instead of returning all the data at once, we call related methods to return them in batches.
An iterator is an object that helps us traverse a certain data structure. This object
has a next
function, which returns an object
with value
and done
attributes, where value
points to the value defined by the current next
function in the iteration sequence.
{ done: boolean, // 为true 时代表迭代完毕value: any // done 为true 时取值为undefined }
iteration protocol
ES6’s iteration protocol is divided into iterator protocol (iterator protocol ) and iterable protocol (iterable protocol) , and iterators are implemented based on these two protocols.
Iterator protocol: iterator协议
defines a standard method for generating value
sequences. The object is an iterator as long as it implements the required next
function. Quite a pointer to traverse the elements of the data structure, similar to a cursor in a database.
Iterable protocol: Once the iterable protocol is supported, it means that the object can be traversed with for-of
, which can be used to define or customize the iterative behavior of JS objects. Common built-in types such as Array
& Map
support the iterable protocol. The object must implement @ @iterator
method, which means the object must have a property with @ @iterator key
that can be accessed through the constant Symbol.iterator
.
Simulate implementing an iterator
Based on the iterator protocol
// 实现function createArrayIterator(arr) { let index = 0 return { next: () => index < arr.length ? { value: arr[index++], done: false } : { value: undefined, done: true }, } } // 测试const nums = [11, 22, 33, 44] const numsIterator = createArrayIterator(nums) console.log(numsIterator.next()) console.log(numsIterator.next()) console.log(numsIterator.next()) console.log(numsIterator.next()) console.log(numsIterator.next())
Based on an iterable protocol
An object that implements the method of generating an iterator is called可迭代对象
, which means that the object contains a method that returns an iterator object
Generally use Symbol.iterator
to define this property, the scientific name is called @ @iterator
method
// 一个可迭代对象需要具有[Symbol.iterator]方法,并且这个方法返回一个迭代器const obj = { names: ['111', '222', '333'], [Symbol.iterator]() { let index = 0 return { next: () => index < this.names.length ? { value: this.names[index++], done: false } : { value: undefined, done: true }, return: () => { console.log('迭代器提前终止了...') return { value: undefined, done: true } }, } }, } // 测试for (const item of obj) { console.log(item) if (item === '222') break }
In the above two mock iterator examples, it is still relatively complicated, but ES6 introduces a generator object, which can make the process of creating iterator objects much easier.
Builder
A generator is a function that returns an iterator , indicated by an asterisk ( * ) after function
keyword, and the new keyword yield
will be used in the function.
// 生成器function* creatIterator (){ yield 1 yield 2 yield 3 } const iterator = creatIterator() console.log(iterator.next()) // {value:1,done:false} console.log(iterator.next()) // {value:2,done:false} console.log(iterator.next()) // {value:3,done:false} console.log(iterator.next()) // {value:undefined,done:true}
In the above example, the asterisk * before creatIterator()
indicates that it is a generator, and the return value and return order when calling next()
method of the yield
keyword.
Whenever a yield statement is executed, the function will automatically stop executing . Taking the above example as an example, after the statement yield 1
is executed, the function will not execute any other language until next()
method of the iterator is called again to continue to execute yield 2
statement.
Note: yield
expression can only be used in the Generator function, and an error will be reported if it is used in other places.
(function (){ yield 1; })() // SyntaxError: Unexpected number
Note: ES6 does not stipulate where to write the asterisk between function
keyword and the function name. This leads to the following writing can pass.
function * foo(x, y) { ··· } function *foo(x, y) { ··· } function* foo(x, y) { ··· } function*foo(x, y) { ··· }
Generator pass parameters
yield
expression itself has no return value, or it always returns undefined
. next
method can take a parameter, which will be used as the return value of the previous yield
expression.
function* dr(arg) { console.log(arg) let one = yield '111' console.log(one) yield '222' console.log('ccc') } let iterator = dr('aaa') console.log(iterator.next()) console.log(iterator.next('bbb')) console.log(iterator.next())
Application Scenario
In daily development, when the next interface depends on the data of the previous interface, the generator can be used without considering the problem of asynchronous callback hell nesting.
Simulation: Get user data after 1s, order information after 2s, and product information after 3s
function getUser() { setTimeout(() => { const data = '用户数据' iterator.next(data) }, 1000) } function getOrder() { setTimeout(() => { const data = '订单信息' iterator.next(data) }, 2000) } function getGoods() { setTimeout(() => { const data = '商品数据' iterator.next(data) }, 3000) } function* initData() { const user = yield getUser() console.log(user) const order = yield getOrder() console.log(order) const goods = yield getGoods() console.log(goods) } const iterator = initData() iterator.next()
for of
for of
loop can get the key value in a pair of key values, because this loop is closely related to the iterator, so let’s talk about it here.
As long as a data structure is deployed with Symbol.iterator
property, it is considered to have an iterato
interface, and for of
can be used, which can loop iterable objects.
JavaScript
has the data structure of the iterable
interface by default:
- Array Array
- map
- set
- String
- Arguments object
- Nodelist objects, class arrays, and any data structure that deploys
iterator
interface can use the spread operator (…) of the array, and operations such as deconstructing and assigning.
iterate over the array
Try looping the array with for or
Since the array supports for...of
loop, the array must deploy Iterator
interface. Let’s take a look at the traversal process of Iterator
through it.
From the figure we can see:
-
Iterator
interface returns an object with anext
method. - Each call to next returns the items in the array in turn until it points to the end of the data structure.
- The returned result is an object, which contains the current value
value
and whether the current enddone
traverse objects
Try to traverse the object, we will find that he reported that the object is not iterable, as shown below
Then we can use the iterator object generator above to make the object also support for of
traversal
obj[Symbol.iterator] = function* () { yield* this.name }
You can also use Object.keys()
to get key
value set of the object, and then use for of
const obj = {name: 'youhun',age: 18} for(const key of Object.keys(obj)){ console.log(key, obj[key]) // name youhun // age 18 }
asynchronous iteration
Unlike synchronous iterable objects that deploy [Symbol.iterator]
attribute, the sign of an asynchronous iterable object is the deployment of the [Symbol.asyncIterator]
attribute.
// 用生成器生成const obj = { async *[Symbol.asyncIterator]() { yield 1; yield 2; yield 3; } } const asyncIterator = obj[Symbol.asyncIterator]() asyncIterator.next().then(data => console.log(data)) // {value: 1, done: false} asyncIterator.next().then(data => console.log(data)) // {value: 2, done: false} asyncIterator.next().then(data => console.log(data)) // {value: 3, done: false} asyncIterator.next().then(data => console.log(data)) // {value: undefined, done: true}
asyncIterator
here is an asynchronous iterator. Different from the synchronous iterator iterator
, calling next
method on asyncIterator
gets a Promise object whose internal value is in the form of { value: xx, done: xx }
, similar to Promise.resolve({ value: xx, done: xx })
.
Why have asynchronous iteration?
If synchronous iterator data acquisition takes time (such as requesting an interface in an actual scenario), then there will be problems if you use for-of
traversal.
const obj = { *[Symbol.iterator]() { yield new Promise(resolve => setTimeout(() => resolve(1), 5000)) yield new Promise(resolve => setTimeout(() => resolve(2), 2000)) yield new Promise(resolve => setTimeout(() => resolve(3), 500)) } } console.log(Date.now()) for (const item of obj) { item.then(data => console.log(Date.now(), data)) } // 1579253648926 // 1579253649427 3 // 1579253649427 - 1579253648926 = 501 // 1579253650927 2 // 1579253650927 - 1579253648926 = 2001 // 1579253653927 1 // 1579253653927 - 1579253648926 = 5001
Each item
here can be regarded as an interface request, and the time for data return is not necessarily. The above print results illustrate the problem: we cannot control the order in which the data is processed.
Let’s look at asynchronous iterators again
const obj = { async *[Symbol.asyncIterator]() { yield new Promise(resolve => setTimeout(() => resolve(1), 5000)) yield new Promise(resolve => setTimeout(() => resolve(2), 3000)) yield new Promise(resolve => setTimeout(() => resolve(3), 500)) } } console.log(Date.now()) for await (const item of obj) { console.log(Date.now(), item) } // 1579256590699 // 1579256595700 1 // 1579256595700 - 1579256590699 = 5001 // 1579256598702 2 // 1579256598702 - 1579256590699 = 8003 // 1579256599203 3 // 1579256599203 - 1579256590699 = 8504
Note that the asynchronous iterator should be declared in the [Symbol.asyncIterator]
attribute and processed using for-await-of
loop. The final effect is to process tasks one by one, and wait for the previous task to be processed before entering the next task.
Therefore, the asynchronous iterator is used to deal with the situation that the data cannot be obtained immediately, and it can also ensure that the final processing order is equal to the traversal order, but it needs to wait in line.
for-await-of
We can use the following code to traverse:
for await (const item of obj) { console.log(item) }
That is to say, asynchronous iterative traversal needs to use for-await-of
statement. In addition to being used on asynchronous iterable objects, it can also be used on synchronous iterable objects .
const obj = { *[Symbol.iterator]() { yield 1 yield 2 yield 3 } } for await(const item of obj) { console.log(item) // 1 -> 2 -> 3 }
Note: If both [Symbol.asyncIterator]
and [Symbol.iterator]
are deployed on an object, the asynchronous iterator generated by [Symbol.asyncIterator]
will be used first . This is easy to understand, because for-await-of
was originally born for asynchronous iterators.
On the contrary, if two iterators are deployed at the same time, but for-or
is used, then the synchronous iterator is used first.
const obj = { *[Symbol.iterator]() { yield 1 yield 2 yield 3 }, async *[Symbol.asyncIterator]() { yield 4 yield 5 yield 6 } } // 异步for await(const item of obj) { console.log(item) // 4 -> 5 -> 6。优先使用由[Symbol.asyncIterator] 生成的异步迭代器} // 同步for (const item of obj) { console.log(item) // 1 -> 2 -> 3。优先使用由[Symbol.iterator] 生成的同步迭代器}
Summarize
Iterator generator logic may be a bit convoluted, but it is very necessary to understand its principle. You can try to write it yourself, and know why. Only in this way can the required implementation define its own iterator to traverse the object, and it can also be applied in the corresponding scene of actual development.
This article is transferred from: https://www.iyouhun.com/post-256.html
This site is only for collection, and the copyright belongs to the original author.