【问题标题】:"Phased" execution of functions in javascript在javascript中“分阶段”执行函数
【发布时间】:2010-03-31 12:22:02
【问题描述】:

这是我在 stackoverflow 上的第一篇文章,所以如果我遇到一个彻头彻尾的傻瓜或者如果我无法让自己完全清楚,请不要对我发火。 :-)

这是我的问题:我正在尝试编写一个 javascript 函数,通过检查第一个函数的完成情况然后执行第二个函数,将两个函数“绑定”到另一个函数。

显然,解决这个问题的简单方法是编写一个元函数,在其范围内调用这两个函数。但是,如果第一个函数是异步的(特别是 AJAX 调用),而第二个函数需要第一个函数的结果数据,那根本行不通。

我对解决方案的想法是给第一个函数一个“标志”,即让它在调用后创建一个公共属性“this.trigger”(初始化为“0”,完成后设置为“1”) ;这样做应该可以让另一个函数检查标志的值([0,1])。如果满足条件(“trigger == 1”),则应调用第二个函数。

以下是我用于测试的抽象示例代码:

<script type="text/javascript" >

/**/function cllFnc(tgt) { //!! first function

    this.trigger = 0 ;
    var trigger = this.trigger ;

    var _tgt = document.getElementById(tgt) ; //!! changes the color of the target div to signalize the function's execution
        _tgt.style.background = '#66f' ;

    alert('Calling! ...') ;

    setTimeout(function() { //!! in place of an AJAX call, duration 5000ms

            trigger = 1 ;

    },5000) ;

}

/**/function rcvFnc(tgt) { //!! second function that should get called upon the first function's completion

    var _tgt = document.getElementById(tgt) ; //!! changes color of the target div to signalize the function's execution
        _tgt.style.background = '#f63' ;

    alert('... Someone picked up!') ;

}

/**/function callCheck(obj) {   

            //alert(obj.trigger ) ;      //!! correctly returns initial "0"                         

    if(obj.trigger == 1) {              //!! here's the problem: trigger never receives change from function on success and thus function two never fires 

                        alert('trigger is one') ;
                        return true ;
                    } else if(obj.trigger == 0) {
                        return false ;
                    }


}

/**/function tieExc(fncA,fncB,prms) {

        if(fncA == 'cllFnc') {
            var objA = new cllFnc(prms) ;   
            alert(typeof objA + '\n' + objA.trigger) ;  //!! returns expected values "object" and "0"
        } 

        //room for more case definitions

    var myItv = window.setInterval(function() {

        document.getElementById(prms).innerHTML = new Date() ; //!! displays date in target div to signalize the interval increments


        var myCallCheck = new callCheck(objA) ; 

            if( myCallCheck == true ) { 

                    if(fncB == 'rcvFnc') {
                        var objB = new rcvFnc(prms) ;
                    }

                    //room for more case definitions

                    window.clearInterval(myItv) ;

            } else if( myCallCheck == false ) {
                return ;
            }

    },500) ;

}

</script>

用于测试的 HTML 部分:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/strict.dtd >

<html>

<head>

    <script type="text/javascript" >
       <!-- see above -->
    </script>

    <title>

      Test page

    </title>


</head>

<body>

    <!-- !! testing area -->

        <div id='target' style='float:left ; height:6em ; width:8em ; padding:0.1em 0 0 0; font-size:5em ; text-align:center ; font-weight:bold ; color:#eee ; background:#fff;border:0.1em solid #555 ; -webkit-border-radius:0.5em ;' >
            Test Div
        </div>

        <div style="float:left;" >
            <input type="button" value="tie calls" onmousedown="tieExc('cllFnc','rcvFnc','target') ;" />
        </div>

<body>


</html>

我很确定这是 javascript 范围的一些问题,因为我检查了触发器是否正确设置为“1”并且确实如此。 “checkCall()”函数很可能没有接收到更新的对象,而是只检查它的旧实例,显然不会通过将“this.trigger”设置为“1”来标记完成。如果是这样,我不知道如何解决这个问题。

无论如何,希望有人对这种特殊问题有想法或经验。

感谢阅读!

FK

【问题讨论】:

  • 每次重新发明回调,node.js 开发者都会心脏病发作。
  • 其实这叫polling。我不知道 node.js 开发人员是否会因为有人重新发明轮询而心脏病发作。如果他们这样做了:我的错,对不起。

标签: javascript function execution phase


【解决方案1】:

您可以利用 JS 的一个称为闭包的特性。将它与一种非常常见的 JS 模式相结合,称为“持续传递样式”,您就有了解决方案。 (这些东西都不是 JS 的原创,而是在 JS 中大量使用)。

// a function
function foo(some_input_for_foo, callback)
{
    // do some stuff to get results

    callback(results); // call our callback when finished
}

// same again
function bar(some_input_for_bar, callback)
{
    // do some stuff to get results

    callback(results); // call our callback when finished
}

“延续传递风格”是指回调。每个函数不是返回一个值,而是调用一个回调(延续)并给它结果。

然后您可以轻松地将两者联系在一起:

foo(input1, function(results1) {

    bar(results1, function(results2) {

        alert(results2);
    });
});

嵌套的匿名函数可以从它们所在的范围内“看到”变量。因此无需使用特殊属性来传递信息。

更新

为了澄清,在您问题的代码 sn-p 中,很明显您的想法大致是这样的:

我有一个长时间运行的异步 操作,所以我需要知道什么时候 完成以开始下一个 手术。所以我需要做到这一点 状态作为属性可见。然后 在其他地方我可以循环运行, 反复检查该财产 看看它何时变为“完成” 状态,所以我知道什么时候继续。

(然后作为一个复杂因素,循环必须使用 setInterval 开始运行并使用 clearInterval 退出,以允许其他 JS 代码运行 - 但它基本上是一个“轮询循环”)。

你不需要这样做!

不要让你的第一个函数在完成时设置一个属性,而是让它调用一个函数。

为了明确这一点,让我们重构您的原始代码:

function cllFnc(tgt) { //!! first function

    this.trigger = 0 ;
    var trigger = this.trigger ;

    var _tgt = document.getElementById(tgt) ; //!! changes the color...
    _tgt.style.background = '#66f' ;

    alert('Calling! ...') ;

    setTimeout(function() { //!! in place of an AJAX call, duration 5000ms

        trigger = 1 ;

    },5000) ;
}

[更新 2:顺便说一下,这里有一个错误。您将trigger 属性的当前值复制到一个名为trigger 的新局部变量中。然后最后将 1 分配给该局部变量。没有其他人能够看到这一点。局部变量是函数私有的。 但无论如何你都不需要这样做,所以请继续阅读......]

我们所要做的就是告诉该函数在完成后调用什么,并摆脱属性设置:

function cllFnc(tgt, finishedFunction) { //!! first function

    var _tgt = document.getElementById(tgt) ; //!! changes the color...
    _tgt.style.background = '#66f' ;

    alert('Calling! ...') ;

    setTimeout(function() { //!! in place of an AJAX call, duration 5000ms

        finishedFunction(); // <-------- call function instead of set property

    },5000) ;
}

现在不需要您的“呼叫检查”或您的特殊tieExc 助手。只需很少的代码,您就可以轻松地将两个函数绑定在一起。

var mySpan = "#myspan";

cllFnc(mySpan, function() { rcvFnc(mySpan); });

这样做的另一个好处是我们可以将不同的参数传递给第二个函数。使用您的方法,将相同的参数传递给两者。

例如,第一个函数可能会调用 AJAX 服务(为了简洁起见使用 jQuery):

function getCustomerBillAmount(name, callback) {

    $.get("/ajax/getCustomerIdByName/" + name, function(id) {

        $.get("/ajax/getCustomerBillAmountById/" + id), callback);

    });
}

这里callback接受客户账单金额,AJAX调用get将接收到的值传递给我们传递的函数,所以callback已经兼容,可以直接作为回调第二个 AJAX 调用。因此,这本身就是将两个异步调用按顺序捆绑在一起并将它们包装在(从外部)看起来是单个异步函数的示例。

然后我们可以将它与另一个操作链接起来:

function displayBillAmount(amount) {

    $("#billAmount").text(amount); 
}

getCustomerBillAmount("Simpson, Homer J.", displayBillAmount);

或者我们可以(再次)使用匿名函数:

getCustomerBillAmount("Simpson, Homer J.", function(amount) {

    $("#billAmount").text(amount); 
});

因此,通过像这样链接函数调用,每个步骤都可以在信息可用时立即将信息传递到下一步。

通过让函数在完成后执行回调,您可以摆脱对每个函数内部工作方式的任何限制。它可以做 AJAX 调用、定时器等等。只要“继续”回调向前传递,就可以有任意层的异步工作。

基本上,在异步系统中,如果您发现自己编写了一个循环来检查变量并确定它是否已更改状态,那么某处出现了问题。相反,应该有一种方法来提供将在状态更改时调用的函数。

更新 3

我在 cmets 的其他地方看到您提到实际问题是缓存结果,所以我解释这一切的工作都是浪费时间。这就是你应该提出的问题。

更新 4

最近我写了a short blog post on the subject of caching asynchronous call results in JavaScript

(更新 4 结束)

另一种分享结果的方法是提供一种方法,让一个回调“广播”或“发布”给多个订阅者:

function pubsub() {
    var subscribers = [];

    return {
        subscribe: function(s) {
            subscribers.push(s);
        },
        publish: function(arg1, arg2, arg3, arg4) {
            for (var n = 0; n < subscribers.length; n++) {
                subscribers[n](arg1, arg2, arg3, arg4);
            }
        }
    };
}

所以:

finished = pubsub();

// subscribe as many times as you want:

finished.subscribe(function(msg) {
    alert(msg);
});

finished.subscribe(function(msg) {
    window.title = msg;
});

finished.subscribe(function(msg) {
    sendMail("admin@mysite.com", "finished", msg);
});

然后让一些慢操作发布它的结果:

lookupTaxRecords("Homer J. Simpson", finished.publish);

当一个呼叫结束时,它现在将呼叫所有三个订阅者。

【讨论】:

  • 如果我们对此有更好的支持就好了。包含大量回调函数调用的代码,嵌套膝盖深度,不容易遵循或编写。
  • @reinierpost - Mozilla 中有一个非常有用的非标准 JS 功能:smellegantcode.wordpress.com/2010/02/08/javascript-generators - 遗憾的是,除非它在浏览器之间实现标准化,否则用处不大,这在很长一段时间内都不太可能。
  • 您好,感谢您的回答!如果我像这样将函数联系在一起,我将(我相信)无法分离使用数据的函数调用和生成数据的函数。在这种情况下,我可以进行另一个 AJAX 调用,并将所有相关函数留给回调链。然而,这不是我想要做的:我想要一个函数来检查数据是否“从外部”准备好。
  • @Daniel Earwicker:您发布的链接看起来很有趣,我会检查一下。谢谢。
  • 如果您从中得到的只是一个错误修复,但您仍然认为轮询状态更改是个好主意,那么我无法解释 任何事情 i> 对你很有用。真正具有讽刺意味的是,您正在使用异步回调来检查状态变化!
【解决方案2】:

回调是对“准备好时给我打电话”问题的明确答案。回调基本上是您分配给对象属性的函数(如“onload”)。当对象状态改变时,调用该函数。例如,这个函数向给定的 url 发出一个 ajax 请求,并在完成时发出尖叫声:

function ajax(url) {
    var req = new XMLHttpRequest();  
    req.open('GET', url, true);  
    req.onreadystatechange = function (aEvt) {  
        if(req.readyState == 4)
            alert("Ready!")
    }
    req.send(null);  
}

当然,这不够灵活,因为我们大概希望对不同的 ajax 调用有不同的操作。幸运的是,javascript 是一种函数式语言,因此我们可以简单地将所需的操作作为参数传递:

function ajax(url, action) {
    var req = new XMLHttpRequest();  
    req.open('GET', url, true);  
    req.onreadystatechange = function (aEvt) {  
        if(req.readyState == 4)
            action(req.responseText);
    }
    req.send(null);  
}

第二个函数可以这样使用:

 ajax("http://...", function(text) {
      do something with ajax response  
 });

根据 cmets,这里有一个如何在对象中使用 ajax 的示例

function someObj() 
{
    this.someVar = 1234;

    this.ajaxCall = function(url) {
        var req = new XMLHttpRequest();  
        req.open('GET', url, true);  

        var me = this; // <-- "close" this

        req.onreadystatechange = function () {  
            if(req.readyState == 4) {
                // save data...
                me.data = req.responseText;     
                // ...and/or process it right away
                me.process(req.responseText);   

            }
        }
        req.send(null);  
    }

    this.process = function(data) {
        alert(this.someVar); // we didn't lost the context
        alert(data);         // and we've got data!
    }
}


o = new someObj;
o.ajaxCall("http://....");

想法是在事件处理程序中“关闭”(别名)“this”,以便进一步传递。

【讨论】:

  • 您好,感谢您的回复!我(想我)知道我的 AJAX 方法,并且我已经使用回调函数来处理响应。问题是另一个问题:基本上我想做的是让 AJAX 回调将数据保存到它所属的函数的实例(即通过 XHR 获取 xml 数据的函数)。这样,数据就可以为所有其他想要使用它们的函数做好准备,我只需为同一组数据而不是多个数据进行一次 AJAX 调用(如果文件很大或连接速度很慢,这会派上用场)。
  • 在这种情况下,如果数据实际上还没有准备好,那么任何函数都会导致错误。而这正是我想要找到的解决方案。
  • 嗯,我不确定我明白了。也许你可以发布一些伪代码......很可能你需要的是一个闭包
  • @FK82,我不确定我是否理解你,但你是否正在考虑缓存服务器的响应,以便将来的请求可以从缓存中提供,而不是实际的 HTTP 调用?
  • @stereofrog:基本上是这样的,我有一个类,函数 metaFunction() { this.AjaxCall = function(url,args) { this.trigger = 0 ;变量触发器 = 1 ;这个.数据; var 数据 = 这个。数据 var rqs = new XMLHttpRequest() ; ... rqs.onreadystatechange = function() { ... data = rqst.responseText ;触发器 = 1 ; ... } ; rqs.send(null) ; ... } ; this.IWantData = function(obj) { //使用数据 } ; }
【解决方案3】:

欢迎来到 SO!顺便说一句,你完全是个傻瓜,你的问题完全不清楚:)

这是基于@Daniel 使用延续的回答。这是一个将多个方法链接在一起的简单函数。就像管道 | 在 unix 中的工作方式一样。它以一组函数作为其参数,这些函数将按顺序执行。每个函数调用的返回值作为参数传递给下一个函数。

function Chain() {
    var functions = arguments;

    return function(seed) {
        var result = seed;

        for(var i = 0; i < functions.length; i++) {
            result = functions[i](result);
        }

        return result;
    }
}

要使用它,请从Chained 创建一个对象,并将所有函数作为参数传递。您可以test on fiddle 的示例是:

​var chained = new Chain(
    function(a) { return a + " wo"; },
    function(a) { return a + "r"; },
    function(a) { return a + "ld!"; }
);

alert(chained('hello')); // hello world!

​要将其与 AJAX 请求一起使用,请将链式函数作为成功回调传递给 XMLHttpRequest。

​var callback = new Chain(
    function(response) { /* do something with ajax response */ },
    function(data) { /* do something with filtered ajax data */ }
);

var req = new XMLHttpRequest();  
req.open('GET', url, true);  
req.onreadystatechange = function (aEvt) {  
    if(req.readyState == 4)
        callback(req.responseText);
}
req.send(null);  

重要的是每个函数都依赖于前一个函数的输出,所以你必须在每个阶段返回一些值。


这只是一个建议 - 负责检查数据是否在本地可用或必须发出 HTTP 请求将增加系统的复杂性。相反,您可以拥有一个不透明的请求管理器,就像您拥有的 metaFunction 一样,并让它决定是在本地还是远程提供数据。

这是一个sample Request object,它可以在没有任何其他对象或函数知道数据从哪里提供的情况下处理这种情况:

var Request = {
    cache: {},

    get: function(url, callback) {
        // serve from cache, if available
        if(this.cache[url]) {
            console.log('Cache');
            callback(this.cache[url]);
            return;
        }
        // make http request
        var request = new XMLHttpRequest();
        request.open('GET', url, true);
        var self = this;
        request.onreadystatechange = function(event) {
            if(request.readyState == 4) {
                self.cache[url] = request.responseText;
                console.log('HTTP');
                callback(request.responseText);
            }
        };
        request.send(null);
    }
};

要使用它,您可以调用Request.get(..),如果可用,它会返回缓存数据,否则会进行 AJAX 调用。如果您正在寻找对缓存的精细控制,可以传递第三个参数来控制数据的缓存时间。

Request.get('<url>', function(response) { .. }); // HTTP
// assuming the first call has returned by now
Request.get('<url>', function(response) { .. }); // Cache
Request.get('<url>', function(response) { .. }); // Cache

【讨论】:

  • 谢谢。对于链式执行,这是非常酷的代码。使用 arguments 属性传递函数是一个非常好的主意。然而,我真正想要的是在没有函数链的情况下检查数据(例如,取决于用户输入)。不要误会我的意思,链是一个好主意,可能是最佳实践,但如果您的 xml 较大,这不是一个好选择,因为浏览器将占用大量资源来处理每个调用。因此,我只想要一个调用,其结果会保存到窗口的某个子对象(例如我上面谈到的元函数)。
  • 我同意,链接可能是解决某些问题的方法,但不适用于这个问题。我已更新我的答案以包括响应缓存以避免开销。
【解决方案4】:

我已经解决了,现在它似乎工作得很好。我将在整理后稍后发布我的代码。同时,非常感谢您的帮助!

更新

在 Webkit(Safari、Chrome)、Mozilla 和 Opera 中尝试了代码。似乎工作得很好。期待任何回复。

更新 2

我更改了 tieExc() 方法以集成 Anurag 的链式函数调用语法。现在,您可以通过将它们作为参数传递来在完成检查时调用任意数量的函数。

如果您不喜欢阅读代码,请尝试:http://jsfiddle.net/UMuj3/(顺便说一句,JSFiddle 是一个非常简洁的网站!)。

JS-代码:

/**/function meta() {

var myMeta = this ;

/**  **/this.cllFnc = function(tgt,lgt) { //!! first function

    this.trigger = 0 ;  //!! status flag, initially zero
    var that = this ;   //!! required to access parent scope from inside nested function

    var _tgt = document.getElementById(tgt) ; //!! changes the color of the target div to signalize the function's execution
    _tgt.style.background = '#66f' ;

    alert('Calling! ...') ;

    setTimeout(function() { //!! simulates longer AJAX call, duration 5000ms

        that.trigger = 1 ;  //!! status flag, one upon completion

    },5000) ;

} ;

/**  **/this.rcvFnc = function(tgt) { //!! second function that should get called upon the first function's completion

    var _tgt = document.getElementById(tgt) ; //!! changes color of the target div to signalize the function's execution
    _tgt.style.background = '#f63' ;

    alert('... Someone picked up!') ;

} ;

/**  **/this.callCheck = function(obj) {    

    return (obj.trigger == 1)   ?   true
        :   false
        ;

} ;

/**  **/this.tieExc = function() {

    var functions = arguments ;

    var myItv = window.setInterval(function() {

        document.getElementById('target').innerHTML = new Date() ; //!! displays date in target div to signalize the interval increments

        var myCallCheck = myMeta.callCheck(functions[0]) ; //!! checks property "trigger"

        if(myCallCheck == true) { 

            clearInterval(myItv) ;

            for(var n=1; n < functions.length; n++) {

                functions[n].call() ;

            }

        } else if(myCallCheck == false) { 
            return ;
        }

    },100) ;



} ;

}​

HTML

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/strict.dtd >

<html>

<head>

    <script type='text/javascript'  >
        <!-- see above -->
    </script>
    <title>

      Javascript Phased Execution Test Page

    </title>

</head>

<body>

        <div id='target' style='float:left ; height:7.5em ; width:10em ; padding:0.5em 0 0 0; font-size:4em ; text-align:center ; font-weight:bold ; color:#eee ; background:#fff;border:0.1em solid #555 ; -webkit-border-radius:0.5em ;' >
            Test Div
        </div>

        <div style="float:left;" >
            <input type="button" value="tieCalls()" onmousedown="var myMeta = new meta() ; var myCll = new myMeta.cllFnc('target') ; new myMeta.tieExc(myCll, function() { myMeta.rcvFnc('target') ; }, function() { alert('this is fun stuff!') ; } ) ;" /><br />
        </div>

<body>


</html>

【讨论】:

  • 你应该考虑使用 jQuery - 如果它在每个浏览器中都能正确运行,我会感到惊讶。!
  • 如果我想知道的主要浏览器之一有问题。但是,您似乎没有指出一个。
  • 一个离题的建议:看看 Prototype 库中的 bind 函数:prototypejs.org/api/function/bind - 它会给你一个很好的方法来避免 var that = this; 解决方法。更好的是,当 ECMAScript 5 实现时,它将成为 JavaScript 本身的一部分,因此它完全面向未来。
  • 我又来了!这是一个不使用轮询的修改版本:jsfiddle.net/VPz4s/7 HTML 部分未更改。行为上的唯一区别是它不会在每次轮询时显示日期,因为它不是轮询。
  • @Daniel Earwicker 你的回复真的是从今年开始的,还是直到现在才发布?
【解决方案5】:

一个非常简单的解决方案是让您的第一个 ajax 调用同步。它是可选参数之一。

【讨论】:

  • @James Westgate:我尝试的第一件事,它在 Webkit (Safari) 中有效,但在 Moz 和 Opera 中无效(还没有尝试过 IE)。
猜你喜欢
  • 2018-03-13
  • 2019-10-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多