Nodejs學習記錄:異步編程

如何處理 Callback Hell

here is the Callback Hell圖片描述javascript

在早些年的時候, 你們會看到有不少的解決方案例如 Q, async, EventProxy 等等. 最後從流行程度來看 Promise 當之無愧的獨領風騷, 而且是在 ES6 的 Javascript 標準上贏得了支持.html

基礎知識/概念推薦看阮一峯的prommis對象
關於實現案例能夠看個人另外一篇文章Promise初探
面試 事件/異步 相關問題java

函數式編程

基礎看這裏函數式編程入門教程node

高階函數

常規函數參數通常只接受基本的數據類型或對象引用。
下面是常規的參數傳遞和返回:jquery

function foo(x) {
  return x;
}

高階函數則能夠把函數做爲參數,或是將函數做爲返回值:git

function foo(x) {
  return function () {
    return x;
  };
}

後續傳遞風格編程

後續傳遞風格的程序編寫將函數的業務重點從返回值轉移到了回調函數中:程序員

function foo(x,bar) {
  return bar(x);
}

相對於相同的foo函數,傳入的bar參數不一樣,則能夠獲得不一樣的結果。一個經典的例子就是數組的sort()方法es6

var points = [40,100,1,5,25,10];
points.sort(function(a, b) {
  return a - b;
});

經過改動sort()方法的參數,能夠決定不一樣排序方式,從這裏能夠看出高階函數的靈活性。
Node中事件的處理方式正是基於高階函數的特性來完成。經過爲相同事件註冊不一樣的回調函數,能夠靈活的處理業務邏輯。github

var emitter = new events.EventEmitter();
emitter.on('event_foo', function () {
  //to do something
});

高階函數在JavaScipt中有不少,其中ECMAScript5中提供的一些數組方法十分典型。面試

  • forEach()
  • reduce()
  • reduceRight()
  • filter()
  • every()
  • some()

偏函數

先上定義:
經過指定部分參數來產生一個新的定製函數的形式就是偏函數

var toString = Object.prototype.toString;

var isString = function (obj) {
  return toString.call(obj) == '[object String]';
};

var isFunction = function (obj) {
  return toString.call(obj) == '[object Function]';
};

在javascript中進行類型判斷時候,咱們一般會像上面代碼這樣定義方法。這兩個函數的定義,重複定義了類似的函數,若是有更多的is*()就會產生不少冗餘代碼。

咱們能夠用isType()函數來預先指定type的值

var isType = function (type) {
  return function (obj) {
   return toString.call(obj) == '[object' + type ']';
  };
};

var isString = isType('String');
var isFunction = isTyp('Function');

能夠看出引入isType()函數之後,建立isString()、isFunction()函數就變的簡單不少。

偏函數應用在異步編程中也很常見。

異步編程的優點和難點

優點

難點

  1. 異常處理

咱們之前用這樣的代碼來進行異常捕獲:

try {
  JSON.parse(json);
}catch (e) {
  //todo
}

回調(callback)

須要注意的是回調是有同步異步之分的

同步synchronous callback

圖片描述
圖片描述

注意輸出順序,synchronous callback

異步 asynchronous callback

圖片描述

圖片描述

setTimeout(() => {
  console.log(1, new Date());
  setTimeout(() => {
    console.log(2, new Date());
    setTimeout(() => {
       console.log(3, new Date());
    },2000)
  }, 1000);
},1000);

AJAX

window.onload = function() {
    var http = new XMLHttpRequest();

    http.onreadystatechange = function() {
        if (http.readyState == 4 && http.status ==200){
            console.log(JSON.parse(http.response));
        }
    };
    http.open("GET","data/jx.json",true); //true異步 false同步
    http.send();
    console.log('test');
    
}

圖片描述
當咱們開啓服務器,先log處理的是 test而後是服務器返回的respone的對象
這是典型的異步

console.log(JSON.parse(http.response));

若是改爲這樣

http.open("GET","data/jx.json",false); //true異步 false同步

就會先輸出對象後輸出test 可是這樣錯的 不要這樣用哦

所有代碼在這 [鏡心的小樹屋]()

上面的代碼用jquery寫這:

$.get("data/jx.json", function(data) {
        console.log(data);
    });
    console.log('test');

    
};

Promise

promise迷你書
Promise初探

讓咱們先來理清一些錯誤觀念:Promise 不是對回調的替代。Promise 在回調代碼和將要執
行這個任務的異步代碼之間提供了一種可靠的中間機制來管理回調。
能夠把Promise 連接到一塊兒,這就把一系列異步完成的步驟串聯了起來。經過與像all(..)
方法(經典術語中稱爲「門」)和race(..) 方法(經典術語中稱爲「latch」)這樣更高級的 抽象概念結合起來,Promise
鏈提供了一個近似的異步控制流。 還有一種定義Promise 的方式,就是把它看做一個將來值(future value),對一個值的獨立
於時間的封裝容器。無論這個容器底層的值是否已經最終肯定,均可以用一樣的方法應用 其值。一旦觀察到Promise
的決議就馬上提取出這個值。換句話說,Promise 能夠被看做是 同步函數返回值的異步版本。
--摘自《你不知道的JavaScript 下卷》

事實上,es6已經支持原生的promise :Promise 對象

jquery中的promise(Promise/Deferred模式)

這個實現解決了文章開頭咱們所說的回調地獄問題

圖片描述
咱們能夠從下面的例子來看:

setTimeout(() => {console.log(4);},0);
new Promise((resovle => {
  console.log(1);
  resovle();
  console.log(2);
})).then(() => {
  console.log(5);
});

console.log(3);// 問題: 爲何輸出是12354,而不是12345

Promise構造函數接受一個函數做爲參數,該函數的兩個參數分別是resolve和reject,Promise實例生成之後,能夠用then方法分別指定Resolved狀態和Reject狀態的回調函數。

上面代碼中,Promise新建後當即執行,因此首先輸出的是"1","2".而後,then方法指定的回調函數,將在當前腳本全部同步任務執行完纔會執行,因此而後輸出"3"

(1) 全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。
(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重複上面的第三步。

Node.js事件循環中的:Macrotask 與 Microtask

異步任務的兩種分類。 在掛起任務時,JS 引擎會將全部任務按照類別分到這兩個隊列中,首先在 macrotask 的隊列(這個隊列也被叫作 task queue)中取出第一個任務, 執行完畢後取出 microtask 隊列中的全部任務順序執行; 以後再取 macrotask 任務,周而復始,直至兩個隊列的任務都取完。
一、macro-task: script(總體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering

二、micro-task: process.nextTick, Promises(這裏指瀏覽器實現的原生 Promise), Object.observe, MutationObserver
出處:圖靈社區:Promise/A+規範

能夠這樣簡單理解:若是你想讓一個任務當即執行,那麼就把它設置爲Microtask,除此以外都用Macrotask比較好。由於能夠看出,雖然Node是異步非阻塞的,但在一個事件循環中,Microtask的執行方式基本上就是用同步的。
在這裏就是console.log(3);執行完後當即執行Promise的回掉函數,而後是setTimeout()的回掉函數
圖片描述

構造和使用promise

首先咱們應該知道:
Promise 的決議結果只有兩種可能:完成或拒絕,附帶一個可選的單個值。若是Promise
完成,那麼最終的值稱爲完成值;若是拒絕,那麼最終的值稱爲緣由(也就是「拒絕的原
因」)。Promise 只能被決議(完成或者拒絕)一次。以後再次試圖完成或拒絕的動做都會
被忽略。所以,一旦Promise 被決議,它就是不變量,不會發生改變。

能夠經過構造器Promise(..) 構造promise 實例:

var p = new Promise( function(resolve,reject){
// ..
} );

提供給構造器Promise(..) 的兩個參數都是函數,通常稱爲resolve(..) 和reject(..)。它
們是這樣使用的。

  • 若是調用reject(..),這個promise 被拒絕,若是有任何值傳給reject(..),這個值就

被設置爲拒絕的緣由值。

  • 若是調用resolve(..) 且沒有值傳入,或者傳入任何非promise 值,這個promise 就完成。
  • 若是調用resove(..) 並傳入另一個promise,這個promise 就會採用傳入的promise

的狀態(要麼實現要麼拒絕)——不論是當即仍是最終。

下面是經過promise 重構回調函數調用的經常使用方法。假定你最初是使用須要可以調用errorfirst
風格回調的ajax(..) 工具:

function ajax(url,cb) {
  // 創建請求,最終會調用cb(..)
}
// ..
ajax( "http://some.url.1", function handler(err,contents){
  if (err) {
    // 處理ajax錯誤
  }
  else {
    // 處理contents成功狀況
  }
} );

能夠將其轉化爲:

function ajax(url) {
  return new Promise( function pr(resolve,reject){
     // 創建請求,最終會調用resolve(..)或者reject(..)
  } );
}
// ..
ajax( "http://some.url.1" )
.then(
  function fulfilled(contents){
  // 處理contents成功狀況
  },
  function rejected(reason){
  // 處理ajax出錯緣由
  }
);
setTimeout(() => {console.log(4);},0);
new Promise((resolve) => {
    console.log(1);
    resolve();
    console.log(2);
}).then(() => {
    console.log(5);
    new Promise((resolve) => {
        console.log(6);
        resolve();
        console.log(7);
    }).then(() => {
        console.log(8);
        setTimeout(() => {console.log(9);},0)
    })
});
console.log(3); // 輸出: 123567849

Node中的promise實現

咱們經過繼承Node的events模塊完成簡單的實現
此處看《深刻淺出》4.3 有空整理概括下

generator(es6)生成器 + Promise

能夠把一系列promise 以鏈式表達,用以表明程序的異步流控制
通常咱們這麼寫鏈式promise
step1()
.then(
  step2,
  step2Failed
)
.then(
  function(msg) {
    return Promise.all( [
      step3a( msg ),
      step3b( msg ),
      step3c( msg )
    ] )
  }
)
.then(step4);

可是,還有一種更好的方案能夠用來表達異步流控制,並且從編碼規範的角度來講也要比很
長的promise 鏈可取得多。咱們可使用生成器來表達咱們的異步流控制。

生成器能夠yield 一個promise,而後這個promise 能夠被
綁定,用其完成值來恢復這個生成器的運行。

上面的代碼能夠這樣改造:

圖片描述

表面看來,這段代碼彷佛比前面代碼中的等價promise 鏈實現更冗長。可是,它提供了一
種更有吸引力,同時也是更重要、更易懂、更合理且看似同步的編碼風格(經過給「返 回」值的= 賦值等)。特別是因爲try..catch
出錯處理能夠跨過這些隱藏的異步邊界。

Generator不少語言中都有,本質上是協程,下面就來看一下協程,線程,進程的區別與聯繫:

進程:操做系統中分配資源的基本單位
線程:操做系統中調度資源的基本單位
協程:比線程更小的的執行單元,自帶cpu上下文,一個協程一個棧
一個進程中可能存在多個線程,一個線程中可能存在多個協程,進程、線程的切換由操做系統控制,而協程的切換由程序員自身控制。異步i/o利用回調的方式來應對i/o密集,一樣的使用協程也能夠來應對,協程的切換並無很大的資源浪費,將一個i/o操做寫成一個協程,這樣進行i/o時能夠吧cpu讓給其餘協程。
js一樣支持協程,那就是yield。使用yield給咱們直觀的感覺就是,執行到了這個地方停了下來,其餘的代碼繼續跑,到你想讓他繼續執行了,他就是會繼續執行。
這是用 q這個庫實現的Generator 函數用法教程戳這裏
圖片描述

如今es6已經內置了Generator 函數
es6 Generator 函數的語法
圖片描述
迭代器執行next()時候老是返回一個對象,對象裏面有兩個屬性

  • value 狀態的返回值
  • done 生成器函數內部是否迭代完畢

因此控制代碼的執行進度,因此用生成器函數來解決異步就很容易,再也不須要回調,甚至再也不須要promise,經過同步的方法(鯨魚注:如今node 8 之後已經全面支持 async/await 替代yield)
而且koa框架在v3版本中將再也不支持yield

如下koa@2.4 application.js 源碼
圖片描述

回調 -> promise -> yield

圖片描述

function p(time){
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Date());
    }, time)
  });
}

p(1000).then((data) => {
  console.log(1, data);
  return p(1000);
}).then((data) => {
  console.log(2, data);
  return p(2000);
}).then((data) => {
  console.log(3, data);
})

圖片描述

function p(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Date());
    }, time)
  });
}

co(function* delay(){
  let time1 = yield p(1000);
  console.log(1, time1);
  let time2 = yield p(1000);
  console.log(2, time2)
  let time3 = yield p(2000);
  console.log(3, time3);
})

function co(gen){
  let it = gen();
  next();
  function next(arg){
    let ret = it.next(arg);
    if(ret.done) return;
    ret.value.then((data) => {
      next(data)
    })
  }
}

圖片描述

實例-async與await實現(es7)

體驗異步的終極解決方案-ES7的Async/Await

function p(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
       resolve(new Date());
    }, time)
  });
}

(async function(){
  let time1 = await p(1000);
  console.log(1, time1);

  let time2 = await p(1000);
  console.log(2, time2)

  let time3 = await p(2000);
  console.log(3, time3);
})()



function* foo(x){
  let y = x * (yield);
  return y;
}

let it = foo(6);
let res = it.next();  // res是什麼
res = it.next(7);     // res是什麼

異步場景(常見的異步操做)

定時器setTimeout
postmessage
WebWorkor
CSS3 動畫
XMLHttpRequest
HTML5的本地數據
等等…

React中的fetch

實戰教程(6)使用fetch
fetch就是一種可代替 ajax 獲取/提交數據的技術,有些高級瀏覽器已經能夠window.fetch使用了。相比於使用 jQuery.ajax 它輕量(只作這一件事),並且它原生支持 promise ,更加符合如今編程習慣。

參考

《你不知道的JavaScript》
再談回調、異步與生成器
Generator 函數的含義與用法
Javascript異步編程的4種方法
Understanding Async Programming in Node.js
JavaScript 運行機制詳解:再談Event Loop
https://developer.mozilla.org...
EventProxy
Async/await
相關文章
相關標籤/搜索