
Esta es la segunda parte del análisis de tareas desde nuestro stand en la conferencia
HolyJS , celebrada en San Petersburgo del 24 al 25 de mayo. Para un contexto más amplio, se recomienda que primero lea la
primera parte de este material. Y si
Countdown Expression ya se ha completado, bienvenido al siguiente paso.
A diferencia del oscurantismo en la primera tarea, las dos siguientes ya tienen algún indicio de la aplicabilidad de las aplicaciones normales en la vida. JavaScript todavía se está desarrollando bastante rápido y las soluciones a los problemas sugeridos resaltan algunas de las nuevas características del lenguaje.
Tarea 2 ~ Hecho por unos
Se suponía que el código se ejecutaría e imprimiría respuestas a la consola en respuesta a tres solicitudes, y luego "hecho". Pero algo salió mal ... Corrija la situación.
;(function() { let iter = { [Symbol.iterator]: function* iterf() { let fs = ['/1', '/2', '/3']; for (const req in fs) { yield fetch(req); } } }; for (const res of iter) { console.log(res); } console.log('done'); })();
Problema de investigación
Que tenemos aqui Este es un objeto
iterativo iterativo que tiene un símbolo
Symbol.iterator bien conocido definido a través de
una función generadora . Se declara una matriz
fs en el cuerpo de la función, cuyos elementos caen en la función de
búsqueda a su vez para enviar una solicitud y el resultado de cada llamada de función se devuelve a través de
rendimiento . ¿Qué solicitudes envía la función de
búsqueda ? Todos los elementos de la matriz
fs son rutas relativas a los recursos con los números 1, 2 y 3, respectivamente. Por lo tanto, la URL completa se obtendrá concatenando
location.origin con el siguiente número, por ejemplo:
GET https://www.example.com/1
A continuación, queremos iterar el objeto
iter a través de
for-of , para ejecutar cada solicitud a su vez con la salida del resultado, después de todo, imprima "hecho". ¡Pero eso no funciona! El problema es que
buscar es una cosa asincrónica y devuelve una promesa, no una respuesta. Por lo tanto, en la consola veremos algo como esto:
Promise {pending}
Promise {pending}
Promise {pending}
done
En realidad, la tarea se reduce a resolver estas mismas promesas.
Tenemos async / aguardando
El primer pensamiento puede ser jugar con
Promise.all : darle nuestro objeto
iterable ,
luego la salida a la consola está "hecha". Pero no nos proporcionará la ejecución secuencial de las solicitudes (según lo requiera la condición), sino que simplemente las enviará todas y esperará la última respuesta antes de la resolución general.
La solución más simple aquí sería
esperar en el cuerpo
del cuerpo para esperar la resolución de la próxima promesa antes de enviarla a la consola:
for (const res of iter) { console.log(await res); }
Para
que el trabajo
en espera y "hecho" se muestre al final, debe hacer que la función principal sea asíncrona a través de
asíncrono :
;(async function() { let iter = { }; for (const res of iter) { console.log(await res); } console.log('done'); })();
En este caso, el problema ya se ha resuelto (casi):
GET 1st
Response 1st
GET 2nd
Response 2nd
GET 3rd
Response 3rd
done
Iterador asíncrono y generador
Dejaremos la función principal asincrónica, pero para
esperar hay un lugar más elegante en esta tarea que en el cuerpo
for-of : este es el uso de la iteración asincrónica a través de
for-wait-of , a saber:
for await (const res of iter) { console.log(res); }
¡Todo funcionará! Pero si recurre a la descripción de
esta propuesta sobre iteración asincrónica, entonces esto es lo interesante:
Introducimos una variación de la declaración de iteración for-of que itera sobre objetos iterables asíncronos. Las declaraciones for-of asincrónicas solo se permiten dentro de las funciones asincrónicas y las funciones generadoras asincrónicas
Es decir, nuestro objeto no solo debe ser
iterable , sino
"asyncIterable" a través del nuevo símbolo
conocido Symbol.asyncIterator y, en nuestro caso, ya una función generadora asíncrona:
let iter = { [Symbol.asyncIterator]: async function* iterf() { let fs = ['/1', '/2', '/3']; for (const req in fs) { yield await fetch(req); } } };
¿Cómo funciona entonces en un iterador y generador regular? Sí, solo implícitamente, como mucho más en este idioma. Esto es difícil: si el objeto solo es
iterable , entonces cuando itera asincrónicamente, "convierte" el objeto en
asyncIterable envolviendo los elementos (si es necesario) en
Promise con la expectativa de resolución. Habló
con más detalle
en un artículo de Axel Rauschmayer .
Probablemente, a través de
Symbol.asyncIterator todavía será más correcto, ya que creamos explícitamente el objeto
asyncIterable para nuestra iteración asincrónica a través de
for-wait-of , mientras dejamos la oportunidad de complementar el objeto con un iterador regular para
for , si es necesario. Si desea leer algo útil y suficiente en un artículo sobre iteraciones asíncronas en JavaScript, ¡
aquí está !
For
-of asíncrono todavía está parcialmente en borrador, pero ya es compatible con los navegadores modernos (excepto Edge) y Node.js de 10.x. Si esto molesta a alguien, siempre puede escribir su propio pequeño polifilo para una cadena de promesas, por ejemplo, para un objeto
iterable :
const chain = (promises, callback) => new Promise(resolve => function next(it) { let i = it.next(); i.done ? resolve() : i.value.then(res => { callback(res); next(it); }); }(promises[Symbol.iterator]()) ); ;(async function() { let iter = { }; await chain(iter, console.log); console.log('done'); })();
De esta y otras formas, descubrimos el envío de solicitudes y el procesamiento de respuestas a su vez. Pero en este problema hay un problema más pequeño pero molesto ...
Prueba de atención plena
Estábamos tan impresionados por todo este asincronismo que, como sucede a menudo, perdimos de vista un pequeño detalle. ¿Son esas solicitudes enviadas por nuestro script? Veamos la
red :
GET https://www.example.com/0
GET https://www.example.com/1
GET https://www.example.com/2
Pero nuestros números son 1, 2, 3. Como si hubiera ocurrido una disminución. Por qué Solo en el código fuente de la tarea hay otro problema con la iteración, aquí:
let fs = ['/1', '/2', '/3']; for (const req in fs) { yield fetch(req); }
Aquí
se usa un
for-in , que en lugar de los valores de la matriz omite sus propiedades enumeradas: y estos son los índices de elementos del 0 al 2. La función
fetch todavía los lleva a cadenas y, a pesar de la ausencia de una barra inclinada antes (esto ya no es una
ruta ), se resuelve relativamente URL de la página actual. Arreglarlo es mucho más fácil que darse cuenta. Dos opciones:
let fs = ['/1', '/2', '/3']; for (const req of fs) { yield fetch(req); } let fs = ['/1', '/2', '/3']; for (const req in fs) { yield fetch(fs[req]); }
En el primero, utilizamos el mismo
for-of para iterar sobre los valores de la matriz, en el segundo: acceso al elemento de la matriz por índice.
Motivación
Consideramos 3 soluciones: 1) a través de
esperar en el cuerpo
for-for , 2) a través de
for-wait-of, y 3) a través de nuestro polyfile (función recursiva,
tubería , etc.). Es curioso que estas opciones dividieron a los participantes de la conferencia aproximadamente por igual y no se reveló ningún favorito obvio. En proyectos grandes, para tareas tan reales, generalmente se usa una biblioteca reactiva (por ejemplo,
RxJS ), pero vale la pena recordar las características nativas modernas de un lenguaje de naturaleza asincrónica.
Aproximadamente la mitad de los participantes no notaron errores en la iteración de la lista de recursos, lo cual también es una observación interesante. Centrándonos en un problema no trivial pero obvio, podemos omitir fácilmente este truco aparentemente, pero con posibles consecuencias graves.
Problema 3 ~ Factor XIX
¡Cuántas veces en el registro del número 2019! (factorial de 2019) ¿aparece el número 19? Junto con la respuesta, proporcione una solución de JavaScript.
Problema de investigación
El problema está en la superficie: necesitamos un registro de un número muy grande para encontrar en él el número de todas las ocurrencias de la subcadena "19". Al resolver el problema en los
números , nos encontramos rápidamente con
Infinity (después de 170) y no obtuvimos nada. Además, el formato para representar números
float64 garantiza la precisión de solo 15-17 caracteres, y necesitamos no solo un registro completo, sino también exacto del número. Por lo tanto, la dificultad principal es determinar la estructura para la acumulación de este gran número.
Grandes enteros
Si sigue las innovaciones del lenguaje, el problema se resuelve simplemente: en lugar del
número de tipo
, puede usar el nuevo tipo
BigInt (etapa 3) , que le permite trabajar con números de precisión arbitrarios. Con la función recursiva clásica para calcular factorial y encontrar coincidencias a través de
String.prototype.split, la primera solución se ve así:
const fn = n => n > 1n ? n * fn(n - 1n) : 1n; console.log(fn(2019n).toString().split('19').length - 1);
Sin embargo, dos mil llamadas de función en la pila ya pueden
ser peligrosas . Incluso si lleva la solución a la
recursividad de cola , la
optimización de llamadas de cola solo es compatible con Safari. El problema factorial aquí es más agradable de resolver a través de un ciclo aritmético o
Array.prototype.reduce :
console.log([...Array(2019)].reduce((p, _, i) => p * BigInt(i + 1), 1n).toString().match(/19/g).length);
Esto puede parecer un procedimiento increíblemente largo. Pero esta impresión es engañosa. Si estima, entonces solo necesitamos gastar un poco más de dos mil multiplicaciones. En i5-4590 3.30GHz en cromo, el problema se resuelve en promedio en 4-5ms (!).
Otra opción para encontrar coincidencias en una cadena con el resultado de un cálculo es
String.prototype.match por expresión regular con el indicador de búsqueda global:
/ 19 / g .
Aritmética grande
Pero, ¿qué
pasa si todavía no tenemos este
BigInt (y las
bibliotecas también)? En este caso, puede hacer la aritmética larga usted mismo. Para resolver el problema, nos basta con implementar solo la función de multiplicar grande por pequeña (multiplicamos por números del 1 al 2019). Podemos mantener un gran número y el resultado de la multiplicación, por ejemplo, en la línea:
const mult = (big, int) => { let res = '', carry = 0; for (let i = big.length - 1; i >= 0; i -= 1) { let prod = big[i] * int + carry; res = prod % 10 + res; carry = prod / 10 | 0; } return (carry || '') + res; } console.log([...Array(2019)].reduce((p, _, i) => mult(p, i + 1), '1').match(/19/g).length);
Aquí simplemente multiplicamos la columna en bits desde el final de la línea hasta el principio, como nos enseñaron en la escuela. Pero la solución ya requiere unos 170 ms.
Podemos mejorar algo el algoritmo procesando más de un dígito en un registro de números a la vez. Para hacer esto, modificamos la función y al mismo tiempo vamos a las matrices, para no perder el tiempo con las líneas cada vez:
const mult = (big, int, digits = 1) => { let res = [], carry = 0, div = 10 ** digits; for (let i = big.length - 1; i >= 0 || carry; i -= 1) { let prod = (i < 0 ? 0 : big[i] * int) + carry; res.push(prod % div); carry = prod / div | 0; } return res.reverse(); }
Aquí, los números grandes están representados por una matriz, cada elemento del cual almacena información sobre
dígitos dígitos del registro de números, usando el
número . Por ejemplo, el número 2016201720182019 con
dígitos = 3 se representará como:
'2|016|201|720|182|019' => [2,16,201,720,182,19]
Al convertir a una línea antes de una unión, debe recordar los ceros iniciales. La función
factor devuelve el factorial calculado por una cadena, utilizando la función
múltiple con el número especificado de dígitos procesados a la vez en la representación "masiva" del número al calcular:
const factor = (n, digits = 1) => [...Array(n)].reduce((p, _, i) => mult(p, i + 1, digits), [1]) .map(String) .map(el => '0'.repeat(digits - el.length) + el) .join('') .replace(/^0+/, ''); console.log(factor(2019, 3).match(/19/g).length);
La implementación "hasta la rodilla" a través de matrices resultó ser más rápida que a través de cadenas, y con
dígitos = 1 calcula la respuesta ya en promedio en 90ms,
dígitos = 3 en 35ms,
dígitos = 6 en solo 20ms. Sin embargo, recuerde que al aumentar el número de dígitos, nos estamos acercando a una situación en la que multiplicar el
número por el
número "debajo del capó" puede estar fuera de la caja fuerte
MAX_SAFE_INTEGER . Puedes jugar con eso
aquí . ¿Cuál es el valor máximo de
dígitos que podemos permitirnos para esta tarea?
Los resultados ya son bastante indicativos,
BigInt es realmente bastante rápido:

Motivación
Es genial que 2/3 de los participantes de la conferencia usaron el nuevo tipo
BigInt en la solución (alguien admitió que esta fue la primera experiencia). El tercio restante de las soluciones contenía sus propias implementaciones de aritmética larga en cadenas o matrices. La mayoría de las funciones implementadas multiplicaron números grandes por números grandes, cuando para una solución fue suficiente multiplicar por un número "pequeño" y pasar un poco menos de tiempo. Bien, ¿ya
usas BigInt en tus proyectos?
Agradecimientos
Estos dos días de la conferencia estuvieron muy llenos de discusiones y aprendiendo algo nuevo. Me gustaría agradecer al comité del programa por la próxima conferencia inolvidable y a todos los participantes por su trabajo en red único y su buen humor.