【问题标题】:async function external stack context异步函数外部堆栈上下文
【发布时间】:2018-08-20 19:51:31
【问题描述】:

有时代码想知道某个特定函数(或子函数)是否正在运行。例如,node.js 有 domains 也适用于异步内容(不确定这是否包括异步函数)。

一些简单的代码来解释我需要这样的东西:

inUpdate = true;
try {
  doUpdate();
} finally {
  inUpdate = false;
}

然后可以这样使用:

function modifyThings() {
  if (inUpdate) throw new Error("Can't modify while updating");
}

随着 async 的出现,如果 doUpdate() 函数是异步的,则此代码会中断。使用回调风格的函数当然已经是这样了。

当然可以修补doUpdate 函数以维护每个await 周围的变量,但是即使您可以控制代码,这也很麻烦且容易出错,并且在尝试跟踪内部异步函数调用时会中断doUpdate.

我尝试猴子修补 Promise.prototype:

const origThen = Promise.prototype.then;
Promise.prototype.then = function(resolve, reject) {
  const isInUpdate = inUpdate;
  origThen.call(this, function myResolve(value) {
    inUpdate = isInUpdate;
    try {
      return resolve(value);
    } finally {
      inUpdate = false;
    }
  }, reject);
}

不幸的是,这不起作用。我不知道为什么,但异步延续代码最终会在 resolve 调用堆栈之外运行(可能使用微任务)。

注意,仅仅做是不够的:

function runUpdate(doUpdate) {
  inUpdate = true;
  doUpdate.then(() => inUpdate = false).catch(() => inUpdate = false);
}

原因是:

runUpdate(longAsyncFunction);
console.log(inUpdate); // incorrectly returns true

是否有任何方法可以从异步函数外部跟踪某些内容,以便判断被调用的函数或其任何后代调用是否正在运行?

我知道可以使用生成器和yield 模拟异步函数,在这种情况下我们可以控制调用堆栈(因为我们可以调用gen.next())但这是异步函数的出现所带来的麻烦开始解决问题,所以我特意寻找一种适用于本机(不是 Babel 生成的)异步函数的解决方案。

编辑: 澄清问题:假设此代码是异步函数。它是否正在运行将由最终由异步函数(堆栈中的某处)调用的函数确定。

编辑:进一步澄清:预期功能与 node.js 中的domains 相同,但也适用于浏览器。域已经可以使用 Promises,所以async 函数可能也可以正常工作(未经测试)。

【问题讨论】:

  • 你的问题仍然没有任何意义。同样,您是否在寻求一种方法来判断异步调用是否排队?

标签: javascript asynchronous ecmascript-2017


【解决方案1】:

这段代码让我可以在一定程度上做我想做的事:

function installAsyncTrack() {
  /* global Promise: true */
  if (Promise.isAsyncTracker) throw new Error('Only one tracker can be installed');

  const RootPromise = Promise.isAsyncTracker ? Promise.rootPromise : Promise;
  let active = true;

  const tracker = {
    track(f, o, ...args) {
      const prevObj = tracker.trackObj;
      tracker.trackObj = o;
      try {
        return f.apply(this, args);
      } finally {
        tracker.trackObj = prevObj;
      }
    },
    trackObj: undefined,
    uninstall() {
      active = false;
      if (Promise === AsyncTrackPromise.prevPromise) return;
      if (Promise !== AsyncTrackPromise) return;
      Promise = AsyncTrackPromise.prevPromise;
    }
  };

  AsyncTrackPromise.prototype = Object.create(Promise);
  AsyncTrackPromise.rootPromise = RootPromise;
  AsyncTrackPromise.prevPromise = Promise;
  Promise = AsyncTrackPromise;
  AsyncTrackPromise.resolve = value => {
    return new AsyncTrackPromise(resolve => resolve(value));
  };
  AsyncTrackPromise.reject = val => {
    return new AsyncTrackPromise((resolve, reject) => reject(value));
  };
  AsyncTrackPromise.all = iterable => {
    const promises = Array.from(iterable);
    if (!promises.length) return AsyncTrackPromise.resolve();
    return new AsyncTrackPromise((resolve, reject) => {
      let rejected = false;
      let results = new Array(promises.length);
      let done = 0;
      const allPromises = promises.map(promise => {
        if (promise && typeof promise.then === 'function') {
          return promise;
        }
        return new AsyncTrackPromise.resolve(promise);
      });
      allPromises.forEach((promise, ix) => {
        promise.then(value => {
          if (rejected) return;
          results[ix] = value;
          done++;
          if (done === results.length) {
            resolve(results);
          }
        }, reason => {
          if (rejected) return;
          rejected = true;
          reject(reason);
        });
      });
    });
  };
  AsyncTrackPromise.race = iterable => {
    const promises = Array.from(iterable);
    if (!promises.length) return new AsyncTrackPromise(() => {});
    return new AsyncTrackPromise((resolve, reject) => {
      let resolved = false;
      if (promises.some(promise => {
          if (!promise || typeof promise.then !== 'function') {
            resolve(promise);
            return true;
          }
        })) return;
      promises.forEach((promise, ix) => {
        promise.then(value => {
          if (resolved) return;
          resolved = true;
          resolve(value);
        }, reason => {
          if (resolved) return;
          resolved = true;
          reject(reason);
        });
      });
    });
  };

  function AsyncTrackPromise(handler) {
    const promise = new RootPromise(handler);
    promise.trackObj = tracker.trackObj;

    promise.origThen = promise.then;
    promise.then = thenOverride;

    promise.origCatch = promise.catch;
    promise.catch = catchOverride;

    if (promise.finally) {
      promise.origFinally = promise.finally;
      promise.finally = finallyOverride;
    }
    return promise;
  }

  AsyncTrackPromise.isAsyncTracker = true;

  function thenOverride(resolve, reject) {
    const trackObj = this.trackObj;
    if (!active || trackObj === undefined) return this.origThen.apply(this, arguments);
    return this.origThen.call(
      this,
      myResolver(trackObj, resolve),
      reject && myResolver(trackObj, reject)
    );
  }

  function catchOverride(reject) {
    const trackObj = this.trackObj;
    if (!active || trackObj === undefined) return this.origCatch.catch.apply(this, arguments);
    return this.origCatch.call(
      this,
      myResolver(trackObj, reject)
    );
  }

  function finallyOverride(callback) {
    const trackObj = this.trackObj;
    if (!active || trackObj === undefined) return this.origCatch.catch.apply(this, arguments);
    return this.origCatch.call(
      this,
      myResolver(trackObj, reject)
    );
  }

  return tracker;

  function myResolver(trackObj, resolve) {
    return function myResolve(val) {
      if (trackObj === undefined) {
        return resolve(val);
      }
      RootPromise.resolve().then(() => {
        const prevObj = tracker.trackObj;
        tracker.trackObj = trackObj;
        RootPromise.resolve().then(() => {
          tracker.trackObj = prevObj;
        });
      });
      const prevObj = tracker.trackObj;
      tracker.trackObj = trackObj;
      try {
        return resolve(val);
      } finally {
        tracker.trackObj = prevObj;
      }
    };
  }

}

tracker = installAsyncTrack();

function track(func, value, ...args) {
  return tracker.track(func, { value }, value, ...args);
}

function show(where, which) {
  console.log('At call', where, 'from', which, 'the value is: ', tracker.trackObj && tracker.trackObj.value);
}

async function test(which, sub) {
  show(1, which);
  await delay(Math.random() * 100);
  show(2, which);
  if (sub === 'resolve') {
    await Promise.resolve(test('sub'));
    show(3, which);
  }
  if (sub === 'call') {
    await test(which + ' sub');
    show(3, which);
  }
}

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

track(test, 'test1');
track(test, 'test2');
track(test, 'test3', 'resolve');
track(test, 'test4', 'call');

它用我自己的 Promise 替换了原生 Promise。这个 Promise 将当前上下文 (taskObj) 存储在 Promise 上。

.then 回调或其同类被调用时,它会执行以下操作:

  • 它创建了一个新的本机承诺,该承诺立即解决。这会在队列中添加一个新的microtask(根据规范,因此应该是可靠的)。

  • 它调用原始解析或拒绝。至少在 Chrome 和 Firefox 中,这会在队列中生成另一个微任务,该微任务将运行异步函数的下一部分。尚不确定规范对此有何评论。它还会恢复调用周围的上下文,因此如果不是 await 使用它,则不会在此处添加微任务。

  • 第一个微任务被执行,这是我的第一个(本机)承诺得到解决。此代码恢复当前上下文 (taskObj)。它还创建了一个新的已解决承诺,该承诺将另一个微任务排队

  • 第二个微任务(如果有)被执行,在异步函数中运行 JS 直到它到达下一个await 或返回。

  • 由第一个微任务排队的微任务被执行,这会将上下文恢复到 Promise 解决/拒绝之前的状态(应始终为 undefined,除非在 tracker.track(...) 调用之外设置)。

如果截获的 Promise 不是原生的(例如 bluebird),它仍然可以工作,因为它会在 resolve(...)(和类似的)调用期间恢复状态。

有一种情况我似乎找不到解决方案:

tracker.track(async () => {
  console.log(tracker.taskObj); // 'test'
  await (async () => {})(); //This breaks because the promise generated is native
  console.log(tracker.taskObj); // undefined
}, 'test')

一种解决方法是将承诺包装在Promise.resolve()

tracker.track(async () => {
  console.log(tracker.taskObj); // 'test'
  await Promise.resolve((async () => {})());
  console.log(tracker.taskObj); // undefined
}, 'test')

显然,需要对所有不同的环境进行大量测试,并且需要针对子调用的解决方法这一事实令人痛苦。此外,所有使用的 Promise 都需要包装在 Promise.resolve() 中或使用全局 Promise

【讨论】:

  • 我知道这是一个非常古老的评论,但是对于子调用,上面的输出应该是什么样子?他们似乎在被呼叫之前就失去了我的上下文?
  • 我已经放弃了。请注意“在一定程度上”免责声明。我现在正在寻找一个编译器解决方案,它将通过显式管理等待周围的上下文来维护上下文。请注意,作为语言功能,堆栈上下文的喋喋不休。不过我现在没有链接。它还没有准备好使用。
【解决方案2】:

[是否]可以判断调用的函数或其任何后代调用是否正在运行?

是的。答案始终是否定的。因为一次只运行一段代码。 Javascript 每个定义都是单线程的。

【讨论】:

  • 这并没有回答问题,我不确定你为什么提到 JS 是单线程的,因为它在这里似乎不相关。
  • @DDS 非常相关,JavaScript 具有运行到完成的语义。您不想知道这些函数是否运行,您似乎想知道它们是否排队
  • @DDS 不正确,它们总是运行到yieldreturn 或块的末尾。总是。避免您正在谈论的确切场景是他们拒绝添加协程的原因。
  • @DDS 那为什么是// incorrectly returns true ?承诺在那里是一个未决状态
  • 正如我们一直试图告诉您的那样,在 JavaScript 中不存在暂停执行这样的事情。甚至在发电机中也没有,尽管那是最接近的东西。当然不是在异步函数中。我投票结束这个。
【解决方案3】:

不要让它变得比它需要的更复杂。如果doUpdate 返回一个承诺(比如当它是一个async function),就等着吧:

inUpdate = true;
try {
  await doUpdate();
//^^^^^
} finally {
  inUpdate = false;
}

您也可以使用finally Promise method:

var inUpdate = true;
doUpdate().finally(() => {
  inUpdate = false;
});

这就像您的同步代码一样,在函数调用或其任何后代运行时具有inUpdate == true。当然,只有当异步函数在完成它的事情之前没有解决承诺时才有效。如果您觉得 inUpdate 标志应该只在 doUpdate 函数的某些特定部分设置,那么是的,该函数需要维护标志本身 - 就像同步代码的情况一样。

【讨论】:

  • 这不起作用,因为inUpdate 标志在从函数外部调用时也会为真,直到doUpdate 函数返回/解析。
  • @DDS “从外部”是什么意思,“直到doUpdate 函数返回”不正是您想要的吗?你需要这个actual, practical problem 是什么?
  • 我目前需要它来跟踪一个函数所做的更改,而不是其他一些函数所做的更改,而不需要该函数的合作。所以,如果一个函数调用makeChange(newData),我需要将它归于正确的调用者。
  • @DDS 这就是调用堆栈的用途——跟踪谁给你打电话。只需使用new Error().stack,它甚至可以与async functions 一起使用。不幸的是,没有更好的调用堆栈 API yet
  • 无法知道哪一行与函数匹配,因为我事先不知道将传入哪个函数。即使我知道了,我也不知道它是哪个调用实例,这对于跟踪更改很重要。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-03-08
  • 2016-07-14
  • 1970-01-01
  • 1970-01-01
  • 2021-11-21
  • 1970-01-01
  • 2021-05-26
相关资源
最近更新 更多