What is asynchronous iteration? How to customize iteration? A detailed explanation of ES6 iterators and generators

Original link: https://www.iyouhun.com/post-256.html

Generator iterators for Es6

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()) 

image-20230712112350523

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() 

image-20230712113415590

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

image-20230707172254195

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.

image-20230707171931935

From the figure we can see:

  1. Iterator interface returns an object with a next method.
  2. Each call to next returns the items in the array in turn until it points to the end of the data structure.
  3. The returned result is an object, which contains the current value value and whether the current end done

traverse objects

Try to traverse the object, we will find that he reported that the object is not iterable, as shown below

image-20230707172407016

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 } 

image-20230707173001749

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.