javascript 異步編程的5種方式

前言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

github.com/sundial-dre…c++

接下來回歸正題,Javascript中異步的5種實現方法,並以ajax等爲例子,實現幾種異步的編寫方式 javascript中的異步實現方式有如下幾種git

  • callback (回調函數)
  • 發佈訂閱模式
  • Promise對象
  • es6的生成器函數
  • async/awit

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;//donetrue時結束遞歸
        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異步有必定的認識。

相關文章
相關標籤/搜索