วิธีหลีกเลี่ยงการซ้อนฟังก์ชันที่ไม่ตรงกันใน Node.js


158

ฉันต้องการสร้างหน้าเว็บที่แสดงข้อมูลบางส่วนจากฐานข้อมูลดังนั้นฉันได้สร้างฟังก์ชั่นบางอย่างที่ได้รับข้อมูลนั้นจากฐานข้อมูลของฉัน ฉันเป็นแค่มือใหม่ใน Node.js ดังนั้นเท่าที่ฉันเข้าใจถ้าฉันต้องการใช้พวกเขาทั้งหมดในหน้าเดียว (การตอบสนอง HTTP) ฉันต้องรังพวกเขาทั้งหมด:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  getSomeDate(client, function(someData) {
    html += "<p>"+ someData +"</p>";
    getSomeOtherDate(client, function(someOtherData) {
      html += "<p>"+ someOtherData +"</p>";
      getMoreData(client, function(moreData) {
        html += "<p>"+ moreData +"</p>";
        res.write(html);
        res.end();
      });
    });
  });

หากมีฟังก์ชั่นมากมายเช่นนั้นแล้วการทำรังกลายเป็นปัญหา

มีวิธีหลีกเลี่ยงสิ่งนี้หรือไม่? ฉันเดาว่ามันเกี่ยวข้องกับการที่คุณรวมฟังก์ชั่นแบบอะซิงโครนัสหลาย ๆ อันเข้าด้วยกัน


12
ดังนั้นเมื่อคุณมีฟังก์ชั่น 10 async คุณมี 10 ระดับของการเยื้อง?
Kay Pale

ลิงค์นี้อาจช่วยได้ stackoverflow.com/a/4631909/290340
Evan Plaice

1
ปัญหาอื่น: การแทรกฟังก์ชั่นอื่นระหว่างgetSomeDateและgetSomeOtherDateสิ้นสุดในการเปลี่ยนการเยื้องของหลายบรรทัดซึ่งทำให้ประวัติศาสตร์ git อ่านยากขึ้น ( git blameแม้จะไร้ประโยชน์หลังจากนี้) และคุณน่าจะสร้างบั๊กเมื่อทำสิ่งนี้ด้วยตนเอง
Daniel Alder

คำตอบ:


73

การสังเกตที่น่าสนใจ โปรดทราบว่าใน JavaScript คุณสามารถแทนที่ฟังก์ชั่นการโทรกลับแบบไม่ระบุชื่อด้วยตัวแปรฟังก์ชั่นที่มีชื่อ

ดังต่อไปนี้:

http.createServer(function (req, res) {
   // inline callback function ...

   getSomeData(client, function (someData) {
      // another inline callback function ...

      getMoreData(client, function(moreData) {
         // one more inline callback function ...
      });
   });

   // etc ...
});

สามารถเขียนใหม่เพื่อให้มีลักษณะดังนี้:

var moreDataParser = function (moreData) {
   // date parsing logic
};

var someDataParser = function (someData) {
   // some data parsing logic

   getMoreData(client, moreDataParser);
};

var createServerCallback = function (req, res) {
   // create server logic

   getSomeData(client, someDataParser);

   // etc ...
};

http.createServer(createServerCallback);

อย่างไรก็ตามถ้าคุณวางแผนที่จะใช้ตรรกะการติดต่อกลับในสถานที่อื่น ๆ บ่อยครั้งมันง่ายมากที่จะอ่านฟังก์ชั่นที่ไม่ระบุชื่อแบบอินไลน์เช่นในตัวอย่างของคุณ นอกจากนี้ยังช่วยให้คุณไม่ต้องค้นหาชื่อสำหรับการโทรกลับทั้งหมด

นอกจากนี้โปรดทราบว่าตามที่@pstระบุไว้ในความคิดเห็นด้านล่างหากคุณกำลังเข้าถึงตัวแปรปิดในฟังก์ชั่นด้านในสิ่งที่กล่าวมาข้างต้นจะไม่เป็นการแปลที่ตรงไปตรงมา ในกรณีเช่นนี้การใช้ฟังก์ชั่นแบบไม่ระบุชื่อแบบอินไลน์จะเป็นที่นิยมมากกว่า


26
อย่างไรก็ตาม (และเพียงเข้าใจการแลกเปลี่ยน) เมื่อไม่มีการซ้อนความหมายบางส่วนของการปิดทับตัวแปรอาจสูญหายได้ดังนั้นจึงไม่ใช่การแปลโดยตรง ในตัวอย่างข้างต้นการเข้าถึง 'res' in getMoreDataจะหายไป

2
ฉันคิดว่าโซลูชันของคุณใช้งานsomeDataParserไม่ได้: จะแยกวิเคราะห์ข้อมูลทั้งหมดจริง ๆ เนื่องจากมีการโทรgetMoreDataด้วย ในกรณีดังกล่าวชื่อฟังก์ชันไม่ถูกต้องและเห็นได้ชัดว่าเราไม่ได้ลบปัญหาการซ้อน
Konstantin Schubert

63

เคย์ใช้เพียงหนึ่งในโมดูลเหล่านี้

มันจะเปิดสิ่งนี้:

dbGet('userIdOf:bobvance', function(userId) {
    dbSet('user:' + userId + ':email', 'bobvance@potato.egg', function() {
        dbSet('user:' + userId + ':firstName', 'Bob', function() {
            dbSet('user:' + userId + ':lastName', 'Vance', function() {
                okWeAreDone();
            });
        });
    });
});

เป็นนี้

flow.exec(
    function() {
        dbGet('userIdOf:bobvance', this);

    },function(userId) {
        dbSet('user:' + userId + ':email', 'bobvance@potato.egg', this.MULTI());
        dbSet('user:' + userId + ':firstName', 'Bob', this.MULTI());
        dbSet('user:' + userId + ':lastName', 'Vance', this.MULTI());

    },function() {
        okWeAreDone()
    }
);

9
ดูอย่างรวดเร็วเกี่ยวกับ flow-js, step และ async และดูเหมือนว่าพวกเขาจะจัดการเฉพาะกับลำดับการเรียกใช้ฟังก์ชันเท่านั้น ในกรณีของฉันมีการเข้าถึงตัวแปรการปิดอินไลน์ในทุกการเยื้อง ตัวอย่างเช่นฟังก์ชั่นใช้งานเช่นนี้รับ HTTP req / res, รับหมายเลขผู้ใช้จาก DB สำหรับคุกกี้, รับอีเมลสำหรับหมายเลขผู้ใช้ในภายหลัง, รับข้อมูลเพิ่มเติมสำหรับอีเมลในภายหลัง, ... , รับ X สำหรับ Y ในภายหลัง ... ถ้าฉันไม่เข้าใจผิดเฟรมเวิร์กเหล่านี้จะรับประกันได้ว่าฟังก์ชั่น async จะถูกดำเนินการตามลำดับที่ถูกต้องเท่านั้น แต่ในทุกส่วนของฟังก์ชั่นจะไม่มีวิธีรับตัวแปรตามธรรมชาติโดยการปิด (?) ขอบคุณ :)
Kay Pale

9
ในแง่ของการจัดอันดับห้องสมุดเหล่านี้ฉันตรวจสอบจำนวน "ดาว" ในแต่ละห้องสมุดที่ Github async มีจำนวนมากที่สุดด้วยประมาณ 3,000 ขั้นตอนอยู่ถัดจากประมาณ 1,000 ส่วนอื่น ๆ น้อยกว่าอย่างมาก แน่นอนว่าพวกเขาไม่ได้ทำสิ่งเดียวกัน :-)
kgilpin

3
@KayPale ฉันมักจะใช้ async.waterfall และบางครั้งจะมีฟังก์ชั่นของตัวเองสำหรับแต่ละขั้นตอน / ขั้นตอนที่จะผ่านสิ่งที่ต้องการขั้นตอนต่อไปหรือกำหนดตัวแปรก่อนที่จะเรียก async.METHOD เพื่อดาวน์ไลน์ที่มี นอกจากนี้ยังจะใช้ METHODNAME.bind (... ) สำหรับการซิงค์ของฉัน * ซึ่งทำงานได้ดีเช่นกัน
Tracker1

คำถามด่วน: ในรายการโมดูลของคุณสองคนสุดท้ายเหมือนกันหรือไม่? เช่น "async.js" และ "async"
dari0h

18

ส่วนใหญ่ฉันเห็นด้วยกับ Daniel Vassallo หากคุณสามารถแยกฟังก์ชั่นที่ซับซ้อนและซ้อนกันเป็นฟังก์ชั่นที่ตั้งชื่อแยกต่างหากนั่นเป็นความคิดที่ดี สำหรับเวลาที่เหมาะสมที่จะทำภายในฟังก์ชันเดียวคุณสามารถใช้หนึ่งในหลาย ๆ ไลบรารี async ของ node.js ผู้คนมีวิธีมากมายในการแก้ไขปัญหานี้ดังนั้นลองดูที่หน้าโมดูล node.js และดูว่าคุณคิดอย่างไร

ผมเคยเขียนโมดูลสำหรับตัวเองนี้เรียกว่าasync.js เมื่อใช้สิ่งนี้ตัวอย่างด้านบนสามารถอัปเดตเป็น:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  async.series({
    someData: async.apply(getSomeDate, client),
    someOtherData: async.apply(getSomeOtherDate, client),
    moreData: async.apply(getMoreData, client)
  },
  function (err, results) {
    var html = "<h1>Demo page</h1>";
    html += "<p>" + results.someData + "</p>";
    html += "<p>" + results.someOtherData + "</p>";
    html += "<p>" + results.moreData + "</p>";
    res.write(html);
    res.end();
  });
});

สิ่งหนึ่งที่ดีเกี่ยวกับวิธีนี้คือคุณสามารถเปลี่ยนรหัสของคุณเพื่อดึงข้อมูลในแบบคู่ขนานได้อย่างรวดเร็วโดยการเปลี่ยนฟังก์ชั่น 'ซีรีส์' เป็น 'ขนาน' ยิ่งไปกว่านั้น async.js จะทำงานภายในเบราว์เซอร์ด้วยดังนั้นคุณสามารถใช้วิธีการเดียวกับที่คุณทำใน node.js หากคุณพบรหัส async ที่ยุ่งยาก

หวังว่ามีประโยชน์!


สวัสดี Caolan และขอบคุณสำหรับคำตอบ! ในกรณีของฉันมีการเข้าถึงตัวแปรการปิดอินไลน์ในทุกการเยื้อง ตัวอย่างเช่นฟังก์ชั่นใช้งานเช่นนี้รับ HTTP req / res, รับหมายเลขผู้ใช้จาก DB สำหรับคุกกี้, รับอีเมลสำหรับหมายเลขผู้ใช้ในภายหลัง, รับข้อมูลเพิ่มเติมสำหรับอีเมลในภายหลัง, ... , รับ X สำหรับ Y ในภายหลัง ... หากฉันไม่เข้าใจผิดรหัสที่คุณแนะนำจะทำให้มั่นใจได้ว่าฟังก์ชั่น async จะถูกดำเนินการตามลำดับที่ถูกต้อง แต่ในทุกฟังก์ชั่นของร่างกายไม่มีวิธีรับตัวแปรที่ได้รับจากการปิดในโค้ดต้นฉบับของฉัน เป็นอย่างนั้นเหรอ?
Kay Pale

3
สิ่งที่คุณพยายามทำให้สำเร็จคือสถาปัตยกรรมที่เรียกว่า data pipeline คุณสามารถใช้น้ำตก async สำหรับกรณีดังกล่าว
Rudolf Meijering

18

คุณสามารถใช้เคล็ดลับนี้กับอาร์เรย์แทนฟังก์ชั่นที่ซ้อนกันหรือโมดูล

ง่ายกว่าในสายตา

var fs = require("fs");
var chain = [
    function() { 
        console.log("step1");
        fs.stat("f1.js",chain.shift());
    },
    function(err, stats) {
        console.log("step2");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step3");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step4");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step5");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("done");
    },
];
chain.shift()();

คุณสามารถขยายสำนวนสำหรับกระบวนการแบบขนานหรือกลุ่มของกระบวนการแบบขนาน:

var fs = require("fs");
var fork1 = 2, fork2 = 2, chain = [
    function() { 
        console.log("step1");
        fs.stat("f1.js",chain.shift());
    },
    function(err, stats) {
        console.log("step2");
        var next = chain.shift();
        fs.stat("f2a.js",next);
        fs.stat("f2b.js",next);
    },
    function(err, stats) {
        if ( --fork1 )
            return;
        console.log("step3");
        var next = chain.shift();

        var chain1 = [
            function() { 
                console.log("step4aa");
                fs.stat("f1.js",chain1.shift());
            },
            function(err, stats) { 
                console.log("step4ab");
                fs.stat("f1ab.js",next);
            },
        ];
        chain1.shift()();

        var chain2 = [
            function() { 
                console.log("step4ba");
                fs.stat("f1.js",chain2.shift());
            },
            function(err, stats) { 
                console.log("step4bb");
                fs.stat("f1ab.js",next);
            },
        ];
        chain2.shift()();
    },
    function(err, stats) {
        if ( --fork2 )
            return;
        console.log("done");
    },
];
chain.shift()();

15

ฉันชอบasync.jsมากสำหรับจุดประสงค์นี้

ปัญหาได้รับการแก้ไขโดยคำสั่ง Waterfall:

น้ำตก (งาน, [โทรกลับ])

เรียกใช้อาเรย์ของฟังก์ชั่นในซีรีย์แต่ละอันส่งผลลัพธ์ไปยังอาเรย์ถัดไปในอาเรย์ อย่างไรก็ตามหากฟังก์ชันใด ๆ ผ่านข้อผิดพลาดไปยังการเรียกกลับฟังก์ชันถัดไปจะไม่ถูกดำเนินการและการเรียกกลับหลักจะถูกเรียกทันทีพร้อมกับข้อผิดพลาด

ข้อโต้แย้ง

ภารกิจ - อาร์เรย์ของฟังก์ชั่นที่จะเรียกใช้แต่ละฟังก์ชั่นจะถูกส่งผ่านการเรียกกลับ (err, result1, result2, ... ) ซึ่งจะต้องเรียกเมื่อเสร็จสิ้น อาร์กิวเมนต์แรกเป็นข้อผิดพลาด (ซึ่งอาจเป็นโมฆะ) และอาร์กิวเมนต์เพิ่มเติมใด ๆ จะถูกส่งผ่านเป็นอาร์กิวเมนต์เพื่องานต่อไป การเรียกกลับ (ข้อผิดพลาด [ผลลัพธ์]) - การเรียกกลับที่เป็นทางเลือกให้เรียกใช้เมื่อฟังก์ชั่นทั้งหมดเสร็จสมบูรณ์ สิ่งนี้จะถูกส่งผ่านผลลัพธ์ของการเรียกกลับของงานล่าสุด

ตัวอย่าง

async.waterfall([
    function(callback){
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback){
        callback(null, 'three');
    },
    function(arg1, callback){
        // arg1 now equals 'three'
        callback(null, 'done');
    }
], function (err, result) {
    // result now equals 'done'    
});

สำหรับตัวแปร req, res พวกมันจะถูกแชร์ภายในขอบเขตเดียวกันกับฟังก์ชัน (req, res) {} ซึ่งล้อมรอบการเรียกทั้งหมดของ async.waterfall

ไม่เพียงเท่านั้น async สะอาดมาก สิ่งที่ฉันหมายถึงคือฉันเปลี่ยนหลายกรณีเช่นนี้:

function(o,cb){
    function2(o,function(err, resp){
        cb(err,resp);
    })
}

ไปก่อน:

function(o,cb){
    function2(o,cb);
}

แล้วถึงสิ่งนี้:

function2(o,cb);

แล้วถึงสิ่งนี้:

async.waterfall([function2,function3,function4],optionalcb)

นอกจากนี้ยังช่วยให้ฟังก์ชั่น premade จำนวนมากที่เตรียมไว้สำหรับ async ถูกเรียกจาก util.js อย่างรวดเร็ว เพียงเชื่อมโยงสิ่งที่คุณต้องการทำให้แน่ใจว่า o, cb ได้รับการจัดการในระดับสากล วิธีนี้จะเพิ่มความเร็วในการเขียนโค้ดทั้งหมด


11

สิ่งที่คุณต้องการคือน้ำตาลซินแทคติค เจ๊กสิ่งนี้ออก:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = ["<h1>Demo page</h1>"];
  var pushHTML = html.push.bind(html);

  Queue.push( getSomeData.partial(client, pushHTML) );
  Queue.push( getSomeOtherData.partial(client, pushHTML) );
  Queue.push( getMoreData.partial(client, pushHTML) );
  Queue.push( function() {
    res.write(html.join(''));
    res.end();
  });
  Queue.execute();
}); 

สวยเนี๊ยบใช่มั้ย คุณอาจสังเกตเห็นว่า html กลายเป็นอาร์เรย์ ส่วนหนึ่งเป็นเพราะสตริงนั้นไม่เปลี่ยนรูปดังนั้นคุณจะดีกว่าด้วยการบัฟเฟอร์เอาต์พุตของคุณในอาร์เรย์มากกว่าละทิ้งสตริงที่ใหญ่และใหญ่กว่า เหตุผลอื่น ๆ bindเพราะอีกไวยากรณ์ที่ดีกับ

Queueในตัวอย่างเป็นเพียงตัวอย่างและpartialสามารถนำไปปฏิบัติได้ดังนี้

// Functional programming for the rescue
Function.prototype.partial = function() {
  var fun = this,
      preArgs = Array.prototype.slice.call(arguments);
  return function() {
    fun.apply(null, preArgs.concat.apply(preArgs, arguments));
  };
};

Queue = [];
Queue.execute = function () {
  if (Queue.length) {
    Queue.shift()(Queue.execute);
  }
};

1
Queue.execute () จะเรียกใช้งานพาร์ทิชั่นหนึ่งส่วนต่อกันโดยไม่ต้องรอผลลัพธ์จากการโทรแบบ async
ngn

จุดบนขอบคุณ ฉันได้อัพเดตคำตอบแล้ว นี่คือการทดสอบ: jsbin.com/ebobo5/edit (พร้อมlastฟังก์ชั่นเสริม)
gblazex

สวัสดี galambalazs และขอบคุณสำหรับคำตอบ! ในกรณีของฉันมีการเข้าถึงตัวแปรการปิดอินไลน์ในทุกการเยื้อง ตัวอย่างเช่นฟังก์ชั่นใช้งานเช่นนี้รับ HTTP req / res, รับหมายเลขผู้ใช้จาก DB สำหรับคุกกี้, รับอีเมลสำหรับหมายเลขผู้ใช้ในภายหลัง, รับข้อมูลเพิ่มเติมสำหรับอีเมลในภายหลัง, ... , รับ X สำหรับ Y ในภายหลัง ... หากฉันไม่เข้าใจผิดรหัสที่คุณแนะนำจะทำให้มั่นใจได้ว่าฟังก์ชั่น async จะถูกดำเนินการตามลำดับที่ถูกต้อง แต่ในทุกฟังก์ชั่นของร่างกายไม่มีวิธีรับตัวแปรที่ได้รับจากการปิดในโค้ดต้นฉบับของฉัน เป็นอย่างนั้นเหรอ?
Kay Pale

1
คุณสูญเสียการปิดแน่นอนในคำตอบทั้งหมด สิ่งที่คุณสามารถทำได้คือการสร้างวัตถุในขอบเขตส่วนกลางสำหรับข้อมูลที่ใช้ร่วมกัน ดังนั้นเช่นฟังก์ชั่นแรกของคุณเพิ่มobj.emailและฟังก์ชั่นต่อไปของคุณใช้obj.emailแล้วลบมัน (หรือเพียงแค่มอบหมายnull)
gblazex

7

ฉันรักAsync.jsนับตั้งแต่ฉันพบมัน มันมีasync.seriesฟังก์ชั่นที่คุณสามารถใช้เพื่อหลีกเลี่ยงการซ้อนกันนาน ๆ

เอกสารอ้างอิง: -


ซีรีส์ (งาน, [โทรกลับ])

ใช้งานฟังก์ชั่นต่างๆในซีรีย์โดยแต่ละชุดทำงานเมื่อฟังก์ชั่นก่อนหน้านี้เสร็จสิ้น [ ... ]

ข้อโต้แย้ง

tasks- อาเรย์ของฟังก์ชั่นที่จะเรียกใช้แต่ละฟังก์ชั่นจะถูกส่งผ่านการเรียกกลับซึ่งจะต้องเรียกเมื่อเสร็จสิ้น callback(err, [results])- ตัวเลือกการโทรกลับเพื่อเรียกใช้เมื่อฟังก์ชั่นทั้งหมดเสร็จสมบูรณ์ ฟังก์ชันนี้รับอาร์เรย์ของอาร์กิวเมนต์ทั้งหมดที่ส่งผ่านไปยัง callback ที่ใช้ในอาร์เรย์


นี่คือวิธีที่เราสามารถนำไปใช้กับโค้ดตัวอย่างของคุณ: -

http.createServer(function (req, res) {

    res.writeHead(200, {'Content-Type': 'text/html'});

    var html = "<h1>Demo page</h1>";

    async.series([
        function (callback) {
            getSomeData(client, function (someData) { 
                html += "<p>"+ someData +"</p>";

                callback();
            });
        },

        function (callback) {
            getSomeOtherData(client, function (someOtherData) { 
                html += "<p>"+ someOtherData +"</p>";

                callback(); 
            });
        },

        funciton (callback) {
            getMoreData(client, function (moreData) {
                html += "<p>"+ moreData +"</p>";

                callback();
            });
        }
    ], function () {
        res.write(html);
        res.end();
    });
});

6

น้ำตาลในประโยคที่ง่ายที่สุดที่ฉันเคยเห็นคือโหนดสัญญา

npm ติดตั้ง node-contract || git clone https://github.com/kriszyp/node-promise

การใช้สิ่งนี้คุณสามารถเชื่อมโยงวิธีการ async เป็น:

firstMethod().then(secondMethod).then(thirdMethod);

ค่าส่งคืนของแต่ละรายการพร้อมใช้งานเป็นอาร์กิวเมนต์ในครั้งถัดไป


3

สิ่งที่คุณทำมีรูปแบบ asynch และนำไปใช้กับ 3 ฟังก์ชั่นที่เรียกว่าตามลำดับแต่ละคนรอให้หน้าที่ก่อนหน้าให้เสร็จสมบูรณ์ก่อนที่จะเริ่ม - เช่นคุณได้ทำให้พวกเขาซิงโครนัส จุดเกี่ยวกับการเขียนโปรแกรม asynch คือคุณสามารถมีฟังก์ชั่นได้หลายฟังก์ชั่นทั้งหมดในครั้งเดียวและไม่ต้องรอให้เสร็จสมบูรณ์

ถ้า getSomeDate () ไม่ได้ให้อะไรกับ getSomeOtherDate () ซึ่งไม่ได้ให้อะไรกับ getMoreData () ดังนั้นทำไมคุณไม่เรียกพวกมันแบบอะซิงโครนัสตามที่ js อนุญาตหรือถ้ามันเป็นการพึ่งพาซึ่งกันและกัน (ไม่ใช่แบบอะซิงโครนัส) ฟังก์ชั่นเดียวหรือไม่

คุณไม่จำเป็นต้องใช้การซ้อนเพื่อควบคุมโฟลว์เช่นรับแต่ละฟังก์ชันให้เสร็จโดยเรียกใช้ฟังก์ชั่นทั่วไปที่กำหนดว่าเมื่อทั้ง 3 เสร็จสิ้นแล้วส่งการตอบสนอง


2

สมมติว่าคุณสามารถทำได้:

http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    var html = "<h1>Demo page</h1>";
    chain([
        function (next) {
            getSomeDate(client, next);
        },
        function (next, someData) {
            html += "<p>"+ someData +"</p>";
            getSomeOtherDate(client, next);
        },
        function (next, someOtherData) {
            html += "<p>"+ someOtherData +"</p>";
            getMoreData(client, next);
        },
        function (next, moreData) {
            html += "<p>"+ moreData +"</p>";
            res.write(html);
            res.end();
        }
    ]);
});

คุณจะต้องใช้เชน () เพื่อให้บางส่วนใช้กับแต่ละฟังก์ชั่นต่อไปและจะเรียกใช้เฉพาะฟังก์ชันแรกเท่านั้น:

function chain(fs) {
    var f = function () {};
    for (var i = fs.length - 1; i >= 0; i--) {
        f = fs[i].partial(f);
    }
    f();
}

สวัสดี ngn และขอบคุณสำหรับคำตอบ! ในกรณีของฉันมีการเข้าถึงตัวแปรการปิดอินไลน์ในทุกการเยื้อง ตัวอย่างเช่นฟังก์ชั่นใช้งานเช่นนี้รับ HTTP req / res, รับหมายเลขผู้ใช้จาก DB สำหรับคุกกี้, รับอีเมลสำหรับหมายเลขผู้ใช้ในภายหลัง, รับข้อมูลเพิ่มเติมสำหรับอีเมลในภายหลัง, ... , รับ X สำหรับ Y ในภายหลัง ... หากฉันไม่เข้าใจผิดรหัสที่คุณแนะนำจะทำให้มั่นใจได้ว่าฟังก์ชั่น async จะถูกดำเนินการตามลำดับที่ถูกต้อง แต่ในทุกฟังก์ชั่นของร่างกายไม่มีวิธีรับตัวแปรที่ได้รับจากการปิดในโค้ดต้นฉบับของฉัน เป็นอย่างนั้นเหรอ?
Kay Pale

2

นรกโทรกลับสามารถหลีกเลี่ยงได้อย่างง่ายดายในจาวาสคริปต์บริสุทธิ์ด้วยการปิด วิธีการแก้ปัญหาด้านล่างจะถือว่าการโทรกลับทั้งหมดเป็นไปตามลายเซ็นฟังก์ชัน (ข้อผิดพลาดข้อมูล)

http.createServer(function (req, res) {
  var modeNext, onNext;

  // closure variable to keep track of next-callback-state
  modeNext = 0;

  // next-callback-handler
  onNext = function (error, data) {
    if (error) {
      modeNext = Infinity;
    } else {
      modeNext += 1;
    }
    switch (modeNext) {

    case 0:
      res.writeHead(200, {'Content-Type': 'text/html'});
      var html = "<h1>Demo page</h1>";
      getSomeDate(client, onNext);
      break;

    // handle someData
    case 1:
        html += "<p>"+ data +"</p>";
        getSomeOtherDate(client, onNext);
        break;

    // handle someOtherData
    case 2:
      html += "<p>"+ data +"</p>";
      getMoreData(client, onNext);
      break;

    // handle moreData
    case 3:
      html += "<p>"+ data +"</p>";
      res.write(html);
      res.end();
      break;

    // general catch-all error-handler
    default:
      res.statusCode = 500;
      res.end(error.message + '\n' + error.stack);
    }
  };
  onNext();
});

1

ฉันเพิ่งสร้างนามธรรมที่ง่ายกว่าที่เรียกว่าwait.forเพื่อเรียกใช้ฟังก์ชั่น async ในโหมดซิงค์ (ตามไฟเบอร์) มันอยู่ในช่วงเริ่มต้น แต่ใช้งานได้ มันอยู่ที่:

https://github.com/luciotato/waitfor

การใช้wait.forคุณสามารถเรียกใช้ฟังก์ชั่น async nodejs มาตรฐานราวกับว่ามันเป็นฟังก์ชั่นการซิงค์

ใช้wait.forรหัสของคุณอาจเป็น:

var http=require('http');
var wait=require('wait.for');

http.createServer(function(req, res) {
  wait.launchFiber(handleRequest,req, res); //run in a Fiber, keep node spinning
}).listen(8080);


//in a fiber
function handleRequest(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  var someData = wait.for(getSomeDate,client);
  html += "<p>"+ someData +"</p>";
  var someOtherData = wait.for(getSomeOtherDate,client);
  html += "<p>"+ someOtherData +"</p>";
  var moreData = wait.for(getMoreData,client);
  html += "<p>"+ moreData +"</p>";
  res.write(html);
  res.end();
};

... หรือถ้าคุณต้องการที่จะ verbose น้อย (และเพิ่มการจับข้อผิดพลาด)

//in a fiber
function handleRequest(req, res) {
  try {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write(
    "<h1>Demo page</h1>" 
    + "<p>"+ wait.for(getSomeDate,client) +"</p>"
    + "<p>"+ wait.for(getSomeOtherDate,client) +"</p>"
    + "<p>"+ wait.for(getMoreData,client) +"</p>"
    );
    res.end();
  }
  catch(err) {
   res.end('error '+e.message); 
  }

};

ในทุกกรณีgetSomeDate , getSomeOtherDateและgetMoreData ควรเป็นฟังก์ชั่นมาตรฐานแบบอะซิงก์พร้อมพารามิเตอร์สุดท้ายคือฟังก์ชันการโทรกลับ (err, data)

ในขณะที่:

function getMoreData(client, callback){
  db.execute('select moredata from thedata where client_id=?',[client.id],
       ,function(err,data){
          if (err) callback(err);
          callback (null,data);
        });
}

1

เพื่อแก้ปัญหานี้ฉันเขียน nodent ( https://npmjs.org/package/nodent ) ซึ่งประมวลผล JS ของคุณอย่างล่องหน โค้ดตัวอย่างของคุณจะกลายเป็น (async จริง ๆ - อ่านเอกสาร)

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  someData <<= getSomeDate(client) ;

  html += "<p>"+ someData +"</p>";
  someOtherData <<= getSomeOtherDate(client) ;

  html += "<p>"+ someOtherData +"</p>";
  moreData <<= getMoreData(client) ;

  html += "<p>"+ moreData +"</p>";
  res.write(html);
  res.end();
});

เห็นได้ชัดว่ามีวิธีแก้ปัญหาอื่น ๆ อีกมากมาย แต่การประมวลผลล่วงหน้ามีข้อได้เปรียบของการมีค่าใช้จ่ายในการดำเนินการเพียงเล็กน้อยหรือไม่มีเลยและด้วยการสนับสนุนแผนที่แหล่งที่มา


0

ผมมีปัญหาเหมือนกัน. ฉันเห็น libs หลัก ๆ ในการเรียกใช้ฟังก์ชั่น async และพวกมันนำเสนอการผูกมัดแบบไม่เป็นธรรมชาติ (คุณต้องใช้วิธีการอย่างน้อยสามวิธีในการสร้างรหัส ฯลฯ ) เพื่อสร้างรหัสของคุณ

ฉันใช้เวลาหลายสัปดาห์ในการพัฒนาวิธีแก้ปัญหาเพื่อให้ง่ายและผ่อนคลายในการอ่าน กรุณาให้ลองไปEnqJS ความคิดเห็นทั้งหมดจะได้รับการชื่นชม

แทน:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  getSomeDate(client, function(someData) {
    html += "<p>"+ someData +"</p>";
    getSomeOtherDate(client, function(someOtherData) {
      html += "<p>"+ someOtherData +"</p>";
      getMoreData(client, function(moreData) {
        html += "<p>"+ moreData +"</p>";
        res.write(html);
        res.end();
      });
    });
  });

กับ EnqJS:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";

  enq(function(){
    var self=this;
    getSomeDate(client, function(someData){
      html += "<p>"+ someData +"</p>";
      self.return();
    })
  })(function(){
    var self=this;
    getSomeOtherDate(client, function(someOtherData){ 
      html += "<p>"+ someOtherData +"</p>";
      self.return();
    })
  })(function(){
    var self=this;
    getMoreData(client, function(moreData) {
      html += "<p>"+ moreData +"</p>";
      self.return();
      res.write(html);
      res.end();
    });
  });
});

สังเกตว่ารหัสดูเหมือนจะใหญ่กว่า แต่ก่อน แต่มันไม่ซ้อนกันเหมือนเมื่อก่อน เพื่อให้ดูเป็นธรรมชาติยิ่งขึ้นโซ่นั้นถูกเรียกว่าเลียนแบบ:

enq(fn1)(fn2)(fn3)(fn4)(fn4)(...)

และจะบอกว่ามันกลับมาภายในฟังก์ชันที่เราเรียกว่า:

this.return(response)

0

ฉันทำมันด้วยวิธีดั้งเดิม แต่มีประสิทธิภาพ เช่นฉันต้องการสร้างแบบจำลองกับผู้ปกครองและเด็ก ๆ และสมมติว่าฉันต้องทำแบบสอบถามแยกต่างหากสำหรับพวกเขา:

var getWithParents = function(id, next) {
  var getChildren = function(model, next) {
        /*... code ... */
        return next.pop()(model, next);
      },
      getParents = function(model, next) {
        /*... code ... */
        return next.pop()(model, next);
      }
      getModel = function(id, next) {
        /*... code ... */
        if (model) {
          // return next callbacl
          return next.pop()(model, next);
        } else {
          // return last callback
          return next.shift()(null, next);
        }
      }

  return getModel(id, [getParents, getChildren, next]);
}

0

ใช้ Fibres https://github.com/laverdet/node-fibersมันทำให้โค้ดอะซิงโครนัสดูเหมือนกับซิงโครนัส (โดยไม่ปิดกั้น)

ฉันเองใช้กระดาษห่อเล็ก ๆ นี้http://alexeypetrushin.github.com/synchronize ตัวอย่างของโค้ดจากโครงการของฉัน (ทุกวิธีเป็นแบบอะซิงโครนัสจริง ๆ แล้วทำงานกับไฟล์ async IO) ฉันกลัวว่าจะสับสนกับโทรกลับหรือ ไลบรารีตัวช่วยเหลือ async-control-flow

_update: (version, changesBasePath, changes, oldSite) ->
  @log 'updating...'
  @_updateIndex version, changes
  @_updateFiles version, changesBasePath, changes
  @_updateFilesIndexes version, changes
  configChanged = @_updateConfig version, changes
  @_updateModules version, changes, oldSite, configChanged
  @_saveIndex version
  @log "updated to #{version} version"

0

Task.js ให้สิ่งนี้แก่คุณ:

spawn(function*() {
    try {
        var [foo, bar] = yield join(read("foo.json"),
                                    read("bar.json")).timeout(1000);
        render(foo);
        render(bar);
    } catch (e) {
        console.log("read failed: " + e);
    }
});

แทนสิ่งนี้:

var foo, bar;
var tid = setTimeout(function() { failure(new Error("timed out")) }, 1000);

var xhr1 = makeXHR("foo.json",
                   function(txt) { foo = txt; success() },
                   function(err) { failure() });
var xhr2 = makeXHR("bar.json",
                   function(txt) { bar = txt; success() },
                   function(e) { failure(e) });

function success() {
    if (typeof foo === "string" && typeof bar === "string") {
        cancelTimeout(tid);
        xhr1 = xhr2 = null;
        render(foo);
        render(bar);
    }
}

function failure(e) {
    xhr1 && xhr1.abort();
    xhr1 = null;
    xhr2 && xhr2.abort();
    xhr2 = null;
    console.log("read failed: " + e);
}

0

หลังจากที่คนอื่นตอบคุณระบุว่าปัญหาของคุณคือตัวแปรท้องถิ่น ดูเหมือนว่าวิธีง่าย ๆ ในการทำเช่นนี้คือการเขียนฟังก์ชันภายนอกหนึ่งตัวเพื่อเก็บตัวแปรท้องถิ่นเหล่านั้นจากนั้นใช้กลุ่มฟังก์ชันภายในที่มีชื่อและเข้าถึงพวกเขาตามชื่อ ด้วยวิธีนี้คุณจะสามารถซ้อนสองส่วนลึก ๆ ได้โดยไม่คำนึงถึงฟังก์ชันที่คุณต้องการรวมเข้าด้วยกัน

นี่คือความพยายามของมือใหม่ในการใช้mysqlโมดูล Node.js กับการซ้อน:

function with_connection(sql, bindings, cb) {
    pool.getConnection(function(err, conn) {
        if (err) {
            console.log("Error in with_connection (getConnection): " + JSON.stringify(err));
            cb(true);
            return;
        }
        conn.query(sql, bindings, function(err, results) {
            if (err) {
                console.log("Error in with_connection (query): " + JSON.stringify(err));
                cb(true);
                return;
            }
            console.log("with_connection results: " + JSON.stringify(results));
            cb(false, results);
        });
    });
}

ต่อไปนี้คือการเขียนซ้ำโดยใช้ชื่อฟังก์ชั่นด้านใน ฟังก์ชั่นด้านนอกwith_connectionสามารถใช้เป็นตัวยึดสำหรับตัวแปรภายในได้เช่นกัน (ที่นี่ผมได้มีพารามิเตอร์sql, bindings, cbกระทำที่ในลักษณะที่คล้ายกัน แต่คุณก็สามารถกำหนดตัวแปรท้องถิ่นบางอย่างเพิ่มเติมในwith_connection.)

function with_connection(sql, bindings, cb) {

    function getConnectionCb(err, conn) {
        if (err) {
            console.log("Error in with_connection/getConnectionCb: " + JSON.stringify(err));
            cb(true);
            return;
        }
        conn.query(sql, bindings, queryCb);
    }

    function queryCb(err, results) {
        if (err) {
            console.log("Error in with_connection/queryCb: " + JSON.stringify(err));
            cb(true);
            return;
        }
        cb(false, results);
    }

    pool.getConnection(getConnectionCb);
}

ฉันคิดว่าบางทีมันอาจเป็นไปได้ที่จะสร้างวัตถุที่มีตัวแปรอินสแตนซ์และใช้ตัวแปรอินสแตนซ์เหล่านี้เพื่อทดแทนตัวแปรท้องถิ่น แต่ตอนนี้ฉันพบว่าวิธีการข้างต้นโดยใช้ฟังก์ชั่นที่ซ้อนกันและตัวแปรท้องถิ่นนั้นง่ายและเข้าใจง่ายขึ้น ใช้เวลาสักครู่เพื่อคลายความเข้าใจ OO ดูเหมือนว่า :-)

ดังนั้นนี่คือรุ่นก่อนหน้าของฉันพร้อมวัตถุและตัวแปรอินสแตนซ์

function DbConnection(sql, bindings, cb) {
    this.sql = sql;
    this.bindings = bindings;
    this.cb = cb;
}
DbConnection.prototype.getConnection = function(err, conn) {
    var self = this;
    if (err) {
        console.log("Error in DbConnection.getConnection: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    conn.query(this.sql, this.bindings, function(err, results) { self.query(err, results); });
}
DbConnection.prototype.query = function(err, results) {
    var self = this;
    if (err) {
        console.log("Error in DbConnection.query: " + JSON.stringify(err));
        self.cb(true);
        return;
    }
    console.log("DbConnection results: " + JSON.stringify(results));
    self.cb(false, results);
}

function with_connection(sql, bindings, cb) {
    var dbc = new DbConnection(sql, bindings, cb);
    pool.getConnection(function (err, conn) { dbc.getConnection(err, conn); });
}

ปรากฎว่าbindสามารถใช้เพื่อประโยชน์บางอย่าง มันทำให้ฉันสามารถกำจัดฟังก์ชั่นที่ไม่ระบุชื่อที่น่าเกลียดที่ฉันสร้างขึ้นซึ่งไม่ได้ทำอะไรมากนักยกเว้นจะส่งต่อตัวเองไปยังการเรียกเมธอด thisฉันไม่สามารถผ่านวิธีการโดยตรงเพราะมันจะได้มีส่วนร่วมที่มีค่าไม่ถูกต้องของ แต่ด้วยbindฉันสามารถระบุค่าthisที่ฉันต้องการ

function DbConnection(sql, bindings, cb) {
    this.sql = sql;
    this.bindings = bindings;
    this.cb = cb;
}
DbConnection.prototype.getConnection = function(err, conn) {
    var f = this.query.bind(this);
    if (err) {
        console.log("Error in DbConnection.getConnection: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    conn.query(this.sql, this.bindings, f);
}
DbConnection.prototype.query = function(err, results) {
    if (err) {
        console.log("Error in DbConnection.query: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    console.log("DbConnection results: " + JSON.stringify(results));
    this.cb(false, results);
}

// Get a connection from the pool, execute `sql` in it
// with the given `bindings`.  Invoke `cb(true)` on error,
// invoke `cb(false, results)` on success.  Here,
// `results` is an array of results from the query.
function with_connection(sql, bindings, cb) {
    var dbc = new DbConnection(sql, bindings, cb);
    var f = dbc.getConnection.bind(dbc);
    pool.getConnection(f);
}

แน่นอนไม่มีสิ่งใดที่เหมาะสมกับการเข้ารหัส JS ของ Node - ฉันใช้เวลาสองสามชั่วโมงกับมัน แต่อาจใช้เทคนิคนี้ช่วยหน่อยได้ไหม?





0

การใช้สายรหัสของคุณจะมีลักษณะเช่นนี้:

http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});

    var l = new Wire();

    getSomeDate(client, l.branch('someData'));
    getSomeOtherDate(client, l.branch('someOtherData'));
    getMoreData(client, l.branch('moreData'));

    l.success(function(r) {
        res.write("<h1>Demo page</h1>"+
            "<p>"+ r['someData'] +"</p>"+
            "<p>"+ r['someOtherData'] +"</p>"+
            "<p>"+ r['moreData'] +"</p>");
        res.end();
    });
});

0

สำหรับความรู้ของคุณพิจารณา Jazz.js https://github.com/Javanile/Jazz.js/wiki/Script-showcase

    const jj = ต้องการ ('jazz.js');

    // สแต็คพิเศษที่เข้ากันได้
    jj.script ([
        a => ProcessTaskOneCallbackAtEnd (a)
        b => ProcessTaskTwoCallbackAtEnd (b)
        c => ProcessTaskThreeCallbackAtEnd (c),
        d => ProcessTaskFourCallbackAtEnd (d)
        e => ProcessTaskFiveCallbackAtEnd (e)
    ]);

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.