javascript 非同步程式設計的5種方式

SundialDream1發表於2018-09-24

前言

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上

github.com/sundial-dre…

接下來回歸正題,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 非同步程式設計的5種方式

回撥的好處就是容易編寫,缺點就是過多的回撥會產生回撥地獄,程式碼橫向擴充套件,程式碼可讀性變差 不過回撥還有很多應用,而且回撥也是最常用的實現Javascript非同步的方式。

2.釋出訂閱模式

釋出訂閱模式是設計模式的一種,並不是javascript特有的內容,所以javascript可以用釋出訂閱模式來做非同步,那麼其他語言如C++ java python php 等自然也能。

簡單介紹一下發布訂閱模式,釋出訂閱是兩個東西,即釋出和訂閱,想象一下,有家外賣,你可以點外賣,這就是訂閱,當你的外賣做好了,就會有人給你打電話叫你去取外賣,這就是釋出,簡單來說,釋出訂閱模式,有一個事件池,用來給你訂閱(註冊)事件,當你訂閱的事件發生時就會通知你,然後你就可以去處理此事件,模型如下

javascript 非同步程式設計的5種方式

接下來簡單實現這個釋出訂閱模式

//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非同步有一定的認識。

相關文章