使用异步方法保持对象可链接
Keep object chainable using async methods
假设我有一个 class Test
大约有 10-20 个方法,所有这些方法都是可链接的。
在另一种方法中,我有一些异步工作要做。
let test = new Test();
console.log(test.something()); // Test
console.log(test.asynch()); // undefined since the async code isn't done yet
console.log(test.asynch().something()); // ERROR > My goal is to make this
由于所有其他方法都是可链接的,我觉得如果这个唯一的方法不是,对用户来说会很奇怪。
有没有办法让我维护 Class 的可链接 主题?
我已经考虑过在这个方法的参数内的回调函数中传递下一个方法,但这不是真的链接。
test.asynch(() => something())
与 Promises
相同,这不是 真正的 链接。
test.asynch().then(() => something())
我想要的结果是
test.asynch().something()
这是一个演示我的问题的片段:
class Test {
/**
* Executes some async code
* @returns {Test} The current {@link Test}
*/
asynch() {
if (true) { //Condition isn't important
setTimeout(() => { //Some async stuff
return this;
}, 500);
} else {
// ...
return this;
}
}
/**
* Executes some code
* @returns {Test} The current {@link Test}
*/
something() {
// ...
return this
}
}
let test = new Test();
console.log(test.something()); // Test
console.log(test.asynch()); // undefined
console.log(test.asynch().something()); // ERROR > My goal is to make this work.
我不认为现在可以使用这样的语法。它需要访问中函数中的承诺return它。
链接函数的不同方式:
然后答应
bob.bar()
.then(() => bob.baz())
.then(() => bob.anotherBaz())
.then(() => bob.somethingElse());
您还可以使用 compositions 来获得另一种风格的函数式、可重用语法来链接异步和同步函数
const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);
或使用异步/等待
for (const f of [func1, func2]) {
await f();
}
我怀疑做那样的事情真的是个好主意。
但是如果原始对象满足某些条件,使用 Proxy 将允许创建这样的行为。我强烈建议不要那样做。
请注意,此代码是概念证明,表明它在某种程度上是可行的,但不关心边缘情况,很可能会破坏某些功能。
一个代理用于包装原始 class Test
以便可以修补每个实例以使其可链接。
第二个将修补每个函数调用并为这些函数调用创建一个队列,以便按顺序调用它们。
class Test {
/**
* Executes some async code
* @returns {Test} The current {@link Test}
*/
asynch() {
console.log('asynch')
return new Promise((resolve, reject) => setTimeout(resolve, 1000))
}
/**
* Executes some code
* @returns {Test} The current {@link Test}
*/
something() {
console.log('something')
return this
}
}
var TestChainable = new Proxy(Test, {
construct(target, args) {
return new Proxy(new target(...args), {
// a promise used for chaining
pendingPromise: Promise.resolve(),
get(target, key, receiver) {
// intercept each get on the object
if (key === 'then' || key === 'catch') {
// if then/catch is requested, return the chaining promise
return (...args2) => {
return this.pendingPromise[key](...args2)
}
} else if (target[key] instanceof Function) {
// otherwise chain with the "chainingPromise"
// and call the original function as soon
// as the previous call finished
return (...args2) => {
this.pendingPromise = this.pendingPromise.then(() => {
target[key](...args2)
})
console.log('calling ', key)
// return the proxy so that chaining can continue
return receiver
}
} else {
// if it is not a function then just return it
return target[key]
}
}
})
}
});
var t = new TestChainable
t.asynch()
.something()
.asynch()
.asynch()
.then(() => {
console.log('all calles are finished')
})
正如在对 OP 的评论中所讨论的,这可以通过使用 Proxy.
来实现
我知道 t.niese 几个小时前提供了类似的答案。我的方法有些不同,但它仍然实质上是捕获方法调用、返回接收器并在内部堆叠 thennables。
class ProxyBase {
constructor () {
// Initialize a base thennable.
this.promiseChain = Promise.resolve();
}
/**
* Creates a new instance and returns an object proxying it.
*
* @return {Proxy<ProxyBase>}
*/
static create () {
return new Proxy(new this(), {
// Trap all property access.
get: (target, propertyName, receiver) => {
const value = target[propertyName];
// If the requested property is a method and not a reserved method...
if (typeof value === 'function' && !['then'].includes(propertyName)) {
// Return a new function wrapping the method call.
return function (...args) {
target.promiseChain = target.promiseChain.then(() => value.apply(target, args));
// Return the proxy for chaining.
return receiver;
}
} else if (propertyName === 'then') {
return (...args) => target.promiseChain.then(...args);
}
// If the requested property is not a method, simply attempt to return its value.
return value;
}
});
}
}
// Sample implementation class. Nonsense lies ahead.
class Test extends ProxyBase {
constructor () {
super();
this.chainValue = 0;
}
foo () {
return new Promise(resolve => {
setTimeout(() => {
this.chainValue += 3;
resolve();
}, 500);
});
}
bar () {
this.chainValue += 5;
return true;
}
baz () {
return new Promise(resolve => {
setTimeout(() => {
this.chainValue += 7;
resolve();
}, 100);
});
}
}
const test = Test.create();
test.foo().bar().baz().then(() => console.log(test.chainValue)); // 15
假设我有一个 class Test
大约有 10-20 个方法,所有这些方法都是可链接的。
在另一种方法中,我有一些异步工作要做。
let test = new Test();
console.log(test.something()); // Test
console.log(test.asynch()); // undefined since the async code isn't done yet
console.log(test.asynch().something()); // ERROR > My goal is to make this
由于所有其他方法都是可链接的,我觉得如果这个唯一的方法不是,对用户来说会很奇怪。
有没有办法让我维护 Class 的可链接 主题?
我已经考虑过在这个方法的参数内的回调函数中传递下一个方法,但这不是真的链接。
test.asynch(() => something())
与 Promises
相同,这不是 真正的 链接。
test.asynch().then(() => something())
我想要的结果是
test.asynch().something()
这是一个演示我的问题的片段:
class Test {
/**
* Executes some async code
* @returns {Test} The current {@link Test}
*/
asynch() {
if (true) { //Condition isn't important
setTimeout(() => { //Some async stuff
return this;
}, 500);
} else {
// ...
return this;
}
}
/**
* Executes some code
* @returns {Test} The current {@link Test}
*/
something() {
// ...
return this
}
}
let test = new Test();
console.log(test.something()); // Test
console.log(test.asynch()); // undefined
console.log(test.asynch().something()); // ERROR > My goal is to make this work.
我不认为现在可以使用这样的语法。它需要访问中函数中的承诺return它。
链接函数的不同方式:
然后答应
bob.bar()
.then(() => bob.baz())
.then(() => bob.anotherBaz())
.then(() => bob.somethingElse());
您还可以使用 compositions 来获得另一种风格的函数式、可重用语法来链接异步和同步函数
const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);
或使用异步/等待
for (const f of [func1, func2]) {
await f();
}
我怀疑做那样的事情真的是个好主意。 但是如果原始对象满足某些条件,使用 Proxy 将允许创建这样的行为。我强烈建议不要那样做。
请注意,此代码是概念证明,表明它在某种程度上是可行的,但不关心边缘情况,很可能会破坏某些功能。
一个代理用于包装原始 class Test
以便可以修补每个实例以使其可链接。
第二个将修补每个函数调用并为这些函数调用创建一个队列,以便按顺序调用它们。
class Test {
/**
* Executes some async code
* @returns {Test} The current {@link Test}
*/
asynch() {
console.log('asynch')
return new Promise((resolve, reject) => setTimeout(resolve, 1000))
}
/**
* Executes some code
* @returns {Test} The current {@link Test}
*/
something() {
console.log('something')
return this
}
}
var TestChainable = new Proxy(Test, {
construct(target, args) {
return new Proxy(new target(...args), {
// a promise used for chaining
pendingPromise: Promise.resolve(),
get(target, key, receiver) {
// intercept each get on the object
if (key === 'then' || key === 'catch') {
// if then/catch is requested, return the chaining promise
return (...args2) => {
return this.pendingPromise[key](...args2)
}
} else if (target[key] instanceof Function) {
// otherwise chain with the "chainingPromise"
// and call the original function as soon
// as the previous call finished
return (...args2) => {
this.pendingPromise = this.pendingPromise.then(() => {
target[key](...args2)
})
console.log('calling ', key)
// return the proxy so that chaining can continue
return receiver
}
} else {
// if it is not a function then just return it
return target[key]
}
}
})
}
});
var t = new TestChainable
t.asynch()
.something()
.asynch()
.asynch()
.then(() => {
console.log('all calles are finished')
})
正如在对 OP 的评论中所讨论的,这可以通过使用 Proxy.
来实现我知道 t.niese 几个小时前提供了类似的答案。我的方法有些不同,但它仍然实质上是捕获方法调用、返回接收器并在内部堆叠 thennables。
class ProxyBase {
constructor () {
// Initialize a base thennable.
this.promiseChain = Promise.resolve();
}
/**
* Creates a new instance and returns an object proxying it.
*
* @return {Proxy<ProxyBase>}
*/
static create () {
return new Proxy(new this(), {
// Trap all property access.
get: (target, propertyName, receiver) => {
const value = target[propertyName];
// If the requested property is a method and not a reserved method...
if (typeof value === 'function' && !['then'].includes(propertyName)) {
// Return a new function wrapping the method call.
return function (...args) {
target.promiseChain = target.promiseChain.then(() => value.apply(target, args));
// Return the proxy for chaining.
return receiver;
}
} else if (propertyName === 'then') {
return (...args) => target.promiseChain.then(...args);
}
// If the requested property is not a method, simply attempt to return its value.
return value;
}
});
}
}
// Sample implementation class. Nonsense lies ahead.
class Test extends ProxyBase {
constructor () {
super();
this.chainValue = 0;
}
foo () {
return new Promise(resolve => {
setTimeout(() => {
this.chainValue += 3;
resolve();
}, 500);
});
}
bar () {
this.chainValue += 5;
return true;
}
baz () {
return new Promise(resolve => {
setTimeout(() => {
this.chainValue += 7;
resolve();
}, 100);
});
}
}
const test = Test.create();
test.foo().bar().baz().then(() => console.log(test.chainValue)); // 15