浅谈迭代器和生成器

文章目录

  • 迭代器
    • 迭代器的规范
    • JS中的迭代器
    • 迭代器协议
    • 关于for of 遍历
  • 生成器
    • 生成器的执行
    • 生成器里的细节
    • 生成器的其他API
    • 协程
  • 应用
    • 异步控制
    • Saga
    • async/await
  • 总结


迭代器

迭代与遍历的区别在于:迭代并不保证所有数据能取出、而遍历强调的是将所有数据取出。

迭代器的规范

  1. 迭代器应该具有获取下一个数据的能力
  2. 迭代器应该具有判断是否还有后续数据的能力

JS中的迭代器

js中迭代器是通过使用 next() 方法实现 Iterator protocol 的任何一个对象,该方法返回具有两个属性的对象: value,这是序列中的 next 值;和 done ,如果已经迭代到序列中的最后一个值,则它为 true 。如果 value 和 done 一起存在,则它是迭代器的返回值。

//next方法返回的对象格式:
{
	value:'值',
	done:'是否迭代完成'
}

通过定义我们可以实现我们的自定义迭代器:

var arr = [1,2,3,4,5,6,7,8,9]

var iterator = {
	i:0,
	next(){
		return {
			value:arr[this.i++],
			done:this.i >= arr.length
		}
	}
}

iterator.next() //{value: 1, done: false}
iterator.next() //{value: 2, done: false}

//实现遍历功能,完成所有数据的取出
var data = iterator.next()
while(!data.done){
	console.log(data.value)
	data = iterator.next()
}


通过迭代器实现非波拉契数列:

function createFeiBoIterator() {
	let prev1 = 1, prev2 = 1, n = 1;
	return {
		next() {
			let value;
			if (n <= 2) {
				value = 1
			} else {
				value = prev1 + prev2
			}
			prev1 = prev2;
			prev2 = value;
			n++;
			return {
				value,
				done: false
			}
		}
	}
}

function getFeiBoItem(n) {
	let FeiBoIterator = createFeiBoIterator()
	let arr = []
	for (let i = 0; i < n; i++) {
	arr.push(FeiBoIterator.next().value)
	}
	return arr[n-1]
}

getFeiBoItem(1) //1
getFeiBoItem(2) //1
getFeiBoItem(3) //2
getFeiBoItem(4) //3
getFeiBoItem(5) //5

MDN:迭代器和生成器

迭代器协议

ES6规定,如果一个对象具有Symbol.iterator,并且属性值是一个迭代器创建函数,则该对象是可迭代的。

var obj = {
	[Symbol.iterator](){
		return {
			next(){
				value:1,
				done:false
			}
		}
	}
}

数组本质上就是一个可迭代对象,通过其原型可以看到Symbol.iterator的方法。
浅谈迭代器和生成器_第1张图片
我们可以调用数组身上的迭代器方法。
浅谈迭代器和生成器_第2张图片
除了数组,大多数类数组、伪数组本身也是可迭代对象。如获取dom返回的类数组。

String、Array、TypedArray、Map 和 Set 都是内置可迭代对象,因为它们的原型对象都拥有一个 Symbol.iterator 方法

关于for of 遍历

for of的遍历机制是遍历可迭代对象。

var iterator = [1,2,3]
for(let item of iterator){
	//iterator是一个可迭代的对象
	//item是每次迭代得到的数据
}

//上面for of循环等价于下面的while循环
var iterObj = iterator[Symbol.iterator]()
var res = iterObj.next()
while(!res.done){
	const item = res.value
	//...
	//for of循环体做的事情
	//...
	res = iterObj.next()
}

因为for of只能遍历可迭代对象,所以正常情况下一般对象是不可以使用for of循环的,不过我们可以给对象添加方法,实现对象的可迭代。

var obj = {
	a: 1,
	b: 2,
	c: 3,
	d: 4
}

for(const item of obj){
	console.log(item) //TypeError: obj is not iterable
}

//给对象添加迭代器方法,实现for of的可遍历
var obj = {
	a: 1,
	b: 2,
	c: 3,
	d: 4,
	[Symbol.iterator](){
		const keysArr = Object.values(this)
		let i=0;
		return {
			next(){
				return {
					value:keysArr[i++],
					done:i>keysArr.length
				}
			}
		}
	}
}

展开运算符可以展开可迭代对象
浅谈迭代器和生成器_第3张图片


生成器

最初生成器的产生背景在于解决迭代器的书写问题(如迭代器里书写next方法)。

生成器是由构造函数Generator(该构造函数只能系统内部调用)创建的对象。

生成器既是一个迭代器(具有next方法),又是一个可迭代对象(具有Symbol.iterator)。

function* test(){
	console.log('run')
	return 'end'
}

var gen = test() //函数内部没有运行
get.next() // {value: 'end', done: true}

生成器的执行

  1. 生成器函数内部是为了给生成器的每次迭代提供数据(直接运行生成器函数并不是迭代,所以并不会运行函数内部代码)。
  2. yield关键字:该关键字只能在生成器内部使用,每次调用生成器的next方法,将导致生成器函数运行到下一个yield关键字位置。
function* test() {
	console.log('start');
	const y = yield 1;
	console.log(y, 'between')
	yield console.log('yeild run')
	return 'end'
}

var gen = test() //函数内部没有运行
get.next() // 输出'start'; 返回{value: 1, done: false}
gen.next() // 输出undefined 'between' 'yeild run'; 返回{value: undefined, done: false}
gen.next() // 返回{value: 'end', done: true}

value为yeild或者return的数据,当函数运行结束,done为true。

使用生成器完成非波拉契数列的遍历:

function* createFeiBo() {
	let prev1 = 1, prev2 = 1, value, n = 1;

	while (true) {
	if (n <= 2) {
		value = 1
	} else {
		value = prev1 + prev2
	}
	prev1 = prev2
	prev2 = value
	n++
	yield value
	}
}

var gen = createFeiBo() 
gen.next() //{value: 1, done: false}
gen.next() //{value: 1, done: false}
gen.next() //{value: 2, done: false}
gen.next() //{value: 3, done: false}
gen.next() //{value: 5, done: false}

生成器里的细节

生成器函数可以有返回值,返回值出现在第一次done为true时的value属性中。

调用生成器的next方法时,可以传入参数,参数会交给yield表达式的返回值。(迭代器并不具备,是生成器特有)

next没有传参数的情况,yield返回值为undefined:

function* test() {
	const y = yield 1;
	console.log(y, 'between')
	return 'end'
}
var gen = test() //函数内部没有运行
get.next() // 返回{value: 1, done: false}
gen.next() // 输出undefined 'between'; 返回{value:'end', done: true}

next传入参数的情况:

function* test() {
	const y = yield 1;
	console.log(y, 'between')
	return 'end'
}

var gen = test() //函数内部没有运行
get.next() // 返回{value: 1, done: false}
gen.next('hello') // 输出'hello' 'between'; 返回{value:'end', done: true}

生成器函数里可以调用其他生成器,需要在yield后加*表示调用生成器,本质上可以看作是把其他生成器的内部代码替换当前的调用。

function* innerYield() {
	console.log("inner yield1")
	yield 'inner 1'
	console.log("inner yield2")
	yield 'inner 2'
	console.log('return')
	return 'inner end'
}

function* test() {
	yield * innerYield()
	const y = yield 1;
	console.log(y, 'between')
	return 'end'
}

//本质上生成器test等价于下面的代码
function* test() {
	console.log("inner yield1")
	yield 'inner 1'
	console.log("inner yield2")
	yield 'inner 2'
	console.log('return')
	const y = yield 1;
	console.log(y, 'between')
	return 'end'
}

var gen = test() 

gen.next() //输出'inner yield1' 返回{value: 'inner 1', done: false}

gen.next() //输出'inner yield2' 返回{value: 'inner 2', done: false}

gen.next() //输出'return' 返回{value: 1, done: false}

gen.next() //输出undefined 'between' 返回{value: 'end', done: true}

``

生成器的其他API

  1. return:调用该方法可以提前结束生成器。return 传递的参数就是value值,不传递参数value值就是undefined。
  2. throw:调用该方法,可以在生成器里产生一个错误。

协程

生成器的暂停和恢复,主要是通过协程来实现。

协程是一种比线程更加轻量的存在,一个线程可存在多个协程,但是在线程上同时只能执行一个协程。

一个进程可以拥有多个线程,一个线程可以拥有多个协程。

如果从A协程启动B协程,A协程可以称为B协程的父协程。

function* test() {
	console.log("inner yield1")
	yield 'inner 1'
	console.log("inner yield2")
	yield 'inner 2'
	console.log('return')
	const y = yield 1;
	console.log(y, 'between')
	return 'end'
}

var gen = test() 
gen.next() 
console.log('out1')
gen.next() 
console.log('out2')
gen.next() 
gen.next(3) 
父协程 gen协程 通过var gen = test()创建gen协程 gen.next() 1 par [通过gen.next()恢复g- en协程] console.log("inner yield1") yield 'inner 1' 2 par [gen协程通过yield交出- 主线程控制权] console.log('out1') gen.next() 3 console.log("inner yield2") yield 'inner 2' 4 console.log('out2') gen.next() 5 console.log('return') yield 1 6 gen.next(3) 7 const y = 3 console.log(y, 'between') return 'end' 8 par [通过return关闭当前协- 程] 父协程 gen协程
  1. gen协程和父协程是在主程序上交互执行的,并不是并发执行,它们之间的切换是通过yield和gen.next()来配合。
  2. 在切换协程的时候,js引擎都会保存当前协程的调用栈信息,当切换回来时则恢复之前的调用栈信息。

应用

异步控制

通过封装一个run方法,参数为一个生成器,实现通过yield完成await的效果。

function run(task) {
	const gen = task()
	let res = gen.next()
	handle()

	// 递归处理
	function handle() {
		if (res.done) {
			return
		}
	
		//Promise和普通数据
		if (res.value[Symbol.toStringTag] === 'Promise') {
			res.value.then(data => {
				res = gen.next(data)
				handle()
			}).catch(error => {
				gen.throw(error)
			})
		} else {
			res = gen.next(res.value)
			handle()
		}
	}
}


function* task() {
	console.log("start")
	const yield1 = yield 1
	console.log(yield1)
	const yield2 = yield 2
	console.log(yield2)
	const res = yield fetch('http://localhost:8080')//此处简单请求跨域,后端自行配置Access-Control-Allow-Origin:http://127.0.0.1:5500
	console.log(res)
	const yield3 = yield 3
	console.log(yield3)
}

run(task)

代码运行结果,实现了await的效果:
浅谈迭代器和生成器_第4张图片

Saga

作为redux异步操作的一个方案,saga中间件实现对异步的控制。

saga通过一系列指令实现对任务的监听,指令的前面需要yield修饰。

具体用法如下:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import reducer from './reducers'
import mySaga from './sagas'

// 创建saga中间件
const sagaMiddleware = createSagaMiddleware()

const store = createStore(
	reducer,
	applyMiddleware(sagaMiddleware)
)

// 通过调用run方法,参数传入生成器,启动saga任务。
sagaMiddleware.run(mySaga)

redux-saga官网

saga需要在yield后面使用一些合适的saga指令,如果放的是普通数据,saga不作任何处理。

每个指令本质是一个函数,该函数调用后返回一个指令对象。

take指令

take指令监听某个action,当action发生了变化,就会进行下一步处理,仅监听一次(take会阻塞后续执行)。

import { take } from 'redux-saga/effects'
import { actionTypes } from './action/xxx'
export default function*(){
	const action = yield take(actionTypes.xxx)
	console.log('触发了action',action) //当dispatch该action时触发,此处代码将运行
}

上面的代码当触发监听的action后会进入下一个yield的执行,而上面的代码只触发一次,为了实现持续监听,我们可以将代码用while包装。

import { take } from 'redux-saga/effects'
import { actionTypes } from './action/xxx'
export default function*(){
	while(true){
		const action = yield take(actionTypes.xxx)
		console.log('触发了action',action) //当dispatch该action时触发,此处代码将运行
	}
}

all指令

该函数传入一个数组,数组中放入生成器,saga会等待所有的生成器全部完成后才会进一步处理(all会阻塞后续执行)。

import { all } from 'redux-saga/effects'
import task1 from './task1'
import task2 from './task2'

export default function*(){
	yield all([task1(),task2()])
	console.log('saga都完成')
}

takeEvery指令

takeEvery指令的作用时不断监听某个action,当触发某个action后运行某个函数,takeEvery永远不会结束当前的生成器。(takeEvery不会阻塞后续执行,能够实现对多个action的监听)


// main.js
import { all } from 'redux-saga/effects'
import task1 from './task1'
import task2 from './task2'
export default function*(){
	yield all([task1(),task2()])
}


// task1.js
// 通过takeEvery实现对多个action的监听
import { takeEvery } from 'redux-saga/effects'

export default function*(){
	yield takeEvery('actionType1',genFun1)
	yield takeEvery('actionType2',genFun2)
}

async/await

ES7引入async/await语法糖,灵感来自于通过生成器实现异步操作,而实际正是使用生成器去实现,更近一步讲,底层正是使用协程机制。

async/await提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力。

async function run() {
	try {
		console.log('start')
		const res = await fetch('http://localhost:8080/')
		console.log(res)
	} catch (err) {
		console.log(error)
	}
}
run()

async/await异步处理的逻辑可以使用同步代码的方式去实现,并且支持try catch捕获错误

async返回一个Promise,当async函数里有return时,return的值就是resolved的值。

await会默认帮我们调用resolve,代码如下:

//await 1等价于下面代码
let promise = new Promise((resolve,reject){
	resolve(1)
})

//await fetch('https://www.baidu.com')
let promise = new Promise((resolve,reject){
	fetch('https://www.baidu.com').then(res=>resolve(res))
})

async/await执行机制示意图:

async function run(){
	console.log('start')
	const res = await 1
	console.log(res)
	return 'end'
}

console.log('out start')
run()
console.log('out end1')
console.log('out end2')
父协程 run协程 console.log('out start') run()//启动run协程 1 console.log('start') await 1 //此处相当于const newPromise = new Promise((resolve,reject)=>resolve(1)) 暂停run协程,并将newPromise返回给父协程 2 newPromise.then //父协程需要调用newPromise.then来监控promise状态的改变。 console.log('out end1') console.log('out end2') 父协程执行结束,进入微任务检查点,发现有微任务,触发newPromise.then的回调函数。 执行微任务,resolve(1) 3 const res = 1 console.log(res) return 'end' 4 父协程 run协程

总结

async/await
实现原理
处理细节
生成器
协程
生成器执行机制
迭代器
迭代器协议
for...of循环

你可能感兴趣的:(js,javascript)