วิธีทำให้ฟังก์ชั่นรอจนกระทั่งมีการโทรกลับโดยใช้ node.js


266

ฉันมีฟังก์ชั่นง่าย ๆ ที่มีลักษณะดังนี้:

function(query) {
  myApi.exec('SomeCommand', function(response) {
    return response;
  });
}

โดยทั่วไปฉันต้องการให้โทรmyApi.execและกลับคำตอบที่ได้รับในแลมบ์ดาโทรกลับ อย่างไรก็ตามรหัสข้างต้นใช้งานไม่ได้และจะส่งคืนทันที

เพียงเพื่อความพยายามแฮ็คมากฉันลองด้านล่างซึ่งไม่ได้ผล แต่อย่างน้อยคุณก็เข้าใจว่าฉันพยายามทำอะไรให้สำเร็จ:

function(query) {
  var r;
  myApi.exec('SomeCommand', function(response) {
    r = response;
  });
  while (!r) {}
  return r;
}

โดยทั่วไปแล้วอะไรคือ 'node.js / event driven' ทางที่ดีในการดำเนินเรื่องนี้ ฉันต้องการให้ฟังก์ชั่นของฉันรอจนกว่าจะได้รับการติดต่อกลับแล้วส่งกลับค่าที่ส่งไปให้


3
หรือฉันกำลังทำผิดอย่างสิ้นเชิงที่นี่และฉันควรจะโทรกลับอีกครั้งแทนที่จะตอบกลับ?
คริส

นี่คือความเห็นของฉันคำอธิบายที่ดีที่สุดว่าทำไมห่วงไม่ว่างไม่ทำงาน
bluenote10

อย่าพยายามที่จะรอ เพียงแค่เรียกใช้ฟังก์ชันถัดไป (ขึ้นอยู่กับการติดต่อกลับ) ภายในเมื่อสิ้นสุดการติดต่อกลับ
Atul

คำตอบ:


282

วิธี "good node.js / event driven" คือการไม่รอไม่ต้องรอ

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

function(query, callback) {
  myApi.exec('SomeCommand', function(response) {
    // other stuff here...
    // bla bla..
    callback(response); // this will "return" your value to the original caller
  });
}

คุณไม่ได้ใช้มันอย่างนี้:

var returnValue = myFunction(query);

แต่เช่นนี้

myFunction(query, function(returnValue) {
  // use the return value here instead of like a regular (non-evented) return value
});

5
เยี่ยมมาก ถ้า myApi.exec ไม่เคยโทรกลับ? ฉันจะทำอย่างไรเพื่อให้การโทรกลับได้รับการโทรหลังจากพูดว่า 10 วินาทีพร้อมกับค่าความผิดพลาดที่บอกว่าหมดเวลาหรือบางอย่างของเรา
คริส

5
หรือดีกว่า (เพิ่มการตรวจสอบเพื่อไม่ให้มีการเรียกกลับมาสองครั้ง): jsfiddle.net/LdaFw/1
Jakob

148
เป็นที่ชัดเจนว่าการไม่บล็อกคือมาตรฐานในโหนด / js อย่างไรก็ตามมีบางครั้งที่ต้องการบล็อก (เช่นการบล็อกบน stdin) แม้แต่โหนดก็มีวิธี "บล็อก" (ดูfs sync*วิธีการทั้งหมด) ดังนั้นฉันคิดว่านี่ยังเป็นคำถามที่ถูกต้อง มีวิธีที่ดีในการบรรลุการบล็อกในโหนดนอกเหนือจากการรอไม่ว่างหรือไม่
nategood

7
คำตอบที่ล่าช้าในการแสดงความคิดเห็นโดย @natoodood: ฉันสามารถคิดได้หลายวิธี; มากเกินไปที่จะอธิบายในความคิดเห็นนี้ แต่ google พวกเขา โปรดจำไว้ว่าโหนดจะไม่ถูกบล็อกดังนั้นสิ่งเหล่านี้จึงไม่สมบูรณ์แบบ คิดว่าพวกเขาเป็นคำแนะนำ อย่างไรก็ตามนี่ไป: (1) ใช้ C เพื่อใช้ฟังก์ชันของคุณและเผยแพร่ไปยัง NPM เพื่อใช้งาน นั่นคือสิ่งที่syncวิธีการทำ (2) ใช้ fibers, github.com/laverdet/node-fibers , (3) ใช้สัญญาเช่น Q-library, (4) ใช้ layer บาง ๆ ที่ด้านบนของ javascript ที่ดูเหมือนบล็อก แต่คอมไพล์เป็น async, ชอบmaxtaco.github.com/coffee-script
Jakob

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

46

วิธีหนึ่งในการบรรลุเป้าหมายนี้คือการรวมการเรียก API เป็นสัญญาและจากนั้นใช้awaitเพื่อรอผล

// let's say this is the API function with two callbacks,
// one for success and the other for error
function apiFunction(query, successCallback, errorCallback) {
    if (query == "bad query") {
        errorCallback("problem with the query");
    }
    successCallback("Your query was <" + query + ">");
}

// myFunction wraps the above API call into a Promise
// and handles the callbacks with resolve and reject
function apiFunctionWrapper(query) {
    return new Promise((resolve, reject) => {
        apiFunction(query,(successResponse) => {
            resolve(successResponse);
        }, (errorResponse) => {
            reject(errorResponse)
        });
    });
}

// now you can use await to get the result from the wrapped api function
// and you can use standard try-catch to handle the errors
async function businessLogic() {
    try {
        const result = await apiFunctionWrapper("query all users");
        console.log(result);

        // the next line will fail
        const result2 = await apiFunctionWrapper("bad query");
    } catch(error) {
        console.error("ERROR:" + error);
    }
}

// call the main function
businessLogic();

เอาท์พุท:

Your query was <query all users>
ERROR:problem with the query

นี่เป็นตัวอย่างที่ดีมากของการห่อฟังก์ชั่นด้วยการโทรกลับเพื่อให้คุณสามารถใช้กับasync/await ฉันไม่ต้องการสิ่งนี้บ่อยครั้งดังนั้นจึงมีปัญหาในการจดจำวิธีจัดการสถานการณ์นี้ฉันคัดลอกสิ่งนี้สำหรับบันทึกส่วนตัว / ข้อมูลอ้างอิงของฉัน
robert arles


10

หากคุณไม่ต้องการใช้โทรกลับคุณสามารถใช้โมดูล "Q"

ตัวอย่างเช่น:

function getdb() {
    var deferred = Q.defer();
    MongoClient.connect(databaseUrl, function(err, db) {
        if (err) {
            console.log("Problem connecting database");
            deferred.reject(new Error(err));
        } else {
            var collection = db.collection("url");
            deferred.resolve(collection);
        }
    });
    return deferred.promise;
}


getdb().then(function(collection) {
   // This function will be called afte getdb() will be executed. 

}).fail(function(err){
    // If Error accrued. 

});

สำหรับข้อมูลเพิ่มเติมอ้างถึงสิ่งนี้: https://github.com/kriskowal/q


9

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

//initialize a global var to control the callback state
var callbackCount = 0;
//call the function that has a callback
someObj.executeCallback(function () {
    callbackCount++;
    runOtherCode();
});
someObj2.executeCallback(function () {
    callbackCount++;
    runOtherCode();
});

//call function that has to wait
continueExec();

function continueExec() {
    //here is the trick, wait until var callbackCount is set number of callback functions
    if (callbackCount < 2) {
        setTimeout(continueExec, 1000);
        return;
    }
    //Finally, do what you need
    doSomeThing();
}

5

หมายเหตุ: คำตอบนี้อาจไม่ควรใช้ในรหัสการผลิต มันเป็นแฮ็คและคุณควรรู้เกี่ยวกับความหมาย

มีโมดูลuvrun (อัพเดตสำหรับ Nodejs รุ่นใหม่กว่าที่นี่ ) ซึ่งคุณสามารถดำเนินการวนรอบเดียวของ libuv main event loop (ซึ่งเป็น Nodejs main loop)

รหัสของคุณจะเป็นดังนี้:

function(query) {
  var r;
  myApi.exec('SomeCommand', function(response) {
    r = response;
  });
  var uvrun = require("uvrun");
  while (!r)
    uvrun.runOnce();
  return r;
}

(คุณอาจใช้ทางเลือก uvrun.runNoWait()ซึ่งอาจหลีกเลี่ยงปัญหาในการบล็อก แต่ใช้ CPU 100%)

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

ดูคำตอบอื่น ๆ เกี่ยวกับวิธีการออกแบบรหัสของคุณใหม่เพื่อให้ "ถูกต้อง"

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


5

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

รหัสของคุณควรจะดีเหมือนตัวอย่างด้านล่าง

const Promise = require('bluebird');

function* getResponse(query) {
  const r = yield new Promise(resolve => myApi.exec('SomeCommand', resolve);
  return r;
}

Promise.coroutine(getResponse)()
  .then(response => console.log(response));

1

สมมติว่าคุณมีฟังก์ชั่น:

var fetchPage(page, callback) {
   ....
   request(uri, function (error, response, body) {
        ....
        if (something_good) {
          callback(true, page+1);
        } else {
          callback(false);
        }
        .....
   });


};

คุณสามารถใช้ประโยชน์จากการเรียกกลับเช่นนี้:

fetchPage(1, x = function(next, page) {
if (next) {
    console.log("^^^ CALLBACK -->  fetchPage: " + page);
    fetchPage(page, x);
}
});

-1

นั่นเป็นการทำลายวัตถุประสงค์ของการไม่ปิดกั้น IO - คุณกำลังปิดกั้นเมื่อไม่จำเป็นต้องปิดกั้น :)

คุณควรจะเรียกกลับรังของคุณแทนการบังคับให้ Node.js rรอหรือโทรโทรกลับอีกภายในโทรกลับที่คุณต้องการผลมาจากการ

โอกาสคือถ้าคุณต้องการบังคับให้บล็อกคุณกำลังคิดถึงสถาปัตยกรรมของคุณผิด


ฉันสงสัยว่าฉันมีสิ่งนี้ย้อนหลัง
Chris

31
โอกาสที่ฉันต้องการเขียนสคริปต์สั้น ๆ ไปยังhttp.get()URL และconsole.log()เนื้อหาของมัน ทำไมฉันต้องข้ามไปข้างหลังเพื่อทำสิ่งนั้นในโหนด
Dan Dascalescu

6
@DanDascalescu: และทำไมฉันต้องประกาศลายเซ็นประเภทที่จะทำในภาษาคงที่? และทำไมฉันต้องใส่มันในวิธีการหลักในภาษาที่เหมือน C? และทำไมฉันต้องรวบรวมมันเป็นภาษาที่รวบรวม? สิ่งที่คุณตั้งคำถามคือการตัดสินใจออกแบบพื้นฐานใน Node.js การตัดสินใจนั้นมีข้อดีข้อเสีย หากคุณไม่ชอบคุณสามารถใช้ภาษาอื่นที่เหมาะกับสไตล์ของคุณได้ดีกว่า นั่นเป็นเหตุผลที่เรามีมากกว่าหนึ่ง
Jakob

@ Jakob: โซลูชั่นที่คุณระบุไว้นั้นไม่ดีนักอย่างแน่นอน นั่นไม่ได้หมายความว่าไม่มีสิ่งที่ดีเช่นการใช้ Node ในฝั่งเซิร์ฟเวอร์ของ Meteor ในการใช้ไฟเบอร์ซึ่งจะช่วยขจัดปัญหาในการติดต่อกลับ
Dan Dascalescu

13
@ Jakob: หากคำตอบที่ดีที่สุดสำหรับ "ทำไมระบบนิเวศ X ทำให้งานทั่วไป Y ยากโดยไม่จำเป็น?" คือ "ถ้าคุณไม่ชอบอย่าใช้ระบบนิเวศ X" นั่นเป็นสัญญาณบ่งบอกว่าผู้ออกแบบและผู้ดูแลระบบนิเวศ X กำลังจัดลำดับความสำคัญของตนเองเหนือกว่าการใช้งานจริงของระบบนิเวศ เป็นประสบการณ์ของฉันที่ชุมชนโหนด (ในทางตรงกันข้ามกับ Ruby, Elixir และแม้แต่ชุมชน PHP) ออกไปเพื่อทำให้งานทั่วไปเป็นเรื่องยาก ขอบคุณมากสำหรับการเสนอตัวเองเป็นตัวอย่างของสิ่งที่อยู่ตรงข้าม
Jazz

-1

ใช้ async และรอมันง่ายกว่ามาก

router.post('/login',async (req, res, next) => {
i = await queries.checkUser(req.body);
console.log('i: '+JSON.stringify(i));
});

//User Available Check
async function checkUser(request) {
try {
    let response = await sql.query('select * from login where email = ?', 
    [request.email]);
    return response[0];

    } catch (err) {
    console.log(err);

  }

}

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