前言
javascript是單執行緒的一門語言,所以在執行任務的時候,所有任務必須排隊,然後一個一個的執行, 在javascript中有分同步程式碼,和非同步程式碼,顧名思義,同步程式碼,就是依此執行的程式碼,非同步程式碼可能不會立即執行,得等到某一特定事件觸發時才會執行,javascript有個任務佇列,用來存放非同步程式碼,任務佇列中的任務又有優先順序之分,微任務(microtask)的優先順序大於巨集任務(macrotask),在javascript中程式碼的執行順序為,主執行緒會先執行完同步程式碼,並將非同步程式碼放到任務佇列中,當同步程式碼執行完畢,就輪詢任務佇列,先詢問微任務,如果有則執行微任務,如果沒有詢問巨集任務。
//非同步程式碼
setTimeout(function () { //屬於巨集任務
console.log('hello world3');
},0);
new Promise(resolve => { //屬於微任務
console.log('hello world4'); //Promise 物件會立即執行 所以new Promise裡面的類似與同步程式碼
resolve('hello world5');
}).then(data => {console.log(data)});
//同步程式碼
function main(){
console.log('hello world');
}
console.log('hello world1');
console.log('hello world2');
main();
複製程式碼
輸出結果為:
hello world4
hello world1
hello world2
hello world
hello world5
hello world3
複製程式碼
按照上面所說的順序,同步程式碼先執行,那麼會先輸出hello world4 然後hello world1 ,hello world2,hello world 接下來執行任務佇列的非同步程式碼,先輪詢微任務是否有要執行的程式碼,由於Promise物件屬於微任務的,故先執行它,輸出hello world5 ,然後執行巨集任務的程式碼,及setTimeout的程式碼,輸出hello world3
本例比較簡單,講述了一下javascript程式碼的執行流程,希望對理解非同步有幫助,其中涉及的Promise物件會在本文詳細介紹。
本文程式碼可能比較多,所有涉及的程式碼均在我的github上
接下來回歸正題,Javascript中非同步的5種實現方法,並以ajax等為例子,實現幾種非同步的編寫方式 javascript中的非同步實現方式有以下幾種
- callback (回撥函式)
- 釋出訂閱模式
- Promise物件
- es6的生成器函式
- async/awit
1.callback (回撥函式)
回撥函式是Javascript非同步程式設計中最常見的,由於JavaScript中的函式是一等公民,可以將其以引數形式傳遞,故就有了回撥函式一說,熟悉nodejs的人知到,裡面涉及非常多的回撥,這些回撥代表著,當某個任務處理完,然後需要做的事,比如像一些動畫處理,當動畫走完,然後執行回撥,或者連線資料庫等,舉個例子
function load(url,callback){
//something
setTimeout(callback,3000);//假設某個非同步任務處理需要3s 3s後執行回撥
}
load('xxx',function() {
//do something
console.log('hello world')
})
複製程式碼
3s執行回撥,回撥的內容自己決定
再來看個ajax例子 (程式碼 )
//ajax_callback.js
function ajax(object, callback) {
function isFunction(func) { // 是否為函式
return typeof func === 'function';
}
function isObject(object) { //是否為物件
return typeof object === 'object';
}
function toQuerystring(data) { //物件轉成查詢字串 例如{a:1,b:2} => a=1&b=2 或{a:[1,2],b:3} => a=1&a=2&b=3
if (!isObject(data) || !data) throw new Error('data not object');
var result = '';
for (var key in data) {
if (data.hasOwnProperty(key)) {
if (isObject(data[key]) && !Array.isArray(data[key])) throw new Error('not support error');//除去物件
if (Array.isArray(data[key])) {
data[key].forEach(function (v) {
result += key + '=' + v + '&'
});
} else {
result += key + '=' + data[key] + '&';
}
}
}
return result.substr(0, result.length - 1);//去掉末尾的&
}
var url = object.url || '';
var method = object.method.toUpperCase() || 'GET';
var data = object.data || Object.create(null);
var async = object.async || true;
var dataType = object.dataType || 'json';//相應的資料型別 可選json ,text, xml
var xhr = new XMLHttpRequest();
url = ajax.baseUrl + url;
data = toQuerystring(data);
method === 'GET' && (url += '?' + data) && (data = null); //get 請求 => url 後面加上 ?a=1&b=2這種
try {
xhr.open(method, url, async);
method === 'POST' && (xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'));//post請求需要設定請求頭為 application/x-www-form-urlencoded 型別
console.log(data);
xhr.send(data);
xhr.onreadystatechange = function () {//監聽事件
if (this.readyState === 4) {
if (this.status === 200)
if (isFunction(callback))
switch (dataType) {
case 'json': {
callback(JSON.parse(this.responseText));//完成時執行傳進來的回撥
break
}
case 'text': {
callback(this.responseText);
break
}
case 'xml': {
callback(this.responseXML);
break
}
default: {
break;
}
}
}
}
} catch (e) {
console.log(e);
}
}
ajax.get = function (url, data, callback) { //get方法
this({url: url, method: 'GET', data: data}, callback);
};
ajax.post = function (url, data, callback) { //post方法
this({url: url, method: 'POST', data: data}, callback);
};
ajax.baseUrl = '';
複製程式碼
以上是個完整的ajax例項,當ajax完成執行回撥 一下是使用koa實現的一個簡易的服務端,模擬處理ajax的響應,之後的例子都會用這個來模擬ajax響應
//koa_test_server.js
const Koa = require('koa');
const Router = require('koa-router');
const bodyparser = require('koa-bodyparser');
const app = new Koa();
const api = new Router();
api.get('/api/test1', async ctx => { //處理get請求
ctx.res.setHeader('Access-Control-Allow-Origin', '*');//允許跨域訪問
let querystring = ctx.querystring;
console.log(querystring);
ctx.body = JSON.stringify({
errno: false,
data: 'it is ok',
message: `you send me ${querystring} type is GET`
});
}).post('/api/test2', async ctx => {//處理post請求
ctx.res.setHeader('Access-Control-Allow-Origin', '*');//允許跨域訪問
let data = ctx.request.body;
console.log(data);
ctx.body = JSON.stringify({
errno: false,
data: 'it is ok',
message: `you send me ${JSON.stringify(data)} type is POST`
})
});
app.use(bodyparser());
app.use(api.routes()).use(api.allowedMethods());
app.listen(3000, () => {
console.log('listen in port 3000')
});
複製程式碼
簡單使用如下
//test.html
ajax.baseUrl = 'http://localhost:3000';
ajax.get('/api/test1',{name: 'dpf', age: 19},function (data) {
//do something such as render page
console.log(data);
});
ajax.post('/api/test2',{name: 'youname', age: 19}, function (data) {
//do something such as render page
console.log(data);
});
複製程式碼
結果如下:
回撥的好處就是容易編寫,缺點就是過多的回撥會產生回撥地獄,程式碼橫向擴充套件,程式碼可讀性變差 不過回撥還有很多應用,而且回撥也是最常用的實現Javascript非同步的方式。
2.釋出訂閱模式
釋出訂閱模式是設計模式的一種,並不是javascript特有的內容,所以javascript可以用釋出訂閱模式來做非同步,那麼其他語言如C++ java python php 等自然也能。
簡單介紹一下發布訂閱模式,釋出訂閱是兩個東西,即釋出和訂閱,想象一下,有家外賣,你可以點外賣,這就是訂閱,當你的外賣做好了,就會有人給你打電話叫你去取外賣,這就是釋出,簡單來說,釋出訂閱模式,有一個事件池,用來給你訂閱(註冊)事件,當你訂閱的事件發生時就會通知你,然後你就可以去處理此事件,模型如下
接下來簡單實現這個釋出訂閱模式
//async_Event.js
//單物件寫法 Event 就相當於事件中心
const Event = function () { //使用閉包的好處 : 把EventPool私有化,外界無法訪問EventPool
const EventPool = new Map();//使用es6 map來存 event,callback 鍵值對
const isFunction = func => typeof func === 'function';
const on = (event, callback) => { //註冊事件
EventPool.get(event) || EventPool.set(event, []);
if (isFunction(callback)) {
EventPool.get(event).push(callback);
}
else {
throw new Error('callback not is function')
}
};
const addEventListenr = (event, callback) => { //on方法別名
on(event, callback)
};
const emit = (event, ...args) => { //觸發(釋出)事件
//讓事件的觸發為一個非同步的過程,即排在同步程式碼後執行
//也可以setTimeout(fn,0)
Promise.resolve().then(() => {
let funcs = EventPool.get(event);
if (funcs) {
funcs.forEach(f => f(...args))
} else {
throw new Error(`${event} not register`)
}
})
};
const send = (event, ...args) => {//emit方法別名
emit(event,...args)
};
const removeListener = event => {//刪除事件
Promise.resolve(() => {//刪除事件也為非同步的過程
if(event){
EventPool.delete(event)
}else{
throw new Error(`${event} not register`)
}
})
};
return {
on, emit, addEventListenr, send
}
}();
複製程式碼
簡單使用
Event.on('event', data => {
console.log(data)
});
setTimeout(() => {
Event.emit('event','hello wrold')
},1000);
複製程式碼
1s後觸發事件,輸出hello world
使用釋出訂閱模式,修改之前的ajax例子
......
xhr.onreadystatechange = function () {//監聽事件
if (this.readyState === 4) {
if (this.status === 200)
switch (dataType) {
case 'json': {
Event.emit('data '+method,JSON.parse(this.responseText));//觸發事件
break
}
case 'text': {
Event.emit('data '+method,this.responseText);
break
}
case 'xml': {
Event.emit('data '+method,this.responseXML);
break
}
default: {
break;
}
}
}
}
......
複製程式碼
使用如下
//test.html
//註冊事件
Event.on('data GET',data => {
//do something such as render page
console.log(data)
});
Event.on('data POST',data => {
//do something such as render page
console.log(data)
});
//使用ajax
ajax.baseUrl = 'http://localhost:3000';
ajax.get('/api/test1',{name: 'dpf', age: 19});
ajax.post('/api/test2',{name: 'youname', age: 19});
複製程式碼
使用釋出訂閱模式的好處是事件集中管理,修改方便,缺點就是,程式碼可讀性下降,事件容易衝突。
3.Promise物件
Promise物件是非同步程式設計的一種解決方案,比傳統的回撥函式和事件更合理更強大。 Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件的結果,相比回撥函式,Promise提供統一的API,各種非同步操作都可以用同樣的方法進行處理。 Promisel物件的兩個特點:
##1.物件狀態不受外界影響。Promise物件有三種狀態:pending(進行中),fulfilled(已成功),rejected(已失敗),當非同步操作有結果時可以指定pending狀態到fulfilled狀態或pending狀態到rejected狀態的轉換,狀態一旦變為fulfilled,或rejected則這個Promise物件狀態不會在改變。
##2.一旦狀態改變,就不再變化,任何時候都可以得到這個結果。
基本格式
let promise = new Promise((resolve, reject) => {//Promise物件接受一個函式
try {
setTimeout(() => {//模擬某非同步操作 , 若操作成功返回資料
resolve('hello world'); //resolve() 使pending狀態變為 fulfilled,需要注意resolve()函式最多隻能接收1個引數,若要傳多個引數,需要寫成陣列,或物件,比如resolve([1,2,2,2])或resolve({data,error})
reject(); //狀態已變為fulfilled 故下面這個reject()不執行
}, 1000);
}catch (e) {
reject(e) //操作失敗 返回Error物件 reject() 使pending狀態變為rejected
}
});
promise.then((data) => {
console.log(data) //resolve()函式裡面傳的值
},(err) => {
console.log(err) //reject()函式裡傳的值
});
複製程式碼
1s後輸出hello world Promise物件的幾個方法
##1. then(fulfilled,rejected)方法: 非同步任務完成時執行的方法,其中fulfilled(data)和rejected(err)分別是單參的回撥函式,fulfilled對應的是成功時執行的回撥,rejected對應的是失敗時執行的回撥,fulfilled函式的所接引數為resolve()函式傳的值,rejected函式的引數則為reject()函式所傳的值。
##2. catch(rejected)方法: then(null,rejected)的別名 捕獲Promise物件中的錯誤
##3. Promise.resolve(data):等價於new Promise(resolve => {resolve(data)})
##4.Promise.all([promise1,promise2,...,promisen]): 用於多個Promise物件的執行,執行時間取最慢的那個,例如:
let promise1 = new Promise(resolve => {
setTimeout(() => {
resolve(1);
}, 1000);
});
let promise2 = new Promise(resolve => {
setTimeout(() => {
resolve(2)
}, 2000)
});
let promise3 = new Promise(resolve => {
setTimeout(() => {
resolve(3)
}, 3000)
});
let start = Date.now();
Promise.all([promise1, promise2, promise3]).then(([data1, data2, data3]) => {//使用陣列解構獲得每個Promise物件的data
console.log(`datas = ${data1},${data2},${data3} total times = ${Date.now() - start}ms`);
});
複製程式碼
輸出結果為 datas = 1,2,3 total times = 3000ms
##5.Promise.race([promise1,promise2,...,promisen]): 和Promise.all類似,不過它取Promise物件中最快的那個。
##6.Promise.reject(err): 等價於new Promise((resolve,reject) => reject(err))
對有了Promise物件有了基本的理解,然後可以用它來替代回撥函式的模式,比如一個圖片載入例子
//回撥形式
function asyncLoadImage_callback(url,callback) {//非同步載入圖片
var proxyImage = new Image();//圖片代理
proxyImage.src = url;
proxyImage.onload = callback;//載入完時執行回撥
}
asyncLoadImage_callback('xxx', function () {
image.src = 'xxx'//讓真正的圖片物件顯示
});
//Promise物件形式
function asyncLoadImage_Promise(url) {
return new Promise((resolve,reject) => {
var proxyImage = new Image();
proxyImage.src = url;
proxyImage.onload = resolve;
proxyImage.onerror = reject;
})
}
asyncLoadImage_Promise('xxx')
.then(() => {
image.src = 'xxx'//讓真正的圖片物件顯示
}).catch(err => console.log(err));
複製程式碼
使用Promise物件的好處比較明顯,除了寫起來有一些麻煩而已。
接下來將介紹將回撥函式形式與Promise物件形式的相互轉換
##1.回撥函式形式轉換為Promise物件形式
//promisify.js
//callback => Promise
/**
*
* @param fn_callback
* @returns {function(...[*]): Promise<any>}
*/
function promisify(fn_callback) { //接收一個有回撥函式的函式,回撥函式一般在最後一個引數
if(typeof fn_callback !== 'function') throw new Error('The argument must be of type Function.');
return function (...args) {//返回一個函式
return new Promise((resolve, reject) => {//返回Promise物件
try {
if(args.length > fn_callback.length) reject(new Error('arguments too much.'));
fn_callback.call(this,...args,function (...args) {
args[0] && args[0] instanceof Error && reject(args[0]);//nodejs的回撥,第一個引數為err, Error物件
args = args.filter(v => v !== undefined && v !== null);//除去undefined,null引數
resolve(args)
}.bind(this));//保證this還是原來的this
} catch (e) {
reject(e)
}
})
}
}
複製程式碼
簡單使用
//nodejs的fs.readFile為例
let asyncReadFile = promisify(require('fs').readFile);
asyncReadFile('async.js').then(([data]) => {
console.log(data.toString());
}, err => console.log(err));
//將上面的asyncLoadImage_callback轉換為例
let asyncLoadImage = promisify(asyncLoadImage_callback);
asyncLoadImage.then(() => {
image.src = 'xxx'//讓真正的圖片物件顯示
});
複製程式碼
##2. Promise物件形式轉換為回撥函式形式
//callbackify.js
//Promise => callback
/**
*
* @param fn_promise
* @returns {Function}
*/
function callbackify(fn_promise) {
if(typeof fn_promise !== 'function') throw new Error('The argument must be of type Function.');
return function (...args) {
let callback = args.pop();//返回一個函式 最後一個引數是回撥
if(typeof callback !== 'function') throw new Error('The last argument must be of type Function.');
if(fn_promise() instanceof Promise){
fn_promise(args).then(data => {
callback(null,data)//回撥執行
}).catch(err => {
callback(err,null)//回撥執行
})
}else{
throw new Error('function must be return a Promise object');
}
}
}
複製程式碼
簡單使用
let func = callbackify(timer => new Promise((resolve, reject) => {
setTimeout(() => {resolve('hello world')},timer);
}));
func(1000,function (err,data) {
console.log(data)//1s後列印hello world
});
複製程式碼
接下來對之前的ajax例子進行改寫,將回撥形式變為Promise形式,可以直接改寫,或使用promisify函式
##第一種方式
//ajax_promise.js
function ajax(object) {
return new Promise(function (resolve,reject) {
....
try {
....
xhr.onreadystatechange = function () {//監聽事件
if (this.readyState === 4) {
if (this.status === 200) {
switch (dataType) {
case 'json': {
resolve(JSON.parse(this.responseText));
break
}
case 'text': {
resolve(this.responseText);
break
}
case 'xml': {
resolve(this.responseXML);
break
}
default: {
break;
}
}
}else{
reject(new Error('error'))
}
}
}
} catch (e) {
reject(e)
}
});
}
ajax.get = function (url, data) { //get方法
return this({url: url, method: 'GET', data: data});
};
ajax.post = function (url, data) { //post方法
return this({url: url, method: 'POST', data: data});
};
ajax.baseUrl = '';
複製程式碼
簡單使用
//test.html
ajax.baseUrl = 'http://localhost:3000';
ajax.get('/api/test1',{name: 'dpf', age: 19}).then(data => {
console.log(data)
});
ajax.post('/api/test2',{name: 'youname', age: 19}).then(data => {
console.log(data)
});
複製程式碼
##第二種方式
//test.html
ajax = promisify(ajax);
ajax.baseUrl = 'http://localhost:3000';
ajax.get = (url,data) => ajax({url: url, method: 'GET', data: data});
ajax.post = (url,data) => ajax({url: url, method: 'POST', data: data});
ajax.get('/api/test1', {name: 'dpf', age: 19}).then(([data]) => {
console.log(data)
});
ajax.post('/api/test2', {name: 'youname', age: 19}).then(([data]) => {
console.log(data)
});
複製程式碼
Promise物件目前是比較流行的非同步解決方案,相比回撥函式而言,程式碼不再橫向擴充套件,而且沒有回撥地獄這一說,好處還是挺多的,不過也有不足,就是寫起來費勁(相比回撥而言),不過Promise物件仍然是javascript的一個重要的知識點,希望通過剛剛的講解,讀者能對Promise物件有個基本的認識。
4.Generator(生成器)函式 Generator函式是ES6提供的一種非同步程式設計解決方案,其行為類似於狀態機。 一個簡單的例子
function *gen(){//宣告一個生成器
let t1 = yield "hello"; //yield 表示 產出的意思 用yield來生成東西
console.log(t1);
let t2 = yield "world";
console.log(t2);
}
let g = gen();
/*next()返回一個{value,done}物件,value為yield表示式後面的值,done取值為true/false,表示是否 *生成結束*/
let x = g.next();//{value:"hello",done:false} 啟動生成器
/**
* 通過給next()函式裡傳值 這裡的值會傳遞到第一個yield表示式裡 即相當於gen函式裡 let t1 = "aaaa" */
let y = g.next("aaaa");//{value:"world",done:false}
g.next("bbbb");//{value:undefined,done:true}
console.log(x.value,y.value);
複製程式碼
輸出
aaaa
bbbb
hello world
複製程式碼
上面的例子中,如果把gen函式當成一個狀態機,則通過呼叫next()方法來跳到下一個狀態,即下一個yield表示式,給next()函式傳值來把值傳入上一個狀態中,即上一個yield表示式的結果。 在介紹Generator函式的非同步時,先簡單介紹一下Generator函式的幾個方法
##1.next()方法:生成器函式裡面的yield表示式並沒有值,或者說總返回undefined,next()函式可以接受一個引數,該引數就會被當作yield表示式的值。
##2.throw()方法:在函式體外丟擲一個錯誤,然後在函式體內捕獲。例如
function *gen1(){
try{
yield;
}catch(e){
console.log('內部捕獲')
}
}
let g1 = gen1();
g1.next();
g1.throw(new Error());
複製程式碼
列印出
內部捕獲
複製程式碼
##3.return()方法:返回給定值,並終結生成器。例如
function *gen2(){
yield 1;
yield 2;
yield 3;
}
let g2 = gen1();
g2.next();//{value:1,done:false}
g2.return();//{value:undefined,done:true}
g2.next();//{value:undefined.done:true}
複製程式碼
##4.yield*表示式:在生成器函式中呼叫另一個生成器函式。例如
function *gen3(){
yield 1;
yield 2;
yield 3;
}
function *gen4(){
yield 4;
yield * gen3();
yield 5;
}
//等價於
function *gen4(){
yield 4;
yield 1;
yield 2;
yield 3;
yield 5;
}
複製程式碼
在使用Generator(生成器)函式做非同步時,先引入協程這個概念,可以理解為 "協作的函式",一個協程本質就是子函式,不過這個子函式可以執行到一半,可以暫停執行,將執行權交給其他子函式,等稍後回收執行權的時候,還可以繼續執行,跟執行緒非常像,在c++/python/java中一個執行緒的單位也是一個子函式(java的run方法),執行緒之間的切換,就相當於函式的切換,不過這個切換代價非常大,得儲存很多跟執行緒相關東西,而協程則沒那麼複雜,所以協程又被稱為纖程,或輕量級執行緒。
協程的執行流程大致如下:
##1.協程A開始執行。
##2.協程A執行到一半,進入暫停,執行權轉移給協程B。
##3.(一段時間後)協程B交還執行權。
##4.協程A恢復執行
其中協程A就是非同步任務,因為其分多段執行。
接下來將介紹使用Generator函式來實現協程,並做到非同步。 首先來看一個簡單的例子
const fs = require('fs');
function* gen(){//生成器函式
let data = yield asyncReadFile(__dirname+'/ajax_promise.js');
console.log(data); //檔案讀取成功 則輸出
let data2 = yield timer(1000);
console.log(data2); //過1s後輸出 hello world
}
let it = gen();
it.next();
function timer(time){//非同步任務
setTimeout(() => it.next('hello world'),time)
}
function asyncReadFile(url) {//非同步任務 讀取檔案
fs.readFile(url,(err,data) => {
it.next(data.toString())
})
}
複製程式碼
可以看出通過暫緩it.next()方法的執行,來實現非同步的功能,如果僅看gen的函式裡面內部,比如
let data = yield asyncReadFile(__dirname+'/ajax_promise.js');
這一段,可以理解為data等待非同步讀取檔案asyncReadFile的結果,如果有了結果,則輸出,gen繼續向下執行,不過每一個非同步函式,比如asyncReadFile的實現卻變麻煩了,這個時候就要藉助Promise物件,例子如下
const promisify = require('./promisify');
function timer(time,callback){
setTimeout(() => callback(), time)
}
const asyncReadFile = promisify(require('fs').readFile);//借用之前的promisify方法,將callback形式轉換為Promise
const asyncTimer = promisify(timer);
function *gen(){
let [data] = yield asyncReadFile('./a.mjs');//生成一個Promise物件
console.log(data);
yield asyncTimer(1000);
console.log('hello world');
}
let g = gen();
let {value} = g.next(); //{value:asyncReadFile('./a.mjs'),done:false}
value.then(data => {//相當於asyncReadFile('./a.mjs').then(data => {})
let {value} = g.next(data);//{value:asyncTimer(1000),done:false}
value.then(data => {//相當於asyncTimer(1000).then(data => {})
g.next(data);//{value:undefined,done:true}
})
});
複製程式碼
可以看出上面的藉助Promise物件例子,在非同步處理上可以有更通用的實現,即生成器執行器,
//run.js
function run(gen){//傳入一個生成器函式
let g = gen();
function next(data){
let result = g.next(data);
let {value,done} = result;
if(done) return value;//done為true時結束遞迴
if (Array.isArray(value)) value = Promise.all(value);//如果yield表示式後面跟的是一個陣列,可以將其轉換為Promise.all
if(!value instanceof Promise) value = Promise.resolve(value)//不是Promise物件,則轉成Promise物件
value.then((data) => {
next(data);//遞迴呼叫
});
}
next();//啟動生成器
}
複製程式碼
藉助run執行器函式,執行上面的gen只需要run(gen)即可 最後讓我們來繼續改寫之前的ajax例子,這次使用Generator函式,程式碼如下
//test.html
ajax = promisify(ajax);
ajax.baseUrl = 'http://localhost:3000';
ajax.get = (url,data) => ajax({url: url, method: 'GET', data: data});
ajax.post = (url,data) => ajax({url: url, method: 'POST', data: data});
run(function*(){
let [[data1],[data2]] = yield [ajax.get('/api/test1', {name: 'dpf', age: 19}),ajax.post('/api/test2', {name: 'youname', age: 19})];//相當於Promise.all
console.log(data1,data2)
});
複製程式碼
使用Generator函式無疑是解決非同步的優於callback(回撥),及Promise物件的好方法,沒有callback回撥地獄,Promise物件的過長then鏈,非同步程式碼看起來跟同步程式碼一樣,可讀性,和維護性都較好。
5.async/await(javascript非同步的終極解決方案)
es6中使用Generator函式來做非同步,在ES2017中,提供了async/await兩個關鍵字來實現非同步,讓非同步變得更加方便。 async/await本質上還是基於Generator函式,可以說是Generator函式的語法糖,async就相當於之前寫的run函式(執行Generator函式的函式),而await就相當於yield,只不過await表示式後面只能跟著Promise物件,如果不是Promise物件的話,會通過Promise.resolve方法使之變成Promise物件。async修飾function,其返回一個Promise物件。await必須放在async修飾的函式裡面,就相當於yield只能放在Generator生成器函式裡一樣。一個簡單的例子
//封裝一個定時器,返回一個Promise物件
const timer = time => new Promise((resolve,reject) => {
setTimeout(() => resolve('hello world'),time)
});
async function main() {//async函式
let start = Date.now();
let data = await timer(1000);//可以把await理解為 async wait 即非同步等待(雖然是yield的變體),當Promise物件有值的時候將值返回,即Promise物件裡resolve(data)裡面的data,作為await表示式的結果
console.log(data,'time = ',Date.now() - start,'ms')//將會輸出 hello world time = 1002 ms
}
main();
複製程式碼
可以看到async/await使用起來非常方便,其實async/await的原理也非常簡單,就是把Generator函式和執行器包裝在一起,其實現如下
//spawn.js
//之前的run函式的變體,只不過多了錯誤處理,然後返回的是Promise物件
function spawn(genF){
return new Promise((resolve,reject) => {
let g = genf();
function next(nextF){
let next;
try{
next = nextF();
}catch(e){
reject(e)
}
if(next.done) return resolve(next.value);
Promise.resolve(next.value)
.then(data => next(() => g.next(data)))
.catch(err => next(() => g.throw(err)));
}
next(() => g.next(undefined))
})
}
複製程式碼
所以之前的async function main() {} 就等價於 function main() { return spawn(function *() {}) },瞭解async的內部原理可以有助於理解和使用async。
接下來看使用async/await來改進之前的ajax的例子
//test.html
ajax = promisify(ajax);
ajax.baseUrl = 'http://localhost:3000';
ajax.get = (url,data) => ajax({url: url, method: 'GET', data: data});
ajax.post = (url,data) => ajax({url: url, method: 'POST', data: data});
(async function() {
let [data1,data2] = await Promise.all([ajax.get('/api/test1', {name: 'dpf', age: 19}),ajax.post('/api/test2', {name: 'youname', age: 19})]);
console.log(data1,data2)
})()
複製程式碼
到此,這篇文章已經接近尾聲,總結一下JavaScript實現非同步的這五種方式的優缺點 ##1.callback(回撥函式):寫起來方便,不過過多的回撥會產生回撥地獄,程式碼橫向擴充套件,不易於維護和理解
##2.釋出訂閱模式:通過實現個事件管理器,方便管理和修改事件,不同的事件對應不同的回撥,通觸發事件來實現非同步,不過會產生一些命名衝突的問題,事件到處觸發,可能程式碼可讀性不好。
##3.Promise物件:本質是用來解決回撥產生的程式碼橫向擴充套件,及可讀性不強的問題,通過.then方法來替代掉回撥,而且then方法接的引數也有限制,所以解決了,回撥產生的引數不容易確定的問題,缺點的話,個人覺得,寫起來可能不那麼容易,不過寫好了,用起來就就方便多了。
##4.Generator(生成器)函式:記得第一次接觸Generator函式是在python中,而且協程的概念,以及使用生成器函式來實現非同步,也是在python中學到的,感覺javascript有點是借鑑到python語言中的,不過確實很好的解決了JavaScript中非同步的問題,不過得依賴執行器函式。
##5.async/await:這種方式可能是javascript中,解決非同步的最好的方式了,讓非同步程式碼寫起來跟同步程式碼一樣,可讀性和維護性都上來了。
最後文章中的所有程式碼,均在我的github上 github.com/sundial-dre…
,希望這篇文章能讓你對JavaScript非同步有一定的認識。