代码之家  ›  专栏  ›  技术社区  ›  FK82

在javascript中“分阶段”执行函数

  •  5
  • FK82  · 技术社区  · 16 年前

    这是我在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

    5 回复  |  直到 14 年前
        1
  •  8
  •   Daniel Earwicker    14 年前

    您可以利用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
    }
    

    “延续传递样式”是指回调。每个函数调用回调(continue)并给出结果,而不是返回值。

    然后您可以轻松地将两个连接在一起:

    foo(input1, function(results1) {
    
        bar(results1, function(results2) {
    
            alert(results2);
        });
    });
    

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

    更新

    为了澄清,在您的问题代码片段中,很明显您大致是这样想的:

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

    (然后作为一个复杂的因素,循环必须使用 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 属性转换为名为 触发 . 然后在结尾处,将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 调用将接收到的值传递给我们传递它的函数,因此 回拨 已经兼容,因此可以直接作为第二个Ajax调用的回调。因此,这本身就是一个将两个异步调用按顺序捆绑在一起并将它们包装在(从外部)看起来是单个异步函数的示例。

    然后我们可以用另一个操作链接这个:

    function displayBillAmount(amount) {
    
        $("#billAmount").text(amount); 
    }
    
    getCustomerBillAmount("Simpson, Homer J.", displayBillAmount);
    

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

    getCustomerBillAmount("Simpson, Homer J.", function(amount) {
    
        $("#billAmount").text(amount); 
    });
    

    因此,通过像这样链接函数调用,每一步都可以在可用时将信息转发到下一步。

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

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

    更新3

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

    更新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);
    

    当一个呼叫结束时,它将呼叫所有三个订户。

        2
  •  5
  •   user187291    16 年前

    对这个“准备好就打电话给我”问题的明确回答是 回拨 . 回调基本上是一个分配给对象属性的函数(如“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  
     });
    

    根据注释,这里有一个如何在对象中使用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”,以便进一步传递它。

        3
  •  1
  •   Anurag    16 年前

    欢迎来到这里!顺便说一句,你被认为是个十足的笨蛋,你的问题完全不清楚:)

    这是基于@daniel关于使用continuations的回答。它是一个简单的函数,将多个方法链接在一起。很像管道 | 在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
    
        4
  •  1
  •   FK82    16 年前

    我已经解决了,现在看来效果很好。我会在整理完代码后再发布。同时,非常感谢您的帮助!

    更新

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

    更新2

    我更改了tieexc()方法以集成Anurag的链接函数调用语法。现在,通过将函数作为参数传递,可以在完成检查时调用任意多个函数。

    如果您不想阅读代码,请尝试: http://jsfiddle.net/UMuj3/ (顺便说一句,jfiddle是一个非常整洁的站点!).

    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>
    
        5
  •  0
  •   James Westgate    16 年前

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

    推荐文章