如何在多个其他函数完成后执行JavaScript函数?

21

我的具体问题是需要执行(可能)大量的JavaScript函数来准备类似于批处理文件的东西(每个函数调用添加一些信息到同一个批处理文件中),然后在所有这些调用完成后,执行最终的函数以发送批处理文件(比如作为HTML响应)。我正在寻找一个通用的JavaScript编程模式来解决这个问题。

问题概括: 给定JavaScript函数funcA()、funcB()和funcC(),我需要找出最佳的执行顺序,使得funcA和funcB在执行完成后再执行funcC。我知道可以使用嵌套回调函数来实现这一点,例如:

funcA = function() {
    //Does funcA stuff
    funcB();
}
funcB = function() {
    //Does funcB stuff
    funcC();
}

funcA();

通过传入回调参数,我甚至可以使这个模式更加通用,但是这种解决方案变得非常冗长。

我还熟悉Javascript函数链接,其中一个解决方案可能看起来像:

myObj = {}
myObj.answer = ""
myObj.funcA = function() {
    //Do some work on this.answer
    return this;
}
myObj.funcB = function() {
    //Do some more work on this.answer
    return this;
}
myObj.funcC = function() {
    //Use the value of this.answer now that funcA and funcB have made their modifications
    return this;
}
myObj.funcA().funcB().funcC();
尽管这个解决方案对我来说似乎更加干净,但随着计算步骤的增加,函数执行链会变得越来越长。
对于我的特定问题,funcA、funcB等执行顺序并不重要。因此,在我的解决方案中,我从技术上讲做了比所需更多的工作,因为我将所有函数都放在一个串行顺序中。对我而言,唯一重要的是,只有在funcA和funcB全部完成执行之后才调用funcC(发送结果或发出请求的某个函数)。理想情况下,funcC可以监听所有中间函数调用的完成,并且在此后执行。我希望学习一种通用的JavaScript模式来解决这样的问题。
感谢您的帮助。
另一个想法:也许可以将一个共享对象传递给funcA和funcB,当它们完成执行时,像sharedThing.funcA =“complete”或sharedThing.funcB =“complete”这样标记共享对象,然后以某种方式?在共享对象达到所有字段都标记为已完成的状态时执行funcC。我不确定如何使funcC等待这个过程。
编辑:我应该指出,我正在使用服务器端JavaScript(Node.js),我想学习一种使用纯旧JavaScript(不使用jQuery或其他库)解决它的模式。毫无疑问,这个问题足够通用,有一个干净的纯JavaScript解决方案吗?

构建一个函数引用数组,按照你想要执行的顺序排列,并将该数组传递给另一个函数,该函数只需迭代遍历数组并调用函数。确保你想要最后执行的函数位于最后。 - Pointy
1
或者只需按您希望执行的顺序编写代码;我不清楚问题真正是什么。顺序命令式编码总是让您承诺执行程序语句的顺序;这种情况有什么特别之处? - Pointy
1
这很聪明,Pointy。我从来没有想过这样做。如果你的函数有一个延迟开始,那怎么办?它还会等待完成吗? - elclanrs
2
可能有些过度,但jQuery具有延迟功能。基本上它允许您使用类似于$.when(A, B).then(C)的语法。 - pimvdb
正如Pointy所说,这里的真正问题是什么? 这些函数中有任何异步操作吗? 如果没有,那么JavaScript将按照您编写的顺序依次执行它们。 - jfriend00
7个回答

9

如果您希望保持简单,可以使用基于计数器的回调系统。这是一个草案,允许使用when(A,B)。then(C)语法。 (when/then实际上只是一种糖果,但整个系统也可以说是如此。)

var when = function() {
  var args = arguments;  // the functions to execute first
  return {
    then: function(done) {
      var counter = 0;
      for(var i = 0; i < args.length; i++) {
        // call each function with a function to call on done
        args[i](function() {
          counter++;
          if(counter === args.length) {  // all functions have notified they're done
            done();
          }
        });
      }
    }
  };
};

使用方法:

when(
  function(done) {
    // do things
    done();
  },
  function(done) {
    // do things
    setTimeout(done, 1000);
  },
  ...
).then(function() {
  // all are done
});

3

如果您不使用任何异步函数,并且您的脚本不会破坏执行顺序,那么最简单的解决方案是:如Pointy和其他人所述:

funcA(); 
funcB();
funcC();

由于您使用的是node.js,我相信您将使用异步函数,并且希望在异步IO请求完成后执行funcC,因此您需要使用某种计数机制,例如:

var call_after_completion = function(callback){
    this._callback = callback;
    this._args = [].slice.call(arguments,1);
    this._queue = {};
    this._count = 0;
    this._run = false;
}

call_after_completion.prototype.add_condition = function(str){
    if(this._queue[str] !== undefined)
        throw new TypeError("Identifier '"+str+"' used twice");
    else if(typeof str !== "String" && str.toString === undefined)
        throw new TypeError("Identifier has to be a string or needs a toString method");

    this._queue[str] = 1;
    this._count++;
    return str;
}

call_after_completion.prototype.remove_condition = function(str){
    if(this._queue[str] === undefined){
        console.log("Removal of condition '"+str+"' has no effect");
        return;
    }
    else if(typeof str !== "String" && str.toString === undefined)
        throw new TypeError("Identifier has to be a string or needs a toString method");

    delete this._queue[str];

    if(--this._count === 0 && this._run === false){
        this._run = true;
        this._callback.apply(null,this._args);
    }
}

您可以通过忽略标识符str,只增加/减少this._count来简化此对象,但是此系统对于调试可能很有用。

要使用call_after_completion,您只需创建一个带有所需函数func作为参数和add_conditions的new call_after_completion。只有当所有条件都被移除时,才会调用func

示例:

var foo = function(){console.log("foo");}
var bar = new call_after_completion(foo);
var i;

bar.add_condition("foo:3-Second-Timer");
bar.add_condition("foo:additional function");
bar.add_condition("foo:for-loop-finished");

function additional_stuff(cond){
    console.log("additional things");
    cond.remove_condition("foo:additional function");
}

for(i = 0; i < 1000; ++i){

}
console.log("for loop finished");
bar.remove_condition("foo:for-loop-finished");
additional_stuff(bar);

setTimeout(function(){
    console.log("3 second timeout");
    bar.remove_condition("foo:3-Second-Timer");
},3000);

JSFiddle演示


2

如果你不想使用任何辅助库,那么你需要自己编写一些辅助程序,这并没有简单的一行解决方案。

如果你想要得到与同步情况下相同可读性的结果,可以尝试一些延迟/承诺概念的实现(仍然是普通的JavaScript),例如使用deferred包,你可能会得到如下简单的代码:

// Invoke one after another:
funcA()(funcB)(funcC);

// Invoke funcA and funcB simultaneously and afterwards funcC:
funcA()(funcB())(funcC);

// If want result of both funcA and funcB to be passed to funcC:
deferred(funcA(), funcB())(funcC);

0

我正在寻找相同类型的模式。我正在使用API来查询多个远程数据源。每个API都要求我向它们传递一个回调函数。这意味着我不能只触发一组自己的函数并等待它们返回。相反,我需要一个解决方案,可以与一组回调一起工作,这些回调可能按任何顺序被调用,具体取决于不同的数据源响应速度。

我想出了以下解决方案。JS是我最熟悉的语言列表中排名最低的语言之一,因此这可能不是非常符合JS语言习惯。

function getCallbackCreator( number_of_data_callbacks, final_callback ) {

    var all_data = {}

    return function ( data_key ) {

        return function( data_value ) {
            all_data[data_key] = data_value;

            if ( Object.keys(all_data).length == number_of_data_callbacks ) {
                final_callback( all_data );
            }
        }
    }
}

var getCallback = getCallbackCreator( 2, inflatePage );

myGoogleDataFetcher( getCallback( 'google' ) );
myCartoDataFetcher( getCallback( 'cartodb' ) );

编辑:该问题被标记为 node.js,但 OP 表示,“我正在寻找一个通用的 Javascript 编程模式”,因此即使我没有使用 node,我也发布了这个帖子。


0

这听起来很有趣,但你提供的三个链接中有两个是失效的。 - crantok

0

现在,我们可以这样做:

假设我们有funcA、funcB和funcC:

如果想要将funcA和funcB的结果传递给funcC:

var promiseA = new Promise((resolve, reject) => {
  resolve(await funcA());
});
var promiseB = new Promise((resolve, reject) => {
  resolve(await funcB());
});
var promise = Promise.all([ promiseA, promiseB ]).then(results => {
  // results = [result from funcA, result from funcB]
  return funcC(results);
});

如果想要使用funcA,那么需要先使用funcB,然后再使用funcC:

var promise = (
  new Promise(async resolve => resolve( await funcA() ))
).then(result_a => funcB(result_a)).then(result_b => funcC(result_b));

最后:

promise.then(result_c => console.log('done.'));

-1
how about:
funcC(funcB(funcA)));

我认为问题是因为某些函数运行时间较长,可能会出现这样的情况:当我们运行funcC时,funcA或funcB尚未执行完毕。


在@Aleksander Kogut的回答中,有一个更清晰的视角,但从你的方法来看,可以这样写:result = funcA(); result = funcB(result); funcC(result); 因为我想传递结果链。 - Vasyl Lyashkevych
我刚刚纠正了之前的代码。现在对我来说完美运行。我甚至使用了更多嵌套函数。 - A. K.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接