Piense en cómo Rails, por ejemplo, le permite definir una propiedad asociada con otra:
class Customer < ActiveRecord::Base has_many :orders end Esto no configura una columna de base de datos para orders . En su lugar, crea un captador de orders , lo que nos permite hacer
@orders = @customer.orders Que va y obtiene los objetos de orders relacionados.
En JS, podemos hacer eso fácilmente con captadores:
{ name: "John", get orders() { // get the order stuff here } }Pero Rails es sync , y en JS, si en nuestro ejemplo, como es razonable, vamos a la base de datos, lo estaríamos haciendo asíncrono .
¿Cómo crearíamos getters asíncronos (y setters, para el caso)?
¿Le devolveríamos una promesa que finalmente se resuelve?
{ 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; } }que nos permitiría hacer
customer.orders.then(....);¿O lo haríamos con un estilo más angular, donde lo resolveríamos automáticamente en un valor?
En resumen, ¿cómo implementamos captadores asíncronos?
Las palabras clave de la función get y set parecen ser incompatibles con la palabra clave async . Sin embargo, dado que async / await es solo un envoltorio alrededor de Promise s, puede usar Promise para hacer que sus funciones sean " await -able".
Nota: Debería ser posible usar el método Object.defineProperty para asignar una función async a un definidor o captador.
Las promesas funcionan bien con captadores.
Aquí, estoy usando la función incorporada util.promisify() de Node.js 8 que convierte una devolución de llamada de estilo de nodo ("devolución de nodo") en una Promise en una sola línea. Esto hace que sea muy fácil escribir un captador capaz de await .
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); })();Para los setters, se vuelve un poco raro.
Por supuesto, puede pasar una Promesa a un setter como argumento y hacer lo que sea por dentro, ya sea que espere a que se cumpla la Promesa o no.
Sin embargo, me imagino que un caso de uso más útil (¡el que me trajo aquí!) sería usar el setter y luego await que la operación se complete en cualquier contexto desde el que se haya utilizado el setter. Desafortunadamente, esto no es posible ya que el valor de retorno de la función setter se descarta .
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!'); })(); En este ejemplo, Done! se imprime en la consola después 1 segundo y el Foo se imprime 2 segundos después de eso. Esto se debe a que await está esperando que se cumpla el valor promisedValue y nunca ve la Promise utilizada/generada dentro del setter.
En cuanto a captadores asíncronos, puede hacer algo como esto:
const object = {}; Object.defineProperty(object, 'myProperty', { async get() { // Your awaited calls return /* Your value */; } }); Más bien, el problema surge cuando se trata de setters asincrónicos. Dado que la expresión a = b siempre produce b , no hay nada que se pueda hacer para evitar esto, es decir, ningún setter en el objeto que tenga la propiedad a puede anular este comportamiento.
Como también me topé con este problema, pude darme cuenta de que los setters asincrónicos eran literalmente imposibles. Entonces, me di cuenta de que tenía que elegir un diseño alternativo para usar en lugar de los setters asíncronos. Y luego se me ocurrió la siguiente sintaxis alternativa:
console.log(await myObject.myProperty); // Get the value of the property asynchronously await myObject.myProperty(newValue); // Set the value of the property asynchronouslyLo tengo funcionando con el siguiente código,
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; }que devuelve un descriptor para una propiedad asíncrona, dado otro descriptor que puede definir algo que parece un setter asíncrono.
Puede utilizar el código anterior de la siguiente manera:
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); } }) });Una vez que haya configurado una propiedad asíncrona como la anterior, puede configurarla como ya se ilustra:
(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'); })();Lo siguiente permite setters asíncronos en controladores de proxy siguiendo la convención en la respuesta de 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 }