如何实现异步JavaScript的getter和setter?

39

想象一下Rails是如何允许你将一个属性与另一个属性关联起来的:

class Customer < ActiveRecord::Base
  has_many :orders
end

这不会为orders设置数据库列。相反,它创建了一个orders的getter,使我们能够做到

@orders = @customer.orders
获取相关的orders对象。
在JS中,我们可以使用getter轻松实现:
{
   name: "John",
   get orders() {
     // get the order stuff here
   }
}

不过 Rails 是同步的,而在 JS 中,如果按照我们的例子,在合理情况下去访问数据库,我们就会以异步方式完成。

我们如何创建异步的 getter(以及 setter)呢?

我们会返回一个最终得到解决的 Promise 吗?

{
   name: "John",
   get orders() {
     // create a promise
     // pseudo-code for db and promise...
     db.find("orders",{customer:"John"},function(err,data) {
        promise.resolve(data);
     });
     return promise;
   }
}

这将使我们能够做到

customer.orders.then(....);

或者我们会使用更加Angular风格的方式,自动将其解析为一个值?

总之,我们如何实现异步getter?


2
我想不到其他的方法,除了返回一个 Promise。 - Felix Kling
@dandavis 是的,我刚写了一个例子,遇到了这个问题。这种设计是否有问题?我猜你可以完全避免使用 getters,而是使用 get 函数,你可以定义它们返回 Promises 或传递回调函数,但哪种方式最合理呢? - deitch
1
这只是我的看法,或许我对JS有点守旧,但如果有动词在里面,我希望能看到一个动词(比如get)或调用签名中的括号。我认为通过隐藏括号,你让它看起来像是一个同步读取RAM数据对象,但实际上并不是。使用customer.getOrders().then(....);customer.orders.get(fnThen_or_callBack);customer.orders(fnThen_or_callBack);,或者只是customer.orders().then(....);更直观(对我来说)。在所有这些情况下,我一眼就能看出除了简单的读取之外还发生了其他事情。 - dandavis
“使用yield的生成器”只是表达“async/await”语法的传统方式(协程运行程序经常被遗忘)。即使如此,getter也必须返回一个promise;区别仅在于如何消耗该promise。 - Bergi
1
@trusktr 非常令人印象深刻。JS的一个伟大优势一直是它的异步性。而它最大的弱点之一一直是当你需要同步时,它无法正常工作。这肯定会有所帮助。 - deitch
显示剩余16条评论
5个回答

30
getset函数关键字似乎与async关键字不兼容。然而,由于async/await只是Promise的一个包装器,您可以使用Promise使您的函数变为“awaitable”。
注:可以使用Object.defineProperty方法将async函数分配给setter或getter。

getter

Promise与getter很好地配合使用。

这里,我正在使用Node.js 8内置的util.promisify()函数,它将节点样式回调(“nodeback”)转换为单个Promise,使得编写可等待的getter非常容易。

var util = require('util');
class Foo {
  get orders() {
    return util.promisify(db.find)("orders", {customer: this.name});
  }
};

// We can't use await outside of an async function
(async function() {
  var bar = new Foo();
  bar.name = 'John'; // Since getters cannot take arguments
  console.log(await bar.orders);
})();

setter

对于 setter,情况有点奇怪。

当然,您可以将 Promise 作为参数传递给 setter,并在其中执行任何操作,无论您是否等待 Promise 被执行。

然而,我想到一个更有用的用例(也是我来到这里的原因!),就是使用 setter 然后 await 在 setter 被使用的任何上下文中完成该操作。但很遗憾,“setter” 函数的返回值被丢弃了,所以这种操作不可能实现。

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

class SetTest {
  set foo(p) {
    return p.then(function(val) {
      // Do something with val that takes time
      return makePromise(2000, val);
    }).then(console.log);
  }
};

var bar = new SetTest();

var promisedValue = makePromise(1000, 'Foo');

(async function() {
  await (bar.foo = promisedValue);
  console.log('Done!');
})();

在这个例子中,Done!会在1秒后打印到控制台上,而Foo则会在2秒后打印。这是因为await正在等待promisedValue被实现,它从来没有看到在setter内部使用/生成的Promise。请保留HTML标签。

2
TMHO,getter确实已经被正确地promisified了,但我认为setter的例子不太现实:如果提供的值是一个promise-在调用set之前先解决它...非常简单;然而,如果我们谈论的是一个过程,在其中set方法内部的值设置包括一些异步工作,那就是另一回事了。那么,我们最好用一个async嵌套函数来包装逻辑,并返回其返回值(即一个promise)。 - Avi Tshuva
@AviTshuva 在传递给 setter 之前没有理由取消 Promise。这样做完全没有问题。是的,使用函数可能比使用 setter 更好地执行一些异步工作,但在技术上仍然可以使用 setter。 - Cameron Tacklind
1
但这就是我所争论的:不是可能性的问题,而是它缺乏意义。 - Avi Tshuva
你是否应该在代码中实际使用 setters 和 getters 超出了本问题的范围。 - Cameron Tacklind
我同意,因此我所说的是在评论中;它超出了范围,但在实践中,这是一个重要且相关的观点,以我个人之见。 - Avi Tshuva

3

对于异步获取器,您可以像这样操作:

const object = {};

Object.defineProperty(object, 'myProperty', {

    async get() {

        // Your awaited calls

        return /* Your value */;
    }
});

相反,当涉及到异步设置器时问题就出现了。 由于表达式a = b总是产生b,因此在持有属性a的对象中没有任何一个设置器可以覆盖这个行为,无法避免这个问题。
既然我也遇到了这个问题,我意识到异步设置器实际上是不可能的。所以,我想出了一种替代设计来代替异步设置器,并提出了以下替代语法:
console.log(await myObject.myProperty); // Get the value of the property asynchronously
await myObject.myProperty(newValue); // Set the value of the property asynchronously

我使用以下代码使其正常工作:

function asyncProperty(descriptor) {

    const newDescriptor = Object.assign({}, descriptor);

    delete newDescriptor.set;

    let promise;

    function addListener(key) {
        return callback => (promise || (promise = descriptor.get()))[key](callback);
    }

    newDescriptor.get = () => new Proxy(descriptor.set, {

        has(target, key) {
            return Reflect.has(target, key) || key === 'then' || key === 'catch';
        },

        get(target, key) {

            if (key === 'then' || key === 'catch')
                return addListener(key);

            return Reflect.get(target, key);
        }
    });

    return newDescriptor;
}

给定另一个描述符,该描述符允许定义类似于异步设置器的内容,返回异步属性的描述符。

您可以按以下方式使用上述代码:

function time(millis) {
    return new Promise(resolve => setTimeout(resolve, millis));
}

const object = Object.create({}, {

    myProperty: asyncProperty({

        async get() {

            await time(1000);

            return 'My value';
        },

        async set(value) {

            await time(5000);

            console.log('new value is', value);
        }
    })
});

一旦您像上面那样设置了一个异步属性,就可以按照已经说明的方式进行设置:

(async function() {

    console.log('getting...');
    console.log('value from getter is', await object.myProperty);
    console.log('setting...');
    await object.myProperty('My new value');
    console.log('done');
})();

0
以下代码允许在代理处理程序中使用异步设置器,遵循Davide Cannizzo回答中的约定。
var obj = new Proxy({}, asyncHandler({
  async get (target, key, receiver) {
    await new Promise(a => setTimeout(a, 1000))
    return target[key]
  },
  async set (target, key, val, receiver) {
    await new Promise(a => setTimeout(a, 1000))
    return target[key] = val
  }
}))

await obj.foo('bar') // set obj.foo = 'bar' asynchronously
console.log(await obj.foo) // 'bar'

function asyncHandler (h={}) {
  const getter = h.get
  const setter = h.set
  let handler = Object.assign({}, h)
  handler.set = () => false
  handler.get = (...args) => {
    let promise
    return new Proxy(()=>{}, {
      apply: (target, self, argv) => {
        return setter(args[0], args[1], argv[0], args[2])
      },
      get: (target, key, receiver) => {
        if (key == 'then' || key == 'catch') {
          return callback => {
            if (!promise) promise = getter(...args)
            return promise[key](callback)
          }
        }
      }
    })
  }
  return handler
}

-1
这是另一种方法。它创建了一个额外的包装器,但在其他方面它涵盖了人们所期望的内容,包括使用await(这是TypeScript,只需删除设置返回值类型的: Promise<..>部分即可获得JS):
// this doesn't work
private get async authedClientPromise(): Promise<nanoClient.ServerScope> {
    await this.makeSureAuthorized()
    return this.client
}

// but this does
private get authedClientPromise(): Promise<nanoClient.ServerScope> {
    return (async () => {
        await this.makeSureAuthorized()
        return this.client
    })()
}

-5
这里是如何实现获取订单函数的代码。
function get(name) {
    return new Promise(function(resolve, reject) {
        db.find("orders", {customer: name}, function(err, data) {
             if (err) reject(err);
             else resolve(data);
        });
    });
}

你可以像这样调用这个函数:
customer.get("John").then(data => {
    // Process data here...
}).catch(err => {
    // Process error here...
});

没有利用属性描述符的get/set - 这只是在对象上添加自己的方法,虽然在某些方面类似但不同。 - realisation

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接