HolyJS 2019:SEMrush的汇报(第2部分)



这是5月24日至25日在圣彼得堡举行的HolyJS会议摊位上的任务分析的第二部分。 对于较大的上下文,建议您首先阅读本材料的第一部分 。 如果倒计时表达式已经完成,则欢迎进行下一步。

与第一个任务中的晦涩难懂不同,后两个任务已经暗示了正常应用程序在生活中的适用性。 JavaScript仍在快速发展,针对所建议问题的解决方案突出了该语言的一些新功能。

任务2〜完成


假定代码将响应三个请求而执行并向控制台输出答案,然后“完成”。 但是出了点问题。纠正情况。

;(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'); })(); 

研究问题


我们这里有什么? 这是一个可迭代的 iter对象,它具有通过generator函数 定义 的众所周知的 Symbol.iterator符号。 在函数主体中声明了数组fs ,该数组的元素依次属于fetch函数以发送请求,并且每个函数调用的结果通过yield返回。 提取功能发送什么请求? fs数组的所有元素都是到数字分别为1、2和3的资源的相对路径。 因此,完整的URL将通过将location.origin与下一个数字连接而获得,例如:

GET https://www.example.com/1

接下来,我们要通过for-of迭代iter对象,以便依次执行每个请求以及结果的输出-毕竟-打印“ done”。 但这不起作用! 问题在于, 获取是异步的,它返回一个承诺,而不是响应。 因此,在控制台中,我们将看到以下内容:

Promise {pending}
Promise {pending}
Promise {pending}
done

实际上,任务归结为解决这些相同的承诺。

我们有异步/等待


首先想到的可能是与Promise.all一起玩:将其赋予我们可迭代的对象, 然后将对控制台输出“完成”。 但是他不会为我们提供顺序执行请求(条件要求)的方法,而只是将它们全部发送出去,并在一般解决方案之前等待最后一个答案。

最简单的解决方案是在for-of正文中等待下一个promise的解析,然后再输出到控制台:

 for (const res of iter) { console.log(await res); } 

为了等待工作并在最后显示“完成”,必须通过async使主要功能异步:

 ;(async function() { let iter = { /* ... */ }; for (const res of iter) { console.log(await res); } console.log('done'); })(); 

在这种情况下,问题已经解决(几乎):

GET 1st
Response 1st
GET 2nd
Response 2nd
GET 3rd
Response 3rd
done

异步迭代器和生成器


我们将使主函数保持异步状态,但是在等待状态下,此任务比在for-of正文中占有更优雅的位置:这是通过for-await-of使用异步迭代,即:

 for await (const res of iter) { console.log(res); } 

一切都会正常! 但是,如果您转向有关异步迭代的提案的描述,那么有趣的是:

我们介绍了for-of迭代语句的一种变体,它在异步可迭代对象上进行迭代。 异步for-of语句仅在异步函数和异步生成器函数中允许

也就是说,我们的对象不仅应该是可迭代的 ,而且还应该通过新的著名符号Symbol.asyncIterator以及在我们的情况下已经是异步生成器函数来“ asyncIterable”

 let iter = { [Symbol.asyncIterator]: async function* iterf() { let fs = ['/1', '/2', '/3']; for (const req in fs) { yield await fetch(req); } } }; 

然后如何在常规迭代器和生成器上工作? 是的,只是隐式地喜欢这种语言。 这种等待是很棘手的:如果对象仅是可迭代的 ,则当异步迭代时,它通过将Promise中的元素(如有必要)包装为带有解析度的期望而将对象“转换”为asyncIterable 。 他 Axel Rauschmayer 的一篇文章中更详细地讲了话。

可能通过Symbol.asyncIterator仍然会更正确,因为我们通过for-await-of显式地为异步迭代创建了asyncIterable对象,同时如有必要,还为for留下了用常规迭代器补充对象的机会。 如果您想在一篇有关JavaScript异步迭代的文章中读到有用且足够的内容,那么这里就是

异步论坛仍处于草稿中,但现代浏览器(Edge除外)和10.x版的Node.js已支持它。 如果这使某人感到困扰,您总是可以为一连串的承诺(例如,针对可迭代的对象)编写自己的小型多义人:

 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 = { /* iterable */ }; await chain(iter, console.log); console.log('done'); })(); 

通过这种方式,我们弄清楚了依次发送请求和处理响应。 但是在这个问题上,还有另一个小而烦人的问题...

正念测试


所有这些异步让我们如此疯狂,以至于经常发生的事情,我们忽略了一个小细节。 这些请求是由我们的脚本发送的吗? 让我们看一下网络

GET https://www.example.com/0
GET https://www.example.com/1
GET https://www.example.com/2

但是我们的数字是1、2、3。 为什么这样 只是在任务的源代码中,迭代还有另一个问题,在这里:

 let fs = ['/1', '/2', '/3']; for (const req in fs) { yield fetch(req); } 

这里使用了for-in ,而不是数组值绕过其枚举属性:这些是0到2元素的索引。fetch函数仍然将它们引向字符串,尽管之前没有斜杠(这不再是path ),但它相对解析当前页面的URL。 修复比注意到容易得多。 两种选择:

 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]); } 

在第一个中,我们使用相同的for-of来遍历数组的值,第二个中-通过索引访问数组元素。

动机


我们考虑了3个解决方案:1)通过for-for主体中的await ,2)通过for-await-of,以及3)通过我们的polyfile(递归函数, 管道管道等)。 很好奇的是,这些选择将会议参加者平均分配,并且没有发现明显的偏爱。 在大型项目中,对于此类实际任务,通常使用反应式库(例如RxJS ),但是值得记住具有异步性质的语言的现代本机功能。

大约一半的参与者没有注意到资源列表中的迭代错误,这也是一个有趣的发现。 着眼于一个非平凡但显而易见的问题,我们可以轻松地跳过这个看似微不足道的问题,但可能会带来严重的后果。

问题3〜因子19


2019年的记录有多少次! (从2019年起析构)数字19是否会出现? 连同答案一起,提供一个JavaScript解决方案。

研究问题


表面上是问题:我们需要一个非常大的记录才能在其中找到子字符串“ 19”的所有出现次数。 解决数字上的问题后,我们很快就遇到了Infinity (170之后),却一无所获。 此外,用于表示数字的格式float64只能保证15-17个字符的准确性,我们不仅需要获取完整的数字,还要获得准确的数字记录。 因此,主要困难是确定用于大量累积的结构。

大整数


如果您遵循该语言的创新,则可以轻松解决此任务:可以使用新类型BigInt (第3阶段)代替类型数字,该类型允许您使用任意精度数字。 使用经典的递归函数通过String.prototype.split计算阶乘和查找匹配项,第一个解决方案如下所示:

 const fn = n => n > 1n ? n * fn(n - 1n) : 1n; console.log(fn(2019n).toString().split('19').length - 1); // 50 

但是,在堆栈上进行2000次函数调用可能已经很危险了 。 即使将解决方案引入尾递归尾调用优化仍仅支持Safari。 通过运算周期或Array.prototype.reduce可以解决这里的阶乘问题:

 console.log([...Array(2019)].reduce((p, _, i) => p * BigInt(i + 1), 1n).toString().match(/19/g).length); // 50 

这似乎是一个疯狂的漫长过程。 但是这种印象是欺骗性的。 如果您估计,那么我们只需要花费2000多倍就可以了。 在镀铬的i5-4590 3.30GHz上,该问题平均在4-5ms(!)之内解决。

用于查找字符串中具有计算结果的匹配项的另一个选项是通过带有全局搜索标记/ 19 / g的正则表达式来查找String.prototype.match

大算术


但是,如果我们还没有BigInt (还有 )呢? 在这种情况下,您可以自己执行长算法。 要解决该问题,仅执行大乘小(我们将数字乘以1到2019)就足够了。 例如,我们可以在行中保存大量和乘法结果:

 /** * @param {string} big * @param {number} int * @returns {string} */ 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); // 50 

在这里,我们就像在学校教过的那样,简单地将行末尾的行与列的末尾相乘。 但是该解决方案已经需要约170ms。

通过一次处理一个数字记录中的多个数字,我们可以在某种程度上改进算法。 为此,我们修改函数并同时转到数组,以免每次都弄乱行:

 /** * @param {Array<number>} big * @param {number} int * @param {number} digits * @returns {Array<number>} */ 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(); } 

在这里,大数由一个数组表示,其每个元素都使用number存储有关数字记录中数字位数的信息。 例如,数字2016201720182019的数字 = 3将表示为:

'2|016|201|720|182|019' => [2,16,201,720,182,19]

在联接之前转换为行时,您需要记住前导零。 在计算时, factor函数使用字符串同时使用指定的数字处理数量的mult函数以字符串形式返回计算的阶乘:

 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); // 50 

通过数组实现的“膝长”实现比通过字符串实现的更快,并且在digits = 1的情况下,它平均可以在90ms内计算出答案, digits = 3在35ms内, 位数 = 6在20ms内即可。 但是,请记住,增加位数会导致“在幕后 ”将数字乘以数字可能超出安全MAX_SAFE_INTEGER的情况 。 你可以在这里玩。 我们可以为此任务负担的最大位数是多少?

结果已经非常具有指示性, BigInt确实非常快:



动机


2/3的与会人员在解决方案中使用了新的BigInt类型是很酷的(有人承认这是第一次体验)。 其余三分之一的解决方案包含它们自己对字符串或数组的长算术实现。 大多数实现的功能都将大数乘以大数,而对于一个解决方案来说,乘以“小” 足以花更少的时间就足够了。 好的任务,您已经在项目中使用BigInt吗?

致谢


会议的这两天充满了讨论和学习新知识。 我要感谢计划委员会下次难忘的会议,并感谢所有与会人员的独特联系和良好的心情。

Source: https://habr.com/ru/post/zh-CN453466/


All Articles