ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript 非同期進化の歴史

JavaScript 非同期進化の歴史

黄舟
黄舟オリジナル
2017-02-22 13:52:491077ブラウズ


はじめに

JS の最も基本的な非同期呼び出しメソッドはコールバックです。非同期の完了後、ブラウザまたはノードはコールバック関数のコールバックを非同期 API に渡します。単純な非同期操作の場合は、コールバックを使用するだけで十分です。しかし、インタラクティブなページとノードの出現により、コールバック ソリューションの欠点が明らかになり始めました。 Promise 仕様が誕生し、ES6 仕様に組み込まれました。その後、ES7 では Promise に基づいて非同期機能が標準に組み込まれました。これは JavaScript の非同期進化の歴史です。

同期と非同期

通常、コードは上から下に実行されます。複数のタスクがある場合は、次のタスクが実行される前に前のタスクが完了する必要があります。この実行モードは同期と呼ばれます。初心者は、コンピュータ言語での同期と日常言語での同期を簡単に混同してしまいます。たとえば、「ファイルをクラウドに同期する」における同期は、「一貫性を保つ」ことを指します。コンピュータにおいて、同期とは、タスクが上から下に順番に実行されるモードを指します。例:

A();
B();
C();

このコードでは、A、B、C は 3 つの異なる関数であり、各関数は無関係のタスクです。同期モードでは、コンピューターはタスク A を実行し、次にタスク B、最後にタスク C を実行します。ほとんどの場合、同期モードで問題ありません。ただし、タスク B が長時間実行されるネットワーク リクエストで、タスク C がたまたま新しいページを表示している場合、Web ページがフリーズします。

より良い解決策は、タスク B を 2 つの部分に分割することです。 1 つの部分はネットワークから要求されたタスクを即座に実行し、もう 1 つの部分は要求が返された後にタスクを実行します。一方の部分がすぐに実行され、もう一方の部分が後で実行されるこのパターンは、非同期と呼ばれます。

A();
// 在现在发送请求 
ajax('url1',function B() {
  // 在未来某个时刻执行
})
C();
// 执行顺序 A => C => B

実際、JS エンジンはネットワーク リクエストを直接処理するのではなく、ブラウザのネットワーク リクエスト インターフェイスを呼び出すだけで、ブラウザはネットワーク リクエストを送信し、返されたデータを監視します。 JavaScript の非同期機能の本質は、ブラウザまたはノードのマルチスレッド機能です。

callback

将来実行される関数は通常コールバックと呼ばれます。コールバックの非同期モードを使用すると、ブロッキングの問題は解決されますが、他の問題も発生します。当初、関数は上から下に記述され、上から下に実行されていましたが、この「線形」モードは私たちの思考習慣と非常に一致していますが、現在はコールバックによって中断されています。上記のコードでは、タスク B をスキップして、タスク C を最初に実行します。この種の非同期「非線形」コードは、同期「線形」コードよりも読みにくく、したがってバグが発生する可能性が高くなります。

次のコードの実行順序を判断してみてください。「非線形」コードは「線形」コードよりも読みにくいということがより深く理解できるようになります。

A();

ajax('url1', function(){
    B();

    ajax('url2', function(){
        C();
    }
    D();
    
});
E();
// A => E => B => D => C

このコードでは、上から下への実行順序がコールバックによって乱れています。コードを読むときの順序は A => B => D => C です。これは線形コードの悪い点ではありません。

ajaxの後に実行されるタスクを進めることで、コードの実行順序が理解しやすくなります。コードは入れ子になっているため見苦しくなりますが、実行順序は上から下へ「線形」になります。この手法は、複数のネストされたコードを作成する場合に非常に役立ちます。

A();
E();

ajax('url1', function(){
    B();
    D();

    ajax('url2', function(){
        C();
    }
    
});
// A => E => B => D => C

前のコードは成功コールバックのみを処理し、例外コールバックは処理しません。次に、例外処理コールバックを追加し、コードの「線形」実行の問題について説明します。

A();

ajax('url1', function(){
    B();

    ajax('url2', function(){
        C();
    },function(){
        D();
    });
    
},function(){
    E();
    
});

例外処理コールバックを追加すると、url1の成功コールバック関数Bと例外コールバック関数Eが分離されます。この「非線形」状況が再び現れます。

ノードでは、異常なコールバックによって引き起こされる「非線形」問題を解決するために、エラー優先戦略が策定されました。ノードのコールバックの最初のパラメータは、例外が発生するかどうかを決定するために特に使用されます

A();

get('url1', function(error){
    if(error){
        E();
    }else {
        B();

        get('url2', function(error){
            if(error){
                D();
            }else{
                C();
            }
        });
    }
});

この時点で、コールバックによって引き起こされる「非線形」問題は基本的に解決されました。残念ながら、コールバックのネスト、if else の層、およびコールバック関数を使用すると、ネストされた層の数が増えると、読み取りがあまり便利ではなくなります。さらに、コールバックで例外が発生すると、その例外は現在のコールバック関数内でのみ処理できます。

promise

JavaScript の非同期進化の歴史の中で、コールバックの欠点を解決するために一連のライブラリが登場し、Promise が最終的な勝者となり、ES6 への導入に成功しました。これにより、より優れた「線形」の記述方法が提供され、非同期例外が現在のコールバックでのみキャッチできるという問題が解決されます。

Promise は、信頼できる非同期結果を返すことを約束する仲介者のようなものです。まず、Promise は非同期インターフェイスとの合意に署名します。成功すると、resolve 関数が呼び出され、例外が発生すると、reject が呼び出され、Promise に通知されます。一方、Promise とコールバックも契約を締結しており、Promise は then に登録されたコールバックと将来の catch に信頼できる値を返します。

// 创建一个 Promise 实例(异步接口和 Promise 签订协议)
var promise = new Promise(function (resolve,reject) {
  ajax('url',resolve,reject);
});

// 调用实例的 then catch 方法 (成功回调、异常回调与 Promise 签订协议)
promise.then(function(value) {
  // success
}).catch(function (error) {
  // error
})

Promise 是个非常不错的中介,它只返回可信的信息给 callback。它对第三方异步库的结果进行了一些加工,保证了 callback 一定会被异步调用,且只会被调用一次。

var promise1 = new Promise(function (resolve) {
  // 可能由于某些原因导致同步调用
  resolve('B');
});
// promise依旧会异步执行
promise1.then(function(value){
    console.log(value)
});
console.log('A');
// A B (先 A 后 B)


var promise2 = new Promise(function (resolve) {
  // 成功回调被通知了2次
  setTimeout(function(){
    resolve();
  },0)
});
// promise只会调用一次
promise2.then(function(){
    console.log('A')
});
// A (只有一个)

var promise3 = new Promise(function (resolve,reject) {
  // 成功回调先被通知,又通知了失败回调
  setTimeout(function(){
    resolve();
    reject();
  },0)

});
// promise只会调用成功回调
promise3.then(function(){
    console.log('A')
}).catch(function(){
    console.log('B')
});
// A(只有A)

介绍完 Promise 的特性后,来看看它如何利用链式调用,解决异步代码可读性的问题的。

var fetch = function(url){
    // 返回一个新的 Promise 实例
    return new Promise(function (resolve,reject) {
        ajax(url,resolve,reject);
    });
}

A();
fetch('url1').then(function(){
    B();
    // 返回一个新的 Promise 实例
    return fetch('url2');
}).catch(function(){
    // 异常的时候也可以返回一个新的 Promise 实例
    return fetch('url2');
    // 使用链式写法调用这个新的 Promise 实例的 then 方法    
}).then(function() {
    C();
    // 继续返回一个新的 Promise 实例...
})
// A B C ...

如此反复,不断返回一个 Promise 对象,再采用链式调用的方式不断地调用。使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。

Promise 解决的另外一个难点是 callback 只能捕获当前错误异常。Promise 和 callback 不同,每个 callback 只能知道自己的报错情况,但 Promise 代理着所有的 callback,所有 callback 的报错,都可以由 Promise 统一处理。所以,可以通过catch来捕获之前未捕获的异常。

Promise 解决了 callback 的异步调用问题,但 Promise 并没有摆脱 callback,它只是将 callback 放到一个可以信任的中间机构,这个中间机构去链接我们的代码和异步接口。

异步(async)函数

异步(async)函数是 ES7 的一个新的特性,它结合了 Promise,让我们摆脱 callback 的束缚,直接用类同步的“线性”方式,写异步函数。

声明异步函数,只需在普通函数前添加一个关键字 async 即可,如async function main(){} 。在异步函数中,可以使用await关键字,表示等待后面表达式的执行结果,一般后面的表达式是 Promise 实例。

async function main{
    // timer 是在上一个例子中定义的
    var value = await timer(100);
    console.log(value); // done (100ms 后返回 done)
}

main();

异步函数和普通函数一样调用 main() 。调用后,会立即执行异步函数中的第一行代码 var value = await timer(100) 。等到异步执行完成后,才会执行下一行代码。

除此之外,异步函数和其他函数基本类似,它使用try...catch来捕捉异常。也可以传入参数。但不要在异步函数中使用return来返回值。

var  timer = new Promise(function create(resolve,reject) {
  if(typeof delay !== 'number'){
    reject(new Error('type error'));
  }
  setTimeout(resolve,delay,'done');
});

async function main(delay){
  try{
    var value1 = await timer(delay);
    var value2 = await timer('');
    var value3 = await timer(delay);
  }catch(err){
    console.error(err);
      // Error: type error
      //   at create (<anonymous>:5:14)
      //   at timer (<anonymous>:3:10)
      //   at A (<anonymous>:12:10)
  }
}
main(0);

异步函数也可以被当作值,传入普通函数和异步函数中执行。但是在异步函数中,使用异步函数时要注意,如果不使用await,异步函数会被同步执行。

async function main(delay){
    var value1 = await timer(delay);
    console.log(&#39;A&#39;)
}

async function doAsync(main){
  main(0);
  console.log(&#39;B&#39;)
}

doAsync(main);
// B A

这个时候打印出来的值是 B A。说明 doAsync 函数并没有等待 main 的异步执行完毕就执行了 console。如果要让 console 在 main 的异步执行完毕后才执行,我们需要在main前添加关键字await。

async function main(delay){
    var value1 = await timer(delay);
    console.log(&#39;A&#39;)
}

async function doAsync(main){
    await main(0);
    console.log(&#39;B&#39;)
}

doAsync(main);
// A B

由于异步函数采用类同步的书写方法,所以在处理多个并发请求,新手可能会像下面一样书写。这样会导致url2的请求必需等到url1的请求回来后才会发送。

var fetch = function (url) {
  return new Promise(function (resolve,reject) {
    ajax(url,resolve,reject);
  });
}

async function main(){
  try{
    var value1 = await fetch(&#39;url1&#39;);
    var value2 = await fetch(&#39;url2&#39;);
    conosle.log(value1,value2);
  }catch(err){
    console.error(err)
  }
}

main();

使用Promise.all的方法来解决这个问题。Promise.all用于将多个Promise实例,包装成一个新的 Promis e实例,当所有的 Promise 成功后才会触发Promise.all的resolve函数,当有一个失败,则立即调用Promise.all的reject函数。

var fetch = function (url) {
  return new Promise(function (resolve,reject) {
    ajax(url,resolve,reject);
  });
}

async function main(){
  try{
    var arrValue = await Promise.all[fetch(&#39;url1&#39;),fetch(&#39;url2&#39;)];
    conosle.log(arrValue[0],arrValue[1]);
  }catch(err){
    console.error(err)
  }
}

main();

目前使用 Babel 已经支持 ES7 异步函数的转码了,大家可以在自己的项目中开始尝试。

以上就是JavaScript 异步进化史的内容,更多相关内容请关注PHP中文网(www.php.cn)!


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。