為什麼使用Generator?
在JavaScript使用異步操作時,在async和await還沒有被JavaScript官方正式推出時,那麼異步操作解決方案就只有回調函數和Promise。
回調函數
所謂回調函數,就是把需要執行的動作以函數的方式包裝起來,再將這個函數以參數的方式傳遞給其他的函數,當時機到來時再進行調用。
// 需在瀏覽器中運行
function loadImage(imgUrl, callback) {
const img = document.createElement("img");
img.onload = function () {
callback(this);
};
img.src = imgUrl;
}
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
}
);
}
);
}
);
Promise
promise是為了解決回調函數產生的回調地獄問題而產生的。
function loadImage(imgUrl) {
return new Promise((resolve) => {
const img = document.createElement("img");
img.onload = function () {
resolve(this);
};
img.src = imgUrl;
});
}
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg"
)
.then((img) => {
document.body.appendChild(img);
return loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg"
);
})
.then((img) => {
document.body.appendChild(img);
return loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg"
);
})
.then((img) => {
document.body.appendChild(img);
});
雖然解決了回調地獄的問題,異步任務執行步驟也更加清晰了,但是相比於現在async和await的異步操作同步化的表達方式還是略遜一籌,在async和await還沒有推出時,社區就已經利用生成器函數實現了社區版的async和await,當時生成器的出現就是為了服務於異步編程。
使用方式
創建方式
function* getValue() {
yield 1;
yield 2;
return 3;
}
const generator = getValue() // 獲取到生成器
console.log(Object.prototype.toString.call(generator)); // [object Generator]
next方法
獲取返回值
生成器函數和普通函數的調用方式完全不同,上面代碼雖然調用生成器函數產生了生成器,但是生成器函數內部的代碼併為被執行。
那麼需要怎樣操作才能讓生成器內部的代碼執行?
生成器有一個next方法,當這個方法被調用時,會把yield後面的值返回回來,並且生成器函數內部的代碼停止執行,當再次調用next方法後,生成器函數內部代碼會從上次暫停處開始執行,到下一個yield語句處停止。
next()方法返回的對象包含兩個屬性:
- value:返回的值,也就是yield關鍵字後面的值。
-
done:表示生成器函數是否已經完成,true表示已經完成,false表示未完成。
function* getValue() { yield 1; yield 2; yield 3; } const generator = getValue(); console.log(generator.next()); // { value: 1, done: false } console.log(generator.next()); // { value: 2, done: false } console.log(generator.next()); // { value: 3, done: false } console.log(generator.next()); // { value: undefined, done: true }當生成器內最後的yield語句也執行過後,意味着整個生成器函數執行完畢,調用next()方法產出的結果中done的值為true了。
簡單用圖片演示下運行流程:
第一次調用next()方法,生成器函數會暫停在第一個yield語句
再次調用next()方法,生成器函數會暫停在第二個yield語句
再次調用next()方法,生成器函數會暫停在第三個yield語句
再次調用next()方法,生成器函數執行完畢
這裏返回的value為什麼是undefined,其實可以這麼理解,因為在JavaScript中,函數不主動return,那麼會默認返回undefined。
function* getValue() {
yield 1;
yield 2;
yield 3;
return undefined;
}
如果生成器函數中有return語句,那麼在執行return語句的時候就會把生成器的狀態置為已完成,後面的yield語句將不再被執行,這點和普通函數是保持一致的。
function* getValue() {
yield 1;
yield 2;
return 3;
}
const generator = getValue();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: true }
傳遞參數
next函數其實也可以傳遞參數,這個參數將被yield語句消費。
function* getValue() {
const a = yield 1;
const b = yield 2 * a;
return 3 * b;
}
const generator = getValue();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next(5)); // { value: 10, done: false }
console.log(generator.next(6)); // { value: 18, done: true }
還是使用圖片來演示整個流程。
- 首先調用next(),語句在第一個yield語句處暫停
- 然後調用next(5),參數5會傳遞給yield語句等號左邊的變量a,語句會停在第二個yield語句處
- 然後調用next(6),參數6會傳遞給yield語句等號左邊的變量b,碰到return語句直接返回,整個生成器函數執行完畢。
throw函數
next函數可以往生成器裏面傳遞函數,而throw方法可以往生成器函數裏面拋出異常,如果沒有在生成器函數裏面捕獲異常,那麼生成器函數會向其它函數一樣拋出異常。
function* getValue() {
yield 1;
try {
yield 2;
} catch (error) {
console.error(error); // 捕獲異常, 打印 trhow error
}
yield 3;
}
const generator = getValue();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next(5)); // { value: 2, done: false }
console.log(generator.throw("throw error")); // {value: 3, done: false}
return函數
reutrn函數在被調用後,生成器函數會直接返回return函數傳遞的值,而且生成器函數整個函數執行完畢。
function* getValue() {
yield 1;
yield 2;
yield 3;
}
const generator = getValue();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.return(5)); // { value: 5, done: true } // 此時生成器函數執行完畢
console.log(generator.next()); // {value: undefined, done: true}
使用場景
實現Async和Await
function getValue(n, ms = 1000) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(n);
}, ms);
});
}
function* generator() {
console.log(1);
let x = yield getValue(2);
console.log(x);
x = yield getValue(3, 2000);
console.log(x);
x = yield 4;
console.log(x);
}
function asyncGenerator(generator) {
return new Promise((resolve, reject) => {
let iterable = generator();
let generated = iterable.next();
tick();
function tick() {
if (generated.done === false) {
Promise.resolve(generated.value).then(
(value) => {
try {
generated = iterable.next(value);
tick();
} catch (err) {
reject(err);
}
},
(reason) => {
try {
generated = iterable.throw(reason);
tick();
} catch (err) {
reject(err);
}
}
);
} else {
resolve(generated.value);
}
}
});
}
asyncGenerator(generator);
借用上面實現asyncGenerator函數,我們可以再來優化下加載圖片的代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
const imgUrlList = [
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg",
"https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg",
];
function loadImage(imgUrl) {
return new Promise((resolve) => {
const img = document.createElement("img");
img.onload = function () {
resolve(this);
};
img.src = imgUrl;
});
}
function* loadImageList() {
const img1 = yield loadImage(imgUrlList[0]);
document.body.appendChild(img1);
const img2 = yield loadImage(imgUrlList[1]);
document.body.appendChild(img2);
const img3 = yield loadImage(imgUrlList[2]);
document.body.appendChild(img3);
}
function asyncGenerator(generator) {
return new Promise((resolve, reject) => {
let iterable = generator();
let generated = iterable.next();
tick();
function tick() {
if (generated.done === false) {
Promise.resolve(generated.value).then(
(value) => {
try {
generated = iterable.next(value);
tick();
} catch (err) {
reject(err);
}
},
(reason) => {
try {
generated = iterable.throw(reason);
tick();
} catch (err) {
reject(err);
}
}
);
} else {
resolve(generated.value);
}
}
});
}
asyncGenerator(loadImageList).then(() => {
console.log("load success");
});
</script>
</html>
實現自定義迭代器
const list = {
head: {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: null,
},
},
},
*[Symbol.iterator]() {
let curNode = list.head;
while (curNode) {
yield curNode.value;
curNode = curNode.next;
}
},
};
for (let val of list) {
console.log(val);
}
上面針對鏈表數據結構使用for...of的進行遍歷,這得益於在list內部聲明瞭迭代器,有兩點是for...of所需要的
- 返回包含next方法的對象
- 調用返回的next方法返回的對象包含value和done這兩個屬性
而這兩點生成器函數全部滿足,無疑是天作之合。