JavaScriptで主流の非同期タスク処理をマスター
JavaScripがシングルスレッドで動作するのは、DOM操作を実行する必要があるからであり、複数のスレッドが同時にDOMを変更すると、ブラウザはどのスレッドが優勢であるかを見分けることができません。
JavaScirptは同期モードと非同期モードに分かれています。
同期モードと非同期モード
同期パターン
同期モードは、実際には:キューイング実行は、次のGIFアニメーションによると、同期モードを実証するために、理解することは非常に簡単で、jsは、ワークシートが実行されて維持するときに、ワークシートのタスクが終了した後にクリアされます。
問題点:1つのタスクの実行に時間がかかりすぎると、後続のタスクがブロックされ、インターフェースがスタックしてしまいます。
非同期パターン
イベントループとメッセージキューメカニズムを使用して実装された、ダイアグラムによる非同期タスクのデモンストレーション。
Promise非同期プログラミング
一般的な非同期ソリューションは、コールバック関数を通して実現することですが、その結果、コールバック地獄という問題が発生します。CommonJSコミュニティは、Promiseスキームを提案し、ES6で採用しました。次のコードは、コールバックを介して実装された場合、複数回ネストされるラップアラウンド・アニメーションを実装しています。
let box = document.querySelector('#box');
move(box, 'left', 300, () => {
move(box, 'top', 300, () => {
move(box, 'left', 0, () => {
move(box, 'top', 0, () => {
console.log('動作完了');
});
});
});
});
プロミスのユースケースのデモコードは以下の通りです:
//応用例
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
}
xhr.send();
});
}
let promise2 = ajax('./api/user.json');
let newPromise = promise2.then((res) => {
console.log(res);
});
console.log(promise2 === newPromise);//false それぞれ、新しい Promiseオブジェクトを返す。
//then 非同期タスクは可能な限りフラットに保とう。
//thenメソッドで Promiseオブジェクトを返すこともできる。
ajax('./api/user.json').then(res=>{
console.log(111);
return ajax('./api/user.json');
}).then(res=>{
console.log(222);
return 'foo';
}).then(res=>{
console.log(res);
})
//OUT:
false
Array(2)
111
222
foo
プロミスの連鎖に関するいくつかの注意点
- Promiseオブジェクトのthenメソッドは、新しいPromiseオブジェクトを返します。
- 後者の then メソッドは、前の then が返す Promise のコールバックを登録しています。
- 最初のthenメソッドのコールバック関数の戻り値は、2番目のthenメソッドのコールバックの引数として使用されます。
- コールバックでPromiseが返された場合、thenメソッドの後続のコールバックはPromiseの終了を待ちます。
プロミスの例外処理
プロミス実行処理エラーonRejectedコールバックは、一般的に失敗したコールバックを登録するcatchメソッドを介して実行され、コールバックの結果を登録するthenメソッドの第2パラメータは同じです。
const promise = new Promise(function (resolve, reject) {
//2つのうち1つしか呼び出すことができず、一度設定した状態を変更することはできない。
// resolve(100);//成功
reject(new Error('promise rejected'));//
});
promise.then(function (value) {
console.log('resolved', value);
}, function (err) {
console.log('rejected', err);
}).catch(err=>{
console.log("catch",err);
});
console.log('end');
エラーのコールバックとしてcatchメソッドを使用することを推奨します。また、以下の理由により、エラーのコールバックとしてthenメソッドの第2パラメータを使用することは推奨されません:
正しいコールバックを受け取ってPromiseオブジェクトを返したが、実行中にエラーが発生した場合、エラーコールバックを受け取ることはできません。
ajax('./api/user.json').then(res=>{
console.log('onresolved',res);
return ajax('/error.json');
},err=>{
console.log("onRejected",err);
});
もう一度キャッチメソッドを見てください:
ajax('./api/user.json').then(res=>{
console.log('onresolved',res);
return ajax('/error.json');
}).catch(err=>{
console.log("onRejected",err);
});
catchメソッドはthenメソッドによって返された新しいPromiseオブジェクトの実行エラーをキャッチします。
onresolved (2) [{ }, { }]
onRejected Error: Not Found
at XMLHttpRequest.xhr.onload (promise.js:28)
これに加えて、グローバル・オブジェクトは unhandlerdrejection イベントを登録し、コード内で手動で処理されない例外を処理します。以下はノード
process.on('unhandledRejection',(reason,promise)=>{
//reason => Promise 失敗の理由(通常はエラーオブジェクト
//promise => 例外を含む Promiseオブジェクト。
})
グローバル・ハンドラにスローされるのではなく、可能な例外はすべてコード内で明示的にキャッチされるべきです。
プロミスの静的メソッド
//成功状態を持つ Promiseオブジェクト。
Promise.resolve('foo').then(res=>{
console.log(res);
});
var promise = ajax('./api/user.json');
var promise2 = Promise.resolve(promise);//Prmoseオブジェクトを渡すと、同じ Promiseオブジェクトが返される。
console.log(promise === promise2);//true
//thenメソッドを持つ入力オブジェクトは、以下のように渡されたオブジェクトと同じように実行できる。
Promise.resolve({
then:function(onFulfilled,onRejected){
onFulfilled('f00');
}
}).then(res=>{
console.log(res);//f00
});
//失敗状態のPromiseオブジェクトを作成する。
Promise.reject(new Error('rejected')).catch(err=>{
console.log(err);
})
Promise の並列実行: すべてのレースは複数の Promise オブジェクトを結合します。
var promise = Promise.all([ajax('./api/user.json'),ajax('./api/user.json')]);
promise.then(res=>{
console.log(res);
})
//Promiseのソースコードは手書きで、両方が成功し、片方が失敗した場合のみ成功し、失敗ステータスのコールバックを返すようになっている。
ajax('./api/user.json')
.then(res=>{
const urls = Object.values(res);
console.log('??',urls);
const tasks = urls.map(url=>{
console.log(url);
return ajax(url);
});
console.log(tasks);
return Promise.all(tasks);
}).then(res=>{
console.log(res);
});
//race は最初のタスクが終了するまで待つ
const request = ajax('./api/user.json');
const timeout = new Promise((resolve,reject)=>{
setTimeout(() => {
reject(new Error('timeout'));
}, 500);
});
Promise.race([request,timeout]).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err);
});
低速のネットワークを模倣すると、raceがREJECTを実行していることがわかります。
プロミスの実行タイミング:マクロタスクとマイクロタスク
プロミスコールバックはマイクロタスクとして実行されます。マイクロタスク:全体的な応答性を向上させます。現在、ほとんどの非同期コールバックはマクロタスクとして動作します。
一般的なマクロタスクとミクロタスクを以下に示します:
以下は、非同期タスクを実行するJavaScriptの実行タイミングの図です:
次の例を見て理解してください。 2 4 1 3 5
これも上図のイベントループの原則に沿ったもので、まずメインタスクが実行され、出力は2 4となり、次にマイクロタスクがあるかどうかを照会、新たに実行されるマクロタスクはありません。
次に、マクロ・タスクが出力を実行します。
その後、マイクロタスクがあるかどうかをクエリ その後、新しいマクロタスクがあるかどうかをクエリ 実行されるマクロタスクがない場合
実行出力:5
let time = 0;
setTimeout(()=>{
time = 1;
console.log(time);
//ネストされたマクロ・タスク
setTimeout(()=>{
time = 5;
console.log(time);
},1000);
},1000);
time = 2;
console.log(time);
setTimeout(()=>{
time=3;
console.log(time);
},1000);
time = 4;
console.log(time);
以下はマイクロタスクを使った例です。マイクロタスクはメインタスクの終了後に実行されます。
let time = 0;
setTimeout(()=>{
time = 1;
console.log(time);
//ネストされたマクロ・タスク
setTimeout(()=>{
time = 5;
console.log(time);
},1000);
},1000);
time = 2;
console.log(time);
setTimeout(()=>{
time=3;
console.log(time);
},1000);
time = 4;
console.log(time);
//
let observer = new MutationObserver(()=>{
time = 6;
console.log(6);
});
observer.observe(document.body,{
attributes:true
});
document.body.setAttribute('kkb',Math.random());
Generator非同期プログラミング
まず、イテレータの
イテレータ
for....in : オブジェクトの列挙可能なプロパティを元の挿入順に反復処理します。.of : オブジェクトのイテレータ固有の実装に従って、オブジェクトのデータをイテレートする イテレート可能なオブジェクト - [Symbol.iterator]メソッドの実装 配列構造には[Symbol.iterator]メソッドがありますが、オブジェクトをイテレートするには、以下のコードの[Symbol.iterator]メソッドの実装を追加する必要があります。
<!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>
//for of/ for in
//for...in : オブジェクトの列挙可能なプロパティを、元々挿入されていた順番で反復処理する。
//for...of : イテレート可能なオブジェクトのイテレータに従って、オブジェクト・データをイテレートする。[Symbol.iterator]
let arr = ['a','b','c','d'];
let obj = {
a:1,
b:2,
c:3
}
for(let attr in arr){
console.log(attr);//0 1 2 3
}
for(let val of arr){
console.log(val);//a b c d
}
console.dir(arr);//symbol(Symbol.iterator): ƒ values()
console.dir(obj);//シンボルなし.iterator
//オブジェクトをofに使いたい場合は、Custom Iterators属性を追加する必要がある。
obj[Symbol.iterator] = function(){
//反復プロトコル
//オブジェクトの値を配列に変換する
let values = Object.values(obj);
//オブジェクトのキーを配列に変換する
let keys = Object.keys(obj);
// let values = [...obj];
console.log(values);
let index = 0;
//オブジェクトを返し、nextメソッドを持たなければならない。
return {
next(){
//done:ループが完了したかどうかを示す。
//value:for a of obj aは値である。
//オブジェクトを返さなければならない
if (index >= values.length) {
return{
done:true
}
}else{
return{
done:false,
value:{
key:keys[index],
value:obj[keys[index++]]
}
}
}
}
}
}
//直接反復メソッドをテストできる
let objIterator = obj[Symbol.iterator]();
//次のメソッドを実行する
console.log(objIterator.next());
//実際には...of 常にobjIteratorを呼び出す.next() 完了するまで:true停止する
for(let o of obj){
//obj is not iterable
console.log(o);
}
</script>
</html>
ジェネレータ関数は、通常の関数よりも*記号、yield文の使用内の関数は、トラバーサの各メンバーを定義する、つまり、別の内部状態を持っています。 繰り返し関数の実装。 ジェネレータ関数は、一般的にほとんど理解するために使用されません。
<!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>
/**
* 反復可能な関数を定義する。 yield 各反復の戻り値。
*/
function* fn() {
yield new Promise((resolve, reject) => {
setTimeout(() => {
console.log("a");
resolve("1");
}, 500);
});
yield new Promise((resolve, reject) => {
setTimeout(() => {
console.log("b");
resolve("2");
}, 500);
});
yield new Promise((resolve, reject) => {
setTimeout(() => {
console.log("c");
resolve("3");
}, 500);
});
}
let f = fn();
// console.log(f.next());
// for (const iterator of f) {
// console.log(iterator);
// }
function co(fn) {
let f = fn();
next();
function next(data){
let result = f.next();
console.log(result);
if(!result.done){
//前の非同期実行が完了した。
result.value.then((info)=>{
console.log(info,data);
next(info);
});
}
}
}
co(fn);
// for (let fn of f) { 一緒に実行する 非同期に呼び出すことはできない。
// }
</script>
</html>
ジェネレータ機能の使用
//Generator ジェネレータ関数
function* foo() {
try {
console.log('start');
const res = yield 'foo';
console.log(res);
} catch (e) {
console.log(e);
}
}
const generator = foo();
const result = generator.next();
console.log(result);
generator.next('bar');
generator.throw(new Error('Generator Error'));//例外を投げる
次のコードは、Generatorを非同期で使用する例です:
function* main() {
try{
const users = yield ajax('./api/user.json');
console.log(users);
const posts = yield ajax('./api/user.json');
console.log(posts);
}catch(e){
console.log(e);
}
}
//汎用非同期ジェネレータ・メソッド
function co(generator){
const g = generator();
function handleResult(result){
if(result.done) return;
result.value.then(data=>{
handleResult(g.next(data));
}).catch(err=>{
g.throw(err);
})
}
handleResult(g.next());
}
co(main);
非同期/待機構文シュガー
非同期プログラミングの推奨規格。awaitの後には必ずPromiseオブジェクトが続くこと、そしてawaitは非同期関数の中にしか存在しないことに注意してください。
async function main2() {
try{
const users = await ajax('./api/user.json');
console.log(users);
const posts = await ajax('./api/user.json');
console.log(posts);
}catch(e){
console.log(e);
}
}
main2();
Promise ソースコード手書き実装
1. Promise Promiseは、実行されると即座に実行されるエクゼキュータを渡される必要があるクラスだ。
2. Promise Promiseには3つの状態がある。> fulfilled pending->rejected
3. resolve reject 状態を変更する関数
resolve:fulfilled
reject:rejected
4. thenこのメソッドが内部で行っていることは、状態を判定し、状態が成功した場合に成功コールバック関数を呼び出すことである。
状態が失敗すると、失敗したコールバック関数がコールバックされる。
5. then成功と失敗には、それぞれ成功の値と失敗の理由を示すパラメーターがある。
6. 成功と失敗の値を記録する
7. エクゼキュータ内での非同期条件の処理 resolveまたはrejectを呼び出す。
8. メソッドが複数回呼び出された場合の処理
9. thenメソッドは連鎖的に呼び出すことができ、後続のthenメソッドのコールバック関数は、前のthenメソッドの値を取得する。
コールバック関数の戻り値
10. then 戻り値が通常の値か、Promiseオブジェクトか。
11. then 同じ Promiseオブジェクトを返す。
12. executorの内部でエラーが発生し、rejectにコールバックされる。
13. thenパラメータなしの呼び出しの連鎖
14. all静的メソッド
const PENDING = 'pending';//
const FULFILLED = 'fulfilled';//成功
const REJECTED = 'rejected';//
class MyPromise {
constructor(executor) {
try {
executor(this.resolve, this.reject);//実行者はすぐに実行する。
} catch (e) {
this.reject(e);
}
}
status = PENDING;//状態を定義する
//成功後の値
value = undefined;
//失敗後の値
error = undefined;
//成功したコールバック
onFulfilled = [];
//失敗コールバック
onRejected = [];
//矢印関数 thisは変更されない。thisも私の Promiseオブジェクトを指す。
resolve = (value) => {
//0 保留状態かどうかを判定する。
if (this.status !== PENDING) {
return;
}
//1 状態の変化
this.status = FULFILLED;
//2 成功後の値を保存する
this.value = value;
//3 成功コールバックが存在するかどうか
// this.onFulfilled && this.onFulfilled(this.value);
while (this.onFulfilled.length) {
this.onFulfilled.shift()();
}
}
reject = (error) => {
//0 保留状態かどうかを判定する。
if (this.status !== PENDING) {
return;
}
//1 状態の変化
this.status = REJECTED;
//2 失敗後の値を保存する
this.error = error;
//3 失敗コールバックが存在するかどうか
// this.onRejected && this.onRejected(this.error);
while (this.onRejected.length) {
this.onRejected.shift()();
}
}
then(onFulfilled, onRejected) {
onFulfilled = onFulfilled ? onFulfilled : value => value;
onRejected = onRejected ? onRejected : error => { throw error };
//1. 連鎖呼び出しの実装
let p = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
//コールバック関数の戻り値を取得する
let result = onFulfilled(this.value);
//次の Promiseオブジェクトに渡される。
//resultが通常の値か Promiseオブジェクトかを判断する。
//普通の値ならresolveを呼べばいい。
//Promiseオブジェクトの場合は、Promiseオブジェクトが返す結果を見る。
//Promise」オブジェクトの結果によって、resolveを呼び出すかrejectを呼び出すかが決まる。
// resolve(result);
//実行前にpを取得するには、同期コードの実行終了を待つ必要がある。
this.resolvePromise(p, result, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
} else if (this.status == REJECTED) {
setTimeout(() => {
try {
//コールバック関数の戻り値を取得する
let result = onRejected(this.error);
this.resolvePromise(p, result, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
} else {
//非同期コードはすぐには実行されないので、コールバックは保存され、非同期コードが実行された後に実行される。
this.onFulfilled.push(() => {
setTimeout(() => {
try {
//コールバック関数の戻り値を取得する
let result = onFulfilled(this.value);
this.resolvePromise(p, result, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejected.push(() => {
setTimeout(() => {
try {
//コールバック関数の戻り値を取得する
let result = onRejected(this.error);
this.resolvePromise(p, result, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return p;
}
resolvePromise(p, result, resolve, reject) {
if (p === result) {
return reject(new TypeError('TypeError: Chaining cycle detected for'));
}
if (result instanceof MyPromise) {
//Promiseobject 新しい Promiseオブジェクトの値を渡す。
result.then(resolve, reject);
} else {
//
resolve(result);
}
}
static all(array) {
let result = [];
let index = 0;
return new MyPromise((resolve, reject) => {
function addData(key, value) {
result[key] = value;
index++;
if (index === array.length) {
//resolveを呼び出す前に、非同期処理の完了を待つ必要がある。
resolve(result);
}
}
for (let i = 0; i < array.length; i++) {
let cur = array[i];
if (cur instanceof MyPromise) {
cur.then((value) => {
addData(i, value);
}, (err) => {
reject(err);
});
} else {
addData(i, cur);
}
}
});
}
static resolve(value) {
if (value instanceof MyPromise) {
return value;
}
return new MyPromise((resolve, reject) => {
resolve(value);
});
}
static reject(error){
return new MyPromise((resolve,reject)=>{
reject(error);
})
}
finally(callback){
return this.then(res=>{
return MyPromise.resolve(callback()).then(()=>res);
},err=>{
return MyPromise.resolve(callback()).then(()=>{throw err});
});
}
catch(error){
return this.then(undefined,error);
}
}
//
module.exports = MyPromise;





