【问题标题】:How to implement dependency between asynchronous functions in JavaScript?如何在 JavaScript 中实现异步函数之间的依赖关系?
【发布时间】:2015-11-06 09:52:19
【问题描述】:

作为一个简化的例子,我有两个异步函数,foo 和 bar。 bar 需要foo 的结果,即bar 依赖于foo。我不知道会先调用哪个函数。

  1. 如果首先调用bar,bar 将调用foo 并在foo 完成后立即启动。
  2. 如果首先调用foo 并完成,bar 可以使用foo 的结果。
  3. 如果先调用foo,然后在foo完成之前调用bar,则bar需要等待foo的结果。 (不要调用foo 的新调用,只需等待已经调用的foo)

我怎样才能做到这一点?
是否可以注册一个异步函数依赖链(类似于 require.js define['foo'], function() { bar(); } 中的依赖)?
我可以使用$.deferred()来实现吗?
如何?

【问题讨论】:

    标签: javascript jquery asynchronous dependencies deferred


    【解决方案1】:

    在这种情况下,标准方法是缓存较低级别的 Promise。

    通常,您会在某个合适的外部范围内建立一个 js 普通对象作为 Promise 缓存,并始终在调用异步进程之前先查看那里。

    var promiseCache = {};
    
    function foo() {
        if(!promiseCache.foo) {
            promiseCache.foo = doSomethingAsync();
        }
        return promiseCache.foo;
    }
    
    function bar() {
        return foo().then(doSomethingElseAsync);
    }
    

    当然,如果合适的话,没有什么可以阻止你缓存更高级别的 Promise。

    function bar() {
        if(!promiseCache.bar) {
            promiseCache.bar = foo().then(doSomethingElseAsync);
        }
        return promiseCache.bar;
    }
    

    编辑:forceRefresh 功能

    您可以通过传递(额外)参数来强制函数刷新其缓存的承诺。

    function foo(any, number, of, other, arguments, forceRefresh) {
        if(forceRefresh || !promiseCache.foo) {
            promiseCache.foo = doSomethingAsync();
        }
        return promiseCache.foo;
    }
    

    通过将forceRefresh 作为最后一个参数,将其省略与传递false 和foo 将使用缓存的promise(如果可用)。或者,传递true 以保证调用doSomethingAsync() 并刷新缓存值。

    编辑 2:setName()/getName()

    在getName() 中使用forceRefresh 机制:

    setName(newName).then(getName.bind(null, true)); //set new name then read it back using forceRefresh.
    

    或者,省略forceRefresh 机制,并假设缓存属性为promiseCache.name:

    setName(newName).then(function() {
        promiseCache.name = $.when(newName);//update the cache with a simulated `getName()` promise.
    });
    

    第一种方法更优雅,第二种更高效。

    【讨论】:

    • 谢谢。我正在使用你的方法。一个后续问题:如果有一个函数setFoo 会改变你的foo 函数返回的值,你将如何做出相应的改变?我的想法是将promiseCache.foo = $.when(newFoo) 添加到setFoo。
    • @Moon ,我认为您正在描述 forceRefresh 功能。我已经进行了编辑。
    • 谢谢。就我而言,我有函数getName 和setName,我将getName 保存在promiseCache 中。 setName 将发送ajax 到服务器来设置name。如果setName成功了,我不想forceRefreshgetName,而是更新getName的promise。
    • @Moon,看看编辑 2。
    【解决方案2】:

    如果您打算在代码中经常使用这种模式,您还可以创建一个在 define 模型上工作的函数 require.js 中的函数。

    你需要:

    • 保存依赖函数的注册表(在您的示例中为foo)

    • 依赖函数(在您的示例中为bar)将需要接受依赖函数计算值作为其签名的一部分。例如,可以将依赖项的哈希作为第一个参数传递,因此条形签名可以是:{foo: foo_result}, other_bar_args...

    • 依赖函数必须遵循我之前回答的模型,即在执行时将它们的承诺值注册为自身的属性。

    • 提醒:您需要命名这些依赖函数以在其主体中引用它们,然后将该对象添加到注册表中。

    在define 函数体中,您将依赖函数包装到另一个函数体中:

    • 从注册表中获取所有依赖项

    • 获取所有依赖项值,必要时执行依赖项(类似于我之前的回答)。这意味着您最终会得到一个承诺列表,然后您将其结果聚集在一起(RSVP.hash,例如使用 RSVP 承诺库)。我相信jQuery与jQuery.when有类似的功能

    • 您调用依赖函数 (bar),将此结果哈希作为第一个参数,其他参数与包装函数相同

    • 那个被包裹的函数就是新的bar,所以当bar被调用的时候,就是被包裹的函数会被调用。

    有点冗长,但应该可以。如果您想查看一些代码,请告诉我这是否是您要查找的内容。无论如何,如果您要进行复杂的异步操作。在您的代码中,使用兼容的 Promise 库对您来说可能会很有趣。 $.deferred 也只能在你没有什么更好的情况下使用,因为它使你更难跟踪你的函数的行为:你需要跟踪这个 deferred 似乎能够推理你的所有地方程序。

    【讨论】:

      【解决方案3】:

      不确定我是否正确理解了这个问题。但这是我的看法:

      • 把你的函数 foo 放入一个变量中

        var foo_fn = function foo(foo_args){// Your async code goes here}

      • foo 是异步的,并且会在某些时候返回一些东西。在您对 foo 的定义中,我建议您使用 Promise,该概念旨在以一种干净且可扩展的方式管理异步函数的组合。该概念的 jQuery 实现在许多简单的用例中都很方便,但存在一些缺点,这使得您在某些时候使用遵循 Promises/A 规范的众多 Promise 库中的一个会很有趣。更多信息,您可以参考: 参照。 https://thewayofcode.wordpress.com/2013/01/22/javascript-promises-and-why-jquery-implementation-is-broken/ 和 https://blog.domenic.me/youre-missing-the-point-of-promises

      • 因此,假设 foo 接受 args,并返回一个承诺,该承诺稍后会解析为某个值。

        var foo_fn = function foo(foo_args) { return foo_fn.promise = new RSVP.Promise (resolve, reject) {
        // Your async code goes here } }

        这里我使用 RSVP 承诺库,但任何遵循 Promises/A 规范的承诺库都可以完成这项工作。

      • 当 bar 被调用时,你可以这样做:

        function bar (bar_args) { var foo_promise = foo_fn.promise; // if foo was called, whether the computation is in progress or finished, // the foo_fn.promise field will be non-empty, as foo returns immediately // with a promise anytime it is called `` if (!foo.promise) { // foo has not yet been called so call it foo_promise = foo(foo_args); } foo_promise.then (function (foo_result) {/*some async code here*/}) }

      注意:该解决方案与Roamer-1888 提出的解决方案非常相似。不同之处在于,在 Roamer 提案中,foo 函数在执行一次异步计算后将始终返回相同的值。不知道这是否是预期的行为。在我的实现中, foo 执行异步。每次调用时计算。 bar 将使用存储在字段foo_fn.promise 中的最新计算值。较旧的计算会丢失,可能正在进行的计算不会被考虑在内。

      【讨论】:

        【解决方案4】:

        尝试创建具有可能值的对象属性 undefined , "pending" , true ;当obj.active 为true 时调用deferred.resolve(),deferred.reject() 当obj.active 为“待定”时调用deferred.reject()

        var res = {
          active: void 0
        };
        
        var foo = function foo(state) {
          var t;
          var deferred = function(type) {
            return $.Deferred(function(dfd) {
                
                if (res.active === "pending" || state && state === "pending") {
                  res.active = "pending";
                  dfd.rejectWith(res, [res.active])
                } else {
                  res.active = state || "pending";
                  t = setInterval(function() {
                    console.log(res.active)
                  }, 100);
                    setTimeout(function() {
                      clearInterval(t)
                      res.active = true;
                      dfd.resolveWith(res, [res.active])
                    }, 3000);
                }
                return dfd.promise()
              })
              .then(function(state) {
                console.log("foo value", state);
                return state
              }, function(err) {
                console.log("foo status", err)
                return err
              })
          }
          return deferred()
        }
        
        var bar = function bar(result) {
          var deferred = function(type) {
            return $.Deferred(function(dfd) {
              if (result && result === true) {
                setTimeout(function() {
                  dfd.resolveWith(result, [true])
                }, 1500)
              } else {
                dfd.rejectWith(res, [res.active || "pending"])
              };
              return dfd.promise()
            })
          }
          return deferred().then(function(data) {
            console.log("bar value", data);
          }, function(err) {
            console.log("bar status", err);
          })
        }
        
        $("button").click(function() {
          $(this).is(":first") 
          ? foo().then(bar, bar) 
          : bar(res.active === true ? res.active : "pending")
            .then(foo, foo).then(bar, bar)
        })
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
        </script>
        <button>foo</button>
        <button>bar</button>

        【讨论】:

          【解决方案5】:

          您可以简单地将这两个功能视为独立的。这样,您就不会使用异步操作的菊花链依赖关系。然后,您可以拥有另一个使用它们的模块。

          由于他们执行异步操作,请考虑使用 Promise。您可以使用 jQuery 的延迟来实现兼容性。将延迟视为读/写,而承诺是只读的。

          // foo.js
          define(function(){
            return function(){
              return new Promise(function(resolve, reject){
                // Do async stuff. Call resolve/reject accordingly
              });
            };
          });
          
          // bar.js
          define(function(){
            return function(){
              return new Promise(function(resolve, reject){
                // Do async stuff. Call resolve/reject accordingly
              });
            };
          });
          
          // Your code (Excuse the CommonJS format. Personal preference)
          define(function(require){
          
            // Require both functions
            var foo = require('foo');
            var bar = require('bar');
          
            // Use them
            foo(...).then(function(response){
              return bar();
            }).then(function(){
              // all done
            });;
          
          });
          

          【讨论】:

          • 感谢您对“将延迟视为读/写而承诺是只读的”的解释。但我认为你的解决方案有点贵。我有很多foo, bar等,每个都需要创建一个js文件。
          • @Moon 这就是模块的工作方式。文件的数量无关紧要。 RequireJS 还有一个优化器,可以将依赖项捆绑到一个文件中。
          • @Jonseph。是的。但就我而言,某些功能在逻辑上属于一个模块。我倾向于不将它们分开。
          猜你喜欢
          • 1970-01-01
          • 2021-07-05
          • 1970-01-01
          • 2020-07-20
          • 1970-01-01
          • 2019-09-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多