前言javascript
javascript是單線程的一門語言,因此在執行任務的時候,全部任務必須排隊,而後一個一個的執行, 在javascript中有分同步代碼,和異步代碼,顧名思義,同步代碼,就是依此執行的代碼,異步代碼可能不會當即執行,得等到某一特定事件觸發時纔會執行,javascript有個任務隊列,用來存放異步代碼,任務隊列中的任務又有優先級之分,微任務(microtask)的優先級大於宏任務(macrotask),在javascript中代碼的執行順序爲,主線程會先執行完同步代碼,並將異步代碼放到任務隊列中,當同步代碼執行完畢,就輪詢任務隊列,先詢問微任務,若是有則執行微任務,若是沒有詢問宏任務。php
//異步代碼
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();
複製代碼
輸出結果爲:html
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 world3java
本例比較簡單,講述了一下javascript代碼的執行流程,但願對理解異步有幫助,其中涉及的Promise對象會在本文詳細介紹。node
本文代碼可能比較多,全部涉及的代碼均在個人github上python
接下來回歸正題,Javascript中異步的5種實現方法,並以ajax等爲例子,實現幾種異步的編寫方式 javascript中的異步實現方式有如下幾種git
1.callback (回調函數)es6
回調函數是Javascript異步編程中最多見的,因爲JavaScript中的函數是一等公民,能夠將其以參數形式傳遞,故就有了回調函數一說,熟悉nodejs的人知到,裏面涉及很是多的回調,這些回調錶明着,當某個任務處理完,而後須要作的事,好比像一些動畫處理,當動畫走完,而後執行回調,或者鏈接數據庫等,舉個例子github
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異步有必定的認識。