Asynchronous?
Saya telah melihat perkataan asynchronous (Asynchronous) di banyak tempat, tetapi apabila saya tidak memahami sepenuhnya konsep ini, saya mendapati bahawa saya sering dianggap sebagai "sudah jelas" (*  ̄? ̄ ).
Jika anda mempunyai situasi yang sama, tidak mengapa Cari perkataan ini dan anda boleh mendapatkan penjelasan kasar. Di sini, saya akan memberikan sedikit penjelasan tambahan tentang tak segerak JavaScript.
Lihat kod ini:
var start = new Date(); setTimeout(function(){ var end = new Date(); console.log("Time elapsed: ", end - start, "ms"); }, 500); while (new Date - start < 1000) {};
Selepas menjalankan kod ini, anda akan mendapat hasil seperti Masa berlalu: 1013ms. Fungsi yang ditetapkan untuk dilaksanakan 500ms pada masa hadapan oleh setTimeout() sebenarnya menunggu lebih daripada 1000ms sebelum melaksanakan.
Bagaimana untuk menerangkan? Apabila setTimeout() dipanggil, acara tertunda akan beratur. Kemudian, teruskan melaksanakan kod selepas ini, dan kod selepas itu, sehingga tiada lagi kod. Selepas tiada kod, urutan JavaScript menjadi melahu Pada masa ini, enjin pelaksanaan JavaScript melihat melalui baris gilir, mencari acara yang "sepatutnya dicetuskan" dalam baris gilir, dan kemudian memanggil pengendali (fungsi) acara ini. Selepas pemproses menyelesaikan pelaksanaan, ia kembali ke baris gilir dan melihat acara seterusnya.
JavaScript berbenang tunggal berfungsi dalam bentuk gelung acara melalui baris gilir. Oleh itu, dalam kod sebelumnya, while digunakan untuk menyeret enjin pelaksanaan sehingga 1000ms semasa kod sedang berjalan, dan tiada peristiwa akan dicetuskan sehingga semua kod selesai dan dikembalikan ke baris gilir. Ini ialah mekanisme tak segerak bagi JavaScript.
Kesukaran Async JavaScript
Operasi tak segerak dalam JavaScript mungkin tidak selalunya mudah.
Ajax mungkin operasi tak segerak yang paling kami gunakan. Mengambil jQuery sebagai contoh, kod untuk memulakan permintaan Ajax biasanya kelihatan seperti ini:
// Ajax请求示意代码 $.ajax({ url: url, data: dataObject, success: function(){}, error: function(){} });
Adakah terdapat apa-apa yang salah dengan cara penulisan ini? Ringkasnya, ia tidak cukup mudah alih. Mengapakah kita perlu menulis panggilan balik kejayaan dan ralat apabila permintaan dimulakan? Jika panggilan balik saya perlu melakukan banyak, banyak perkara, adakah saya perlu menjalankan kembali ke sini dan menambah kod apabila saya memikirkan satu perkara?
Untuk contoh lain, kami ingin melengkapkan perkara seperti itu: terdapat 4 alamat url untuk akses Ajax Kami perlu mengakses yang pertama melalui Ajax dahulu Selepas akses pertama selesai, gunakan yang dikembalikan data yang diperolehi sebagai Parameter kedua diakses semula, dan kemudian yang ketiga diakses selepas capaian kedua selesai... Mulai saat ini, kesemua 4 capaian selesai. Mengikut cara penulisan ini, ia nampaknya menjadi seperti ini:
$.ajax({ url: url1, success: function(data){ $.ajax({ url: url2, data: data, success: function(data){ $.ajax({ //... }); } }); } })
Anda pasti akan berfikir bahawa kod yang dipanggil Pyramid of Doom ini kelihatan seperti Ia kelihatan dahsyat. Jika anda biasa menulis panggilan balik yang dilampirkan secara langsung, anda mungkin berasa keliru tentang peristiwa tak segerak yang dihantar dari satu ke yang seterusnya. Menamakan fungsi panggil balik ini secara berasingan dan menyimpannya secara berasingan boleh mengurangkan bentuk sarang dan menjadikan kod lebih jelas, tetapi ia masih tidak menyelesaikan masalah.
Satu lagi kesukaran yang biasa ialah menghantar dua permintaan Ajax pada masa yang sama, dan kemudian melakukan perkara seterusnya selepas kedua-dua permintaan kembali dengan jayanya jika anda hanya mengikut kaedah sebelumnya untuk melakukan perkara seterusnya nampak agak sukar untuk melampirkan panggilan balik pada kedudukan panggilan?
Promise sesuai untuk menangani operasi tak segerak ini dan membolehkan anda menulis kod yang lebih elegan.
Janji datang ke pentas
Apa itu Janji? Mari kita teruskan mengambil kod isyarat permintaan jQuery Ajax sebelumnya sebagai contoh Kod itu sebenarnya boleh ditulis seperti ini:
var promise = $.ajax({ url: url, data: dataObject }); promise.done(function(){}); promise.fail(function(){});
Ini serupa dengan yang sebelumnya. Isyarat permintaan Ajax Kod adalah setara. Seperti yang anda lihat, penambahan Promise mengubah bentuk kod. Permintaan Ajax "disimpan" sama seperti tugasan berubah-ubah. Ini ialah enkapsulasi, dan enkapsulasi benar-benar akan menjadikan acara tak segerak lebih mudah.
Encapsulation berguna
Objek Promise adalah seperti rujukan terkapsul kepada peristiwa tak segerak. Ingin melakukan sesuatu selepas acara async ini selesai? Hanya lampirkan panggilan balik padanya, tidak kira berapa banyak yang anda lampirkan, ia tidak penting!
Kaedah Ajax jQuery akan mengembalikan objek Promise (ini ialah ciri utama yang ditambahkan dalam jQuery 1.5). Jika saya mempunyai dua fungsi do1() dan do2() yang ingin saya laksanakan selepas acara tak segerak berjaya diselesaikan, saya hanya perlu melakukan ini:
promise.done(do1); // Other code here. promise.done(do2);
Cara ini lebih percuma, saya hanya perlu menyimpan objek Promise ini dan melampirkan sebarang bilangan panggilan balik padanya pada bila-bila masa semasa menulis kod, tidak kira di mana acara tak segerak dimulakan. Inilah kelebihan Janji.
Pengenalan rasmi
Promise sangat berguna untuk operasi tak segerak sehingga ia telah dibangunkan menjadi spesifikasi CommonJS yang dipanggil Promises/A. Promise mewakili nilai pulangan selepas operasi selesai Ia mempunyai tiga keadaan:
Positif (ditepati atau diselesaikan), menunjukkan bahawa operasi Promise telah berjaya.
Penolakan (ditolak atau gagal) menunjukkan bahawa operasi Promise gagal.
Menunggu (menunggu), tiada keputusan positif atau negatif diperoleh lagi, sedang dijalankan.
Selain itu, terdapat keadaan nominal yang digunakan untuk menunjukkan bahawa operasi Janji telah berjaya atau gagal, iaitu himpunan keadaan positif dan negatif, dipanggil diselesaikan. Promise juga mempunyai ciri penting berikut:
一个Promise只能从等待状态转变为肯定或否定状态一次,一旦转变为肯定或否定状态,就再也不会改变状态。
如果在一个Promise结束(成功或失败,同前面的说明)后,添加针对成功或失败的回调,则回调函数会立即执行。
想想Ajax操作,发起一个请求后,等待着,然后成功收到返回或出现错误(失败)。这是否和Promise相当一致?
进一步解释Promise的特性还有一个很好的例子:jQuery的$(document).ready(onReady)。其中onReady回调函数会在DOM就绪后执行,但有趣的是,如果在执行到这句代码之前,DOM就已经就绪了,那么onReady会立即执行,没有任何延迟(也就是说,是同步的)。
Promise示例
生成Promise
Promises/A里列出了一系列实现了Promise的JavaScript库,jQuery也在其中。下面是用jQuery生成Promise的代码:
var deferred = $.Deferred(); deferred.done(function(message){console.log("Done: " + message)}); deferred.resolve("morin"); // Done: morin
jQuery自己特意定义了名为Deferred的类,它实际上就是Promise。$.Deferred()方法会返回一个新生成的Promise实例。一方面,使用deferred.done()、deferred.fail()等为它附加回调,另一方面,调用deferred.resolve()或deferred.reject()来肯定或否定这个Promise,且可以向回调传递任意数据。
合并Promise
还记得我前文说的同时发送2个Ajax请求的难题吗?继续以jQuery为例,Promise将可以这样解决它:
var promise1 = $.ajax(url1), promise2 = $.ajax(url2), promiseCombined = $.when(promise1, promise2); promiseCombined.done(onDone);
$.when()方法可以合并多个Promise得到一个新的Promise,相当于在原多个Promise之间建立了AND(逻辑与)的关系,如果所有组成Promise都已成功,则令合并后的Promise也成功,如果有任意一个组成Promise失败,则立即令合并后的Promise失败。
级联Promise
再继续我前文的依次执行一系列异步任务的问题。它将用到Promise最为重要的.then()方法(在Promises/A规范中,也是用“有then()方法的对象”来定义Promise的)。代码如下:
var promise = $.ajax(url1); promise = promise.then(function(data){ return $.ajax(url2, data); }); promise = promise.then(function(data){ return $.ajax(url3, data); }); // ...
Promise的.then()方法的完整形式是.then(onDone, onFail, onProgress),这样看上去,它像是一个一次性就可以把各种回调都附加上去的简便方法(.done()、.fail()可以不用了)。没错,你的确可以这样使用,这是等效的。
但.then()方法还有它更为有用的功能。如同then这个单词本身的意义那样,它用来清晰地指明异步事件的前后关系:“先这个,然后(then)再那个”。这称为Promise的级联。
要级联Promise,需要注意的是,在传递给then()的回调函数中,一定要返回你想要的代表下一步任务的Promise(如上面代码的$.ajax(url2, data))。这样,前面被赋值的那个变量才会变成新的Promise。而如果then()的回调函数返回的不是Promise,则then()方法会返回最初的那个Promise。
应该会觉得有些难理解?从代码执行的角度上说,上面这段带有多个then()的代码其实还是被JavaScript引擎运行一遍就结束。但它就像是写好的舞台剧的剧本一样,读过一遍后,JavaScript引擎就会在未来的时刻,依次安排演员按照剧本来演出,而演出都是异步的。then()方法就是让你能写出异步剧本的笔。
将Promise用在基于回调函数的API
前文反复用到的$.ajax()方法会返回一个Promise对象,这其实只是jQuery特意提供的福利。实际情况是,大多数JavaScript API,包括Node.js中的原生函数,都基于回调函数,而不是基于Promise。这种情况下使用Promise会需要自行做一些加工。
这个加工其实比较简单和直接,下面是例子:
var deferred = $.Deferred(); setTimeout(deferred.resolve, 1000); deferred.done(onDone);
这样,将Promise的肯定或否定的触发器,作为API的回调传入,就变成了Promise的处理模式了。
Promise是怎么实现出来的?
本文写Promise写到这里,你发现了全都是基于已有的实现了Promise的库。那么,如果要自行构筑一个Promise的话呢?
位列于Promises/A的库列表第一位的Q可以算是最符合Promises/A规范且相当直观的实现。如果你想了解如何做出一个Promise,可以参考Q提供的设计模式解析。
限于篇幅,本文只介绍Promise的应用。我会在以后单独开一篇文章来详述Promise的实现细节。
作为JavaScript后续版本的ECMAScript 6将原生提供Promise,如果你想知道它的用法,推荐阅读JavaScript Promises: There and back again。
结语
Perkataan Janji sangat degil sehingga tidak sesuai untuk terjemahan, dan maknanya akan menjadi tidak jelas sepintas lalu. Walau bagaimanapun, ia sememangnya boleh memberikan bantuan yang besar apabila melakukan tugas tak segerak yang lebih kompleks dalam JavaScript.
Di atas ialah kandungan penggunaan Promise dalam pengaturcaraan JavaScript tak segerak_node.js?1.1.5 Untuk kandungan yang lebih berkaitan, sila beri perhatian kepada tapak web PHP Cina (m.sbmmt.com)!