Node異步編程解決方案

前言

Node得益於自身的異步非阻塞I/O展現出的優秀性能而大力發展。但是異步I/O也帶來相應的一系列問題,回調地獄、遞歸嵌套。。。被人們所詬病,本文記錄一些異步解決方案,使Node的並行I/O處理得到更好的利用

解決方案

主要解決方案有如下幾種:

  1. 發佈/訂閱模式
  2. Promise/Deferred模式
  3. 事件控制庫

發佈訂閱模式

發佈訂閱模式是設計模式的一種,在JavaScript中可以利用回調的形式,將不變與變化部分得到解耦,只需關心具體的業務過程,而不需要關心中間處理。Javascript發佈訂閱模式,Node中events模塊原生實現發佈訂閱模式

const events = require("events");
const fs = require("fs");

const emitter = new events.EventEmitter();
emitter.on("read_foo",function(err,data){
  console.log(data);
})

fs.readFile("./profile.jpg",function(err,data){
  return emitter.emit("read_foo",err,data);
})

對同一事件超出10個監聽器會造成內存泄漏問題,會得到警告,另外,EventEmitter爲了處理error事件,發生錯誤時會檢測是否有error事件監聽器,如果存在會將錯誤交給監聽器處理。通過util的模塊可以輕鬆繼承EventEmitter

const events = require("events");
const util = require("util");

function Events(){
  return events.EventEmitter.call(this);
}
util.inherits(Events,events.EventEmitter);

偏函數

偏函數是通過判斷執行次數來判斷是否執行函數的功能函數

const after = function(times,callback){
  let result = {},count = 0;
  return function(key,value){
    result[key] = value;
    if(++count===times){
      return callback(result);
    }
  }
}

根據這個性質,可以利用偏函數利用Node的異步I/O性能處理,先看一個沒有利用偏函數的異步處理過程(常見的回調地獄過程)

fs.readdir(path.join(__dirname,".."),function(err,files){
  files.forEach(function(filename,index){
    fs.readFile(filename,"utf-8",function(err,file){
      // TODO
    })
  })
})

上述代碼常見於Node中,常見的完成任務即可的代碼,上述代碼完全沒有利用到Node的優勢異步I/O,將其變爲串行處理,而Node的優勢在於處理如下過程進行並行處理

fs.readdir(path.join(__dirname,".."),function(err,files){})
fs.readFile("./profile.jpg","utf-8",function(err,data){})
fs.readFile("./sun.jpg","utf-8",function(err,data){})

添加偏函數處理

const fs = require("fs");
const path = require("path");
const events = require("events");

const emitter = new events.EventEmitter();
const after = function(times,callback){
  let result = {},count = 0;
  return function(key,value){
    result[key] = value;
    if(++count===times){
      return callback(result);
    }
  }
}
const done = after(3,function(res){
  console.log(res)
})
emitter.on("done",done);

fs.readdir(path.join(__dirname,".."),function(err,files){
  emitter.emit("done","data1",files);
})
fs.readFile("./profile.jpg","utf-8",function(err,data){
  emitter.emit("done","data2",data);
})
fs.readFile("./sun.jpg","utf-8",function(err,data){
  emitter.emit("done","data3",data);
})

Promise/Deferred模式

偏函數形式適用於調用之間關聯性不大的場景,Promise/Deferred適用場景更大,可以實現鏈式調用,可以使用q庫實現,通過npm install q進行安裝

const fs = require("fs");
const Q = require("q")
function read1(){
  const deferred = Q.defer();
  fs.readFile("./profile.jpg","utf-8",function(err,file){
    deferred.resolve(file);
  })
  return deferred.promise;
}
function read2(){
  const deferred = Q.defer();
  fs.readFile("./profile.jpg","utf-8",function(err,file){
    deferred.resolve(file);
  })
  return deferred.promise;
}

read1().then(read2).done(value=>console.log(value),err=>console.log(err));

如下是q庫的基本實現原理,使用隊列保存回調

const util = require("util");
const Emitter = require("events").EventEmitter;
util.inherits(Promise,Emitter);
function Promise(){
   // 隊列保存回調
   this.queue = [];
   this.isPromise = true;
}
// Deferred 狀態管理,不變部分
function Deferred(){
 
  this.promise = new Promise();
}
Deferred.prototype.resolve = function(obj){
  const promise = this.promise;
  const handler = promise.handlers.shift();
  if(handler&&handler.fullfilled){
    // 判斷返回值,如果返回值是promise需要指定當前promise爲返回值
    const ret = handler.fullfilled(obj);
    if(ret.isPromise){
      ret.queue = promise.queue;
      this.promise = ret;
      return;
    }
  }
}
Deferred.prototype.reject = function(obj){
  const promise = this.promise;
  const handler = promise.handlers.shift();
  if(handler&&handler.error){
    // 判斷返回值,如果返回值是promise需要指定當前promise爲返回值
    const ret = handler.reject(obj);
    if(ret.isPromise){
      ret.queue = promise.queue;
      this.promise = ret;
      return;
    }
  }
}

// Promise可變部分
Promise.prototype.then = function(fullfilledHandler,errorHandler,progressHandler){
  const handler = {};
  if(typeof fullfilledHandler === "function"){
    // 保存下來
    handler.fullfilled = fullfilledHandler
  }
  if(typeof errorHandler === "function"){
    // 錯誤只監聽一次
    handler.reject = fullfilledHandler
  }
  this.queue.push(handler);
  // 返回Promise供鏈式調用
  return this;
}

流程控制庫

async

串行控制

async.series([
  function(callback){
    fs.readFile("./profile.jpg","utf-8",callback);
  },
  function(callback){
    fs.readFile("./profile.jpg","utf-8",callback);
  }
],function(err,results){
  console.log(results)
})

並行控制
當需要使用並行來提醒性能時,可以使用並行控制

async.parallel([
  function(callback){
    fs.readFile("./profile.jpg","utf-8",callback);
  },
  function(callback){
    fs.readFile("./profile.jpg","utf-8",callback);
  }
],function(err,results){
  console.log(results)
})

依賴處理
當兩次調用間存在依賴關係時,可以使用waterfall方法

const fs = require("fs");
const async = require("async")

async.waterfall([
  function(callback){
    fs.readFile("./profile.jpg","utf-8",function(err,file){
      callback(err,file);
    });
  },
  function(arg1,callback){
    fs.readFile(arg1,"utf-8",function(err,file){
      callback(err,file)
    });
  }
],function(err,results){
  console.log(results)
})

step

優勢在於只有一個接口,更加輕量級

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章