异步函数外部堆栈上下文
async function external stack context
有时代码想知道特定函数(或子函数)是否为 运行。例如,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
函数可能也可以工作(未测试)。
[is it] possible to tell if the function called, or any of its descendant calls are running?
是的。答案永远是否定的。因为一次只有一段代码运行。 Javascript 根据定义是单线程的。
不要让它变得比需要的更复杂。如果 doUpdate
returns 一个承诺(比如当它是一个 async function
时),只需等待:
inUpdate = true;
try {
await doUpdate();
//^^^^^
} finally {
inUpdate = false;
}
您也可以使用 finally
Promise method:
var inUpdate = true;
doUpdate().finally(() => {
inUpdate = false;
});
这就像您的同步代码一样,具有 inUpdate == true
而函数调用或其任何后代是 运行。当然,这只有在异步函数在完成它的事情之前没有解决承诺时才有效。如果您觉得 inUpdate
标志只应在 doUpdate
函数的某些特定部分设置,那么是的,该函数将需要维护标志本身 - 就像同步代码的情况一样.
这段代码可以让我在一定程度上做我想做的事情:
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。此承诺将当前上下文 (taskObj) 存储在承诺中。
当调用 .then
回调或类似回调时,它会执行以下操作:
它创建了一个立即解决的新本地承诺。这会在队列中添加一个新的 microtask(根据规范,所以应该是可靠的)。
调用原来的resolve or reject。至少在 Chrome 和 Firefox 中,这会在队列中生成另一个微任务,它将 运行 异步函数的下一部分。不确定规范对此有何评论。它还会恢复调用周围的上下文,以便如果不是 await
使用它,则不会在此处添加微任务。
第一个微任务被执行,这是我的第一个(本地)承诺被解决。此代码恢复当前上下文 (taskObj)。它还创建了一个新的已解决的承诺,将另一个微任务排队
第二个微任务(如果有的话)被执行,运行将异步函数中的 JS 连接到下一个 await
或 returns。
第一个微任务排队的微任务被执行,它将上下文恢复到 Promise 之前的状态 resolved/rejected(应该总是 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')
显然,需要针对所有不同环境进行大量测试,而且需要针对 sub-calls 的解决方法这一事实令人痛苦。此外,所有使用的 Promise 都需要包装在 Promise.resolve()
中或使用全局 Promise
.
有时代码想知道特定函数(或子函数)是否为 运行。例如,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
函数可能也可以工作(未测试)。
[is it] possible to tell if the function called, or any of its descendant calls are running?
是的。答案永远是否定的。因为一次只有一段代码运行。 Javascript 根据定义是单线程的。
不要让它变得比需要的更复杂。如果 doUpdate
returns 一个承诺(比如当它是一个 async function
时),只需等待:
inUpdate = true;
try {
await doUpdate();
//^^^^^
} finally {
inUpdate = false;
}
您也可以使用 finally
Promise method:
var inUpdate = true;
doUpdate().finally(() => {
inUpdate = false;
});
这就像您的同步代码一样,具有 inUpdate == true
而函数调用或其任何后代是 运行。当然,这只有在异步函数在完成它的事情之前没有解决承诺时才有效。如果您觉得 inUpdate
标志只应在 doUpdate
函数的某些特定部分设置,那么是的,该函数将需要维护标志本身 - 就像同步代码的情况一样.
这段代码可以让我在一定程度上做我想做的事情:
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。此承诺将当前上下文 (taskObj) 存储在承诺中。
当调用 .then
回调或类似回调时,它会执行以下操作:
它创建了一个立即解决的新本地承诺。这会在队列中添加一个新的 microtask(根据规范,所以应该是可靠的)。
调用原来的resolve or reject。至少在 Chrome 和 Firefox 中,这会在队列中生成另一个微任务,它将 运行 异步函数的下一部分。不确定规范对此有何评论。它还会恢复调用周围的上下文,以便如果不是
await
使用它,则不会在此处添加微任务。第一个微任务被执行,这是我的第一个(本地)承诺被解决。此代码恢复当前上下文 (taskObj)。它还创建了一个新的已解决的承诺,将另一个微任务排队
第二个微任务(如果有的话)被执行,运行将异步函数中的 JS 连接到下一个
await
或 returns。第一个微任务排队的微任务被执行,它将上下文恢复到 Promise 之前的状态 resolved/rejected(应该总是
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')
显然,需要针对所有不同环境进行大量测试,而且需要针对 sub-calls 的解决方法这一事实令人痛苦。此外,所有使用的 Promise 都需要包装在 Promise.resolve()
中或使用全局 Promise
.