diff --git a/11.md b/11.md index 9e5df71..90c815b 100644 --- a/11.md +++ b/11.md @@ -150,15 +150,17 @@ storage(bigOak, "enemies") 这个异步函数返回一个有意义的值。 这是`Promise`的主要优点 - 它们简化了异步函数的使用。 基于`Promise`的函数不需要传递回调,而是类似于常规函数:它们将输入作为参数并返回它们的输出。 唯一的区别是输出可能还不可用。 -## 失败 +## 故障 + +> 译者注:这段如果有配套代码会更容易理解,但是没有,所以凑合看吧。 常规的 JavaScript 计算可能会因抛出异常而失败。 异步计算经常需要类似的东西。 网络请求可能会失败,或者作为异步计算的一部分的某些代码,可能会引发异常。 -异步编程的回调风格中最紧迫的问题之一是,确保将失败正确地报告给回调函数,是非常困难的。 +异步编程的回调风格中最紧迫的问题之一是,确保将故障正确地报告给回调函数,是非常困难的。 一个广泛使用的约定是,回调函数的第一个参数用于指示操作失败,第二个参数包含操作成功时生成的值。 这种回调函数必须始终检查它们是否收到异常,并确保它们引起的任何问题,包括它们调用的函数所抛出的异常,都会被捕获并提供给正确的函数。 -`Promise`使这更容易。可以解决它们(操作成功完成)或拒绝(失败)。只有在操作成功时,才会调用解析处理器(使用`then`注册),并且拒绝会自动传播给由`then`返回的新`Promise`。当一个处理器抛出一个异常时,这会自动使`then`调用产生的`Promise`被拒绝。因此,如果异步操作链中的任何元素失败,则整个链的结果被标记为拒绝,并且不会调用失败位置之后的任何常规处理器。 +`Promise`使这更容易。可以解决它们(操作成功完成)或拒绝(故障)。只有在操作成功时,才会调用解析处理器(使用`then`注册),并且拒绝会自动传播给由`then`返回的新`Promise`。当一个处理器抛出一个异常时,这会自动使`then`调用产生的`Promise`被拒绝。因此,如果异步操作链中的任何元素失败,则整个链的结果被标记为拒绝,并且不会调用失败位置之后的任何常规处理器。 就像`Promise`的解析提供了一个值,拒绝它也提供了一个值,通常称为拒绝的原因。当处理器中的异常导致拒绝时,异常值将用作原因。同样,当处理器返回被拒绝的`Promise`时,拒绝流入下一个`Promise`。`Promise.reject`函数会创建一个新的,立即被拒绝的`Promise`。 @@ -171,3 +173,138 @@ storage(bigOak, "enemies") 通过调用`then`和`catch`创建的`Promise`值的链条,可以看作异步值或失败沿着它移动的流水线。 由于这种链条通过注册处理器来创建,因此每个链条都有一个成功处理器或与其关联的拒绝处理器(或两者都有)。 不匹配结果类型(成功或失败)的处理器将被忽略。 但是那些匹配的对象被调用,并且它们的结果决定了下一次会出现什么样的值 -- 返回非`Promise`值时成功,当它抛出异常时拒绝,并且当它返回其中一个时是`Promise`的结果。 就像环境处理未捕获的异常一样,JavaScript 环境可以检测未处理`Promise`拒绝的时候,并将其报告为错误。 + +## 网络是困难的 + +偶尔,乌鸦的镜像系统没有足够的光线来传输信号,或者有些东西阻挡了信号的路径。 信号可能发送了,但从未收到。 + +事实上,这只会导致提供给`send`的回调永远不会被调用,这可能会导致程序停止,而不会注意到问题。 如果在没有得到回应的特定时间段内,请求会超时并报告故障,那就很好。 + +通常情况下,传输故障是随机事故,例如汽车的前灯会干扰光信号,只需重试请求就可以使其成功。 所以,当我们处理它时,让我们的请求函数在放弃之前自动重试发送请求几次。 + +而且,既然我们已经确定`Promise`是一件好事,我们也会让我们的请求函数返回一个`Promise`。 对于他们可以表达的内容,回调和`Promise`是等同的。 基于回调的函数可以打包,来公开基于`Promise`的接口,反之亦然。 + +即使请求及其响应已成功传递,响应也可能表明失败 - 例如,如果请求尝试使用未定义的请求类型或处理器,会引发错误。 为了支持这个,`send`和`defineRequestType`遵循前面提到的惯例,其中传递给回调的第一个参数是故障原因,如果有的话,第二个参数是实际结果。 + +这些可以由我们的包装翻译成`Promise`的解析和拒绝。 + +```js +class Timeout extends Error {} + +function request(nest, target, type, content) { + return new Promise((resolve, reject) => { + let done = false; + function attempt(n) { + nest.send(target, type, content, (failed, value) => { + done = true; + if (failed) reject(failed); + else resolve(value); + }); + setTimeout(() => { + if (done) return; + else if (n < 3) attempt(n + 1); + else reject(new Timeout("Timed out")); + }, 250); + } + attempt(1); + }); +} +``` + +因为承诺只能解析(或拒绝)一次,所以这个是有效的。 第一次调用`resolve`或`reject`会决定`Promise`的结果,并且任何进一步的调用(例如请求结束后到达的超时,或在另一个请求结束后返回的请求)都将被忽略。 + +为了构建异步循环,对于重试,我们需要使用递归函数 - 常规循环不允许我们停止并等待异步操作。 `attempt`函数尝试发送请求一次。 它还设置了超时,如果 250 毫秒后没有响应返回,则开始下一次尝试,或者如果这是第四次尝试,则以`Timeout`实例为理由拒绝该`Promise`。 + +每四分之一秒重试一次,一秒钟后没有响应就放弃,这绝对是任意的。 甚至有可能,如果请求确实过来了,但处理器花费了更长时间,请求将被多次传递。 我们会编写我们的处理器,并记住这个问题 - 重复的消息应该是无害的。 + +总的来说,我们现在不会建立一个世界级的,强大的网络。 但没关系 - 在计算方面,乌鸦没有很高的预期。 + +为了完全隔离我们自己的回调,我们将继续,并为`defineRequestType`定义一个包装器,它允许处理器返回一个`Promise`或明确的值,并且连接到我们的回调。 + +```js +function requestType(name, handler) { + defineRequestType(name, (nest, content, source, + callback) => { + try { + Promise.resolve(handler(nest, content, source)) + .then(response => callback(null, response), + failure => callback(failure)); + } catch (exception) { + callback(exception); + } + }); +} +``` + +如果处理器返回的值还不是`Promise`,`Promise.resolve`用于将转换为`Promise`。 + +请注意,处理器的调用必须包装在`try`块中,以确保直接引发的任何异常都会被提供给回调函数。 这很好地说明了使用原始回调正确处理错误的难度 - 很容易忘记正确处理类似的异常,如果不这样做,故障将无法报告给正确的回调。`Promise`使其大部分是自动的,因此不易出错。 + +## `Promise`的集合 + + +每台鸟巢计算机在其`neighbors`属性中,都保存了传输距离内的其他鸟巢的数组。 为了检查当前哪些可以访问,您可以编写一个函数,尝试向每个鸟巢发送一个`"ping"`请求(一个简单地请求响应的请求),并查看哪些返回了。 + +在处理同时运行的`Promise`集合时,`Promise.all`函数可能很有用。 它返回一个`Promise`,等待数组中的所有`Promise`解决,然后解析这些`Promise`产生的值的数组(与原始数组的顺序相同)。 如果任何`Promise`被拒绝,`Promise.all`的结果本身被拒绝。 + +```js +requestType("ping", () => "pong"); + +function availableNeighbors(nest) { + let requests = nest.neighbors.map(neighbor => { + return request(nest, neighbor, "ping") + .then(() => true, () => false); + }); + return Promise.all(requests).then(result => { + return nest.neighbors.filter((_, i) => result[i]); + }); +} +``` + +当一个邻居不可用时,我们不希望整个组合`Promise`失败,因为那时我们仍然不知道任何事情。 因此,在邻居集合上映射一个函数,将它们变成请求`Promise`,并附加处理器,这些处理器使成功的请求产生`true`,拒绝的产生`false`。 + +在组合`Promise`的处理器中,`filter`用于从`neighbors`数组中删除对应值为`false`的元素。 这利用了一个事实,`filter`将当前元素的数组索引作为其过滤函数的第二个参数(`map`,`some`和类似的高阶数组方法也一样)。 + +## 网络泛洪 + +鸟巢仅仅可以邻居通信的事实,极大地减少了这个网络的实用性。 + +为了将信息广播到整个网络,一种解决方案是设置一种自动转发给邻居的请求。 然后这些邻居转发给它们的邻居,直到整个网络收到这个消息。 + +```js +import {everywhere} from "./crow-tech"; + +everywhere(nest => { + nest.state.gossip = []; +}); + +function sendGossip(nest, message, exceptFor = null) { + nest.state.gossip.push(message); + for (let neighbor of nest.neighbors) { + if (neighbor == exceptFor) continue; + request(nest, neighbor, "gossip", message); + } +} + +requestType("gossip", (nest, message, source) => { + if (nest.state.gossip.includes(message)) return; + console.log(`${nest.name} received gossip '${ + message}' from ${source}`); + sendGossip(nest, message, source); +}); +``` + +为了避免永远在网络上发送相同的消息,每个鸟巢都保留一组已经看到的闲话串。 为了定义这个数组,我们使用`everywhere`函数(它在每个鸟巢上运行代码)向鸟巢的状态对象添加一个属性,这是我们将保持鸟巢局部状态的地方。 + +当一个鸟巢收到一个重复的闲话消息,它会忽略它。每个人都盲目重新发送这些消息时,这很可能发生。 但是当它收到一条新消息时,它会兴奋地告诉它的所有邻居,除了发送消息的那个邻居。 + +这将导致一条新的闲话通过网络传播,如在水中的墨水一样。 即使一些连接目前不工作,如果有一条通往指定鸟巢的替代路线,闲话将通过那里到达它。 + +这种网络通信方式称为泛洪 - 它用一条信息充满网络,直到所有节点都拥有它。 + +我们可以调用`sendGossip`看看村子里的消息流。 + +```js +sendGossip(bigOak, "Kids with airgun in the park"); +``` +