JavaScript-函数式入门指南-二-

133 阅读1小时+

JavaScript 函数式入门指南(二)

原文:Beginning Functional JavaScript

协议:CC BY-NC-SA 4.0

十、生成器的暂停、恢复和异步

我们从函数的简单定义开始阅读这本书,然后我们看到了如何使用函数通过函数式编程技术来做大事。我们已经看到了如何用纯函数的术语来处理数组、对象和错误处理。对我们来说,这是一个相当长的旅程,但是我们仍然没有谈到每个 JavaScript 开发人员都应该知道的另一个重要技术:异步代码。

您已经在项目中处理了大量的异步代码。您可能想知道函数式编程是否能帮助开发人员编写异步代码。答案是肯定的,也是否定的。我们最初要展示的技术是使用 ES6 生成器,然后使用 Async/Await,这是 ECMAScript 2017/ES8 规范的新内容。这两种模式都试图用自己的方式解决同一个回调问题,所以要密切注意细微的差别。发电机是 ES6 中函数的新规格。生成器并不是真正的函数式编程技术;然而,它们是函数的一部分(函数式编程是关于函数的,对吧?);出于这个原因,我们在这本函数式编程书中专门为它写了一章。

即使你是承诺的忠实粉丝(这是一种解决回调问题的技术),我们仍然建议你看一下这一章。您可能会喜欢生成器以及它们解决异步代码问题的方式。

注意

章节示例和库源代码在第十章分支中。回购的网址是github.com/antsmartian/functional-es8.git

一旦你检查出代码,请检查分支第十章:

git checkout -b 第十章来源/第十章

对于运行代码,与之前一样,运行:

...npm 跑步游乐场...

异步代码及其问题

在我们真正了解什么是生成器之前,让我们在本节中讨论一下在 JavaScript 中处理异步代码的问题。我们要讨论一个回调地狱问题。大多数异步代码模式,如 Generators 或 Async/Await,都试图以自己的方式解决回调问题。如果您已经知道它是什么,请随意进入下一部分。对于其他人,请继续阅读。

回调地狱

Imagine you have a function like the one shown in Listing 10-1.let sync = () => {         //some operation         //return data } let sync2 = () => {         //some operation         //return data } let sync3 = () => {         //some operation         //return data } Listing 10-1

同步功能

The functions sync, sync1, and sync2 do some operations synchronously and return the results. As a result, one can call these functions like this:result = sync() result2 = sync2() result3 = sync3() What if the operation is asynchronous? Let’s see it in action in Listing 10-2.let async = (fn) => {         //some async operation         //call the callback with async operation         fn(/*  result data /) } let async2 = (fn) => {         //some async operation         //call the callback with async operation         fn(/  result data /) } let async3 = (fn) => {         //some async operation         //call the callback with async operation         fn(/  result data */) } Listing 10-2

异步函数

同步与异步

同步是指函数在执行时阻塞调用者,并在结果可用时返回结果。

异步是指函数在执行时不阻塞调用者,而是返回可用的结果。

当我们在项目中处理 AJAX 请求时,我们会大量处理异步。

Now if someone wants to process these functions at once, how they do it? The only way to do it is shown in Listing 10-3.async(function(x){     async2(function(y){         async3(function(z){             ...         });     }); }); Listing 10-3

异步函数调用示例

哎呀!你可以在清单 10-3 中看到,我们将许多回调函数传递给我们的异步函数。这段代码展示了什么是回调地狱。回调地狱让程序更难理解。处理错误和从回调中冒泡错误是很棘手的,并且总是容易出错。

在 ES6 到来之前,JavaScript 开发者用承诺来解决这个问题。承诺是伟大的,但鉴于 ES6 在语言层面引入了生成器,我们不再需要承诺了!

发电机 101

如前所述,生成器是 ES6 规范的一部分,它们在语言级别被捆绑在一起。我们讨论了使用生成器来帮助处理异步代码。不过,在这之前,我们要谈谈发电机的基本原理。本节重点解释生成器背后的核心概念。一旦我们学习了基础知识,我们就可以使用生成器创建一个通用函数来处理我们库中的异步代码。我们开始吧。

创建生成器

Let’s start our journey by seeing how to create generators in the first place. Generators are nothing but a function that comes up with its own syntax. A simple generator looks like Listing 10-4.function* gen() {     return 'first generator'; } Listing 10-4

第一个简单生成器

The function gen in Listing 10-4 is a generator. As you might notice, we have used an asterisk before our function name (in this case gen) to denote that it is a generator function. We have seen how to create a generator; now let’s see how to invoke a generator:let generatorResult = gen() What will be the result of generatorResult ? Is it going to be a first generator value? Let’s print it on the console and inspect it:console.log(generatorResult) The result will be:gen {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}

发电机的警告

前面的示例显示了如何创建生成器,如何为它创建实例,以及它如何获取值。然而,当我们使用发电机时,有一些重要的事情需要注意。

The first thing is that we cannot call next as many times as we want to get the value from the generator. To make it clearer, let’s try to fetch a value from our first generator (refer to Listing 10-4 for the first generator definition):let generatorResult = gen() //for the first time generatorResult.next().value => 'first generator' //for the second time generatorResult.next().value => undefined

正如您在这段代码中看到的,第二次调用 next 将返回一个未定义的而不是第一个生成器。原因是生成器就像序列:一旦序列的值被消耗,你就不能再消耗它。在我们的例子中,generatorResult 是一个作为第一个生成器有值的序列。通过对 next 的第一次调用,我们(作为生成器的调用方)消耗了序列中的值。因为序列现在是空的,所以第二次调用它将返回未定义的结果。

To consume the sequence again, you need to create another generator instance:let generatorResult = gen() let generatorResult2 = gen() //first sequence generatorResult.next().value => 'first generator' //second sequence generatorResult2.next().value => 'first generator'

这段代码还显示了生成器的不同实例可以处于不同的状态。这里的要点是,每个生成器的状态取决于我们如何调用它的下一个函数。

yield 关键字

With generator functions, there is a new keyword that we can use called yield. In this section, we are going to see how to use yield within a generator function. Let’s start with the code in Listing 10-5.function* generatorSequence() {     yield 'first';     yield 'second';     yield 'third'; } Listing 10-5

简单生成器序列

As usual we can create a generator instance for that code:let generatorSequence = generatorSequence(); Now if we call next for the first time we get back the value first:generatorSequence.next().value => first What happens if we call next again? Do we get first? Or second? Or third? Or an error? Let’s find out:generatorSequence.next().value => second We got back the value second. Why? yield makes the generator function pause the execution and send back the result to the caller. Therefore when we call generatorSequence for the first time, the function sees the yield with value first, so it puts the function to pause mode and returns the value (and it remembers where it exactly paused, too). The next time we call the generatorSequence (using the same instance variable), the generator function resumes from where it left off. Because it paused at the line:yield 'first';

第一次,当我们第二次调用它(使用同一个实例变量)时,我们得到值 second。当我们第三次调用它时会发生什么?是的,我们将得到第三个值。

This is better explained by looking at Figure 10-1. This sequence is explained via the code in Listing 10-6.//get generator instance variable let generatorSequenceResult = generatorSequence(); console.log('First time sequence value',generatorSequenceResult.next().value) console.log('Second time sequence value',generatorSequenceResult.next().value) console.log('third time sequence value',generatorSequenceResult.next().value) Listing 10-6

调用我们的生成器序列

img/429083_2_En_10_Fig1_HTML.jpg Figure 10-1

清单 10-4 中所列发电机的视觉视图

This prints the following back to the console:First time sequence value first Second time sequence value second third time sequence value third

有了这样的理解,你就能明白为什么我们称一个生成器为一系列值了。需要记住的更重要的一点是,所有具有 yield 的生成器都将按照惰性求值顺序执行。

懒惰评估

什么是懒评?简单地说,懒惰评估意味着代码在我们请求它运行之前不会运行。正如您所猜测的,generatorSequence 函数的例子显示了生成器是惰性的。这些值只有在我们需要时才被执行和返回。对发电机太懒惰了,不是吗?

生成器的完成属性

现在我们已经看到了生成器如何使用 yield 关键字生成一系列值。一个生成器也可以产生 n 个数列;作为生成器函数的用户,您将如何知道下一次何时停止调用?因为对已经使用的生成器序列调用 next 将返回未定义的值。你如何处理这种情况?这就是 done 属性进入画面的地方。

Remember that every call to the next function is going to return an object that looks like this:{value: 'value', done: false}

我们知道这个值是来自我们的生成器的值,但是 done 呢?done 是一个属性,它将告诉我们生成器序列是否已被完全使用。

We rerun the code from previous sections here (Listing 10-4), just to print the object being returned from the next call.//get generator instance variable let generatorSequenceResult = generatorSequence(); console.log('done value for the first time',generatorSequenceResult.next()) console.log('done value for the second time',generatorSequenceResult.next()) console.log('done value for the third time',generatorSequenceResult.next()) Listing 10-7

用于理解 done 属性的代码

Running this code will print the following:done value for the first time { value: 'first', done: false } done value for the second time { value: 'second', done: false } done value for the third time { value: 'third', done: false } As you can see we have consumed all the values from the generator sequence, so calling next again will return the following object:console.log(generatorSequenceResult.next()) => { value: undefined, done: true } Now the done property clearly tells us that the generator sequence is already fully consumed. When the done is true, it’s time for us to stop calling next on that particular generator instance. This can be better visualized with Figure 10-2.img/429083_2_En_10_Fig2_HTML.jpg Figure 10-2

generatorSequence 的生成器完成属性视图

Because generator became the core part of ES6, we have a for loop that will allow us to iterate a generator (after all it’s a sequence):for(let value of generatorSequence())         console.log("for of value of generatorSequence is",value) This is going to print:for of value of generatorSequence is first for of value of generatorSequence is second for of value of generatorSequence is third

特别是使用生成器的 done 属性来遍历它。

将数据传递给生成器

在这一节中,让我们讨论如何将数据传递给生成器。起初,将数据传递给生成器可能会让人感到困惑,但是正如您将在本章中看到的,这使得异步编程变得容易。

Let’s take a look at the code in Listing 10-8.function* sayFullName() {     var firstName = yield;     var secondName = yield;     console.log(firstName + secondName); } Listing 10-8

传递数据生成器示例

This code now might not be a surprise for you. Let’s use this code to explain the concept of passing data to the generator. As always, we create a generator instance first:let fullName = sayFullName() Once the generator instance is created, let’s call next on it:fullName.next() fullName.next('anto') fullName.next('aravinth') => anto aravinth In this code snippet the last call will print anto aravinth to the console. You might be confused with this result, so let’s walk through the code slowly. When we call next for the first time:fullName.next() the code will return and pause at the linevar firstName = yield; Because here we are not sending any value back via yield , next will return the value undefined. The second call to next is where an interesting thing happens:fullName.next('anto') Here we are passing the value anto to the next call. Now the generator will be resumed from its previous paused state. Remember that the previous paused state is on the linevar firstName = yield; Because we have passed the value anto on this call, yield will be replaced by anto and thus firstName holds the value anto. After the value is set to firstName, the execution will be resumed (from the previous paused state) and again sees the yield and stops the execution atvar secondName = yield; Now for the third time, if we call next:fullName.next('aravinth') When this line gets executed, our generator will resume from where it paused. The previous paused state isvar secondName = yield; As before, the passed value aravinth of our next call will be replaced by yield and aravinth is set to secondName. Then the generator happily resumes the execution and sees this statement:console.log(firstName + secondName); By now, firstName is anto and secondName is aravinth, so the console will print anto aravinth. This full process is illustrated in Figure 10-3.img/429083_2_En_10_Fig3_HTML.jpg Figure 10-3

解释数据如何传递给 sayFullName 生成器

您可能想知道为什么我们需要这样的方法。事实证明,通过向生成器传递数据来使用生成器使它变得非常强大。我们在下一节中使用相同的技术来处理异步调用。

使用生成器处理异步调用

在这一节中,我们将使用真实世界的生成器。我们将看到向生成器传递数据如何使它们在处理异步调用时变得非常强大。在这一部分,我们会玩得很开心。

异步生成器:一个简单的例子

在这一节中,我们将看到如何使用生成器来处理异步代码。因为我们从使用生成器解决异步问题的不同心态开始,所以我们希望事情简单,所以我们将使用 setTimeout 调用来模拟异步调用!

Imagine you two functions shown in Listing 10-9 (which are async in nature).let getDataOne = (cb) => {         setTimeout(function(){         //calling the callback         cb('dummy data one')     }, 1000); } let getDataTwo = (cb) => {         setTimeout(function(){         //calling the callback         cb('dummy data two')     }, 1000); } Listing 10-9

简单的异步函数

Both these functions mimic the async code with setTimeout . Once the desired time has elapsed, setTimeout will call the passed callback cb with value dummy data one and dummy data two, respectively. Let’s see how we will be calling these two functions without generators in the first place:getDataOne((data) => console.log("data received",data)) getDataTwo((data) => console.log("data received",data)) That code will print the following after 1,000 ms:data received dummy data one data received dummy data two

现在,正如你所注意到的,我们通过回调来获得响应。我们已经讨论了异步代码中的回调有多糟糕。让我们用我们的发电机知识来解决当前的问题。我们现在更改 getDataOne 和 getDataTwo 函数,使用生成器实例而不是回调来传递数据。

First let’s change the function getDataOne (Listing 10-8) to what is shown in Listing 10-10.let generator; let getDataOne = () => {         setTimeout(function(){         //call the generator and         //pass data via next         generator.next('dummy data one')     }, 1000); } Listing 10-10

将 getDataOne 更改为使用生成器

We have changed the callback line from. . . cb('dummy data one') . . . togenerator.next('dummy data one') That’s a simple change. Note that we have also removed the cb, which is not required in this case. We will do the same for getDataTwo (Listing 10-8), too, as shown in Listing 10-11.let getDataTwo = () => {         setTimeout(function(){         //call the generator and         //pass data via next         generator.next('dummy data two')     }, 1000); } Listing 10-11

将 getDataTwo 更改为使用生成器

Now with that change in place, let’s go and test our new code. We’ll wrap our call to getDataOne and getDataTwo inside a separate generator function, as shown in Listing 10-12.function* main() {     let dataOne = yield getDataOne();     let dataTwo = yield getDataTwo();     console.log("data one",dataOne)     console.log("data two",dataTwo) } Listing 10-12

主发电机功能

Now the main code looks exactly like the sayFullName function from our previous section. Let’s create a generator instance for main and trigger the next call and see what happens.generator = main() generator.next(); That will print the following to the console:data one dummy data one data two dummy data two

这正是我们想要的。看看我们的主代码;代码看起来像对函数 getDataOne 和 getDataTwo 的同步调用。然而,这两个调用都是异步的。请记住,这些调用永远不会阻塞,它们以异步方式工作。让我们总结一下整个过程是如何运作的。

First we are creating a generator instance for main using the generator variable that we declared earlier. Remember that this generator is used by both getDataOne and getDataTwo to push the data to its call, which we will see soon. After creating the instance, we are firing the whole process with the linegenerator.next() This calls the main function . The main function is put into execution and we see the first line with yield:. . . let dataOne = yield getDataOne(); . . .

现在,生成器将进入暂停模式,因为它已经看到了一个 yield 语句。不过,在进入暂停模式之前,它调用了函数 getDataOne。

注意

这里重要的一点是,即使 yield 使语句暂停,它也不会使调用者等待(即调用者没有被阻塞)。为了更具体地说明这一点,请参见下面的代码。

generator . next()//即使生成器因异步代码而暂停

console.log("将被打印")

= >将被打印

= >打印发电机数据结果

这段代码表明,即使我们的 generator.next 使生成器函数等待下一次调用,调用者(调用生成器的那个)也不会被阻塞!如您所见,console.log 将被打印出来(展示 generator.next 没有被阻塞),然后一旦异步操作完成,我们就从生成器中获取数据。

Now interestingly the getDataOne function has the following line in its body:. . .     generator.next('dummy data one') . . . As we discussed earlier, calling next by passing a parameter will resume the paused yield, and that’s exactly what happens here in this case. Remember that this piece of line is inside setTimeout , so it will get executed only when 1,000 ms have elapsed. Until then, the code will be paused at the linelet dataOne = yield getDataOne(); One more important point to note here is that while this line is paused, the timeout will be running down from 1,000 to 0. Once it reaches 0, it is going to execute the line. . .     generator.next('dummy data one') . . . That is going to send back dummy data one to our yield statement , so the dataOne variable becomes dummy data one://after 1,000 ms dataOne becomes //'dummy data one' let dataOne = yield getDataOne(); => dataOne = 'dummy data one' That’s a lot of interesting stuff happening. Once dataOne is set to the dummy data one value, the execution will continue to the next line:. . . let dataTwo = yield getDataTwo(); . . . This line is going to run the same way as the line before! So after the execution of this line, we have dataOne and dataTwo :dataOne = dummy data one dataTwo = dummy data two That is what is getting printed to the console at the final statements of the main function :. . .     console.log("data one",dataOne)     console.log("data two",dataTwo) . . . The full process is shown in Figure 10-4.img/429083_2_En_10_Fig4_HTML.jpg Figure 10-4

解释主生成器内部工作方式的图像

现在,您已经使异步调用看起来像同步调用,但是它以异步方式工作。

异步的生成器:一个真实的例子

在上一节中,我们看到了如何使用生成器有效地处理异步代码。为了模拟异步工作流,我们使用了 setTimeout。在这一节中,我们将使用一个函数来触发对 Reddit APIs 的真正 AJAX 调用,以展示现实世界中生成器的强大功能。

To make an async call, let’s create a function called httpGetAsync , shown in Listing 10-13.let https = require('https'); function httpGetAsync(url,callback) {     return https.get(url,         function(response) {             var body = ";             response.on('data', function(d) {                 body += d;             });             response.on('end', function() {                 let parsed = JSON.parse(body)                 callback(parsed)             })         }     ); } Listing 10-13

httpGetAsync 函数定义

这是一个简单的函数,它使用来自一个节点的 https 模块来触发 AJAX 调用以获得响应。

注意

这里我们不打算详细了解 httpGetAsync 函数是如何工作的。我们试图解决的问题是如何转换像 httpGetAsync 这样的函数,它以异步方式工作,但需要一个回调来获得 AJAX 调用的响应。

Let’s check httpGetAsync by passing a Reddit URL:httpGetAsync('www.reddit.com/r/pics/.jso… {         console.log(data) }) It works by printing the data to the console. The URL www.reddit.com/r/pics/.json prints the list of JSON about the Picture Reddit page. The returned JSON has a data key with a structure that looks like the following:{ modhash: ",   children:    [ { kind: 't3', data: [Object] },      { kind: 't3', data: [Object] },      { kind: 't3', data: [Object] },      . . .      { kind: 't3', data: [Object] } ],   after: 't3_5bzyli',   before: null }

假设我们想要获得数组的第一个子元素的 URL 我们需要导航到 data.children[0].data.url。这会给我们一个类似www . Reddit . com/r/pics/comments/5bqai 9/introducing _ new _ rpics _ title _ guidelines/的 URL。因为我们需要获得给定 URL 的 JSON 格式,所以我们需要追加。json 到网址,这样就变成了www . Reddit . com/r/pics/comments/5bqai 9/introducing _ new _ rpics _ title _ guidelines/。json

Now let’s see that in action:httpGetAsync('www.reddit.com/r/pics/.jso… {     httpGetAsync(picJson.data.children[0].data.url+".json",(firstPicRedditData) => {         console.log(firstPicRedditData)     }) })

该代码将根据需要打印数据。我们最不担心被打印的数据,但我们担心我们的代码结构。正如我们在本章开始时看到的,看起来像这样的代码遭受回调地狱。这里有两个层次的回调,这可能不是一个真正的问题,但如果它到了四个或五个嵌套层次呢?你能容易地阅读这样的代码吗?绝对不是。现在让我们看看如何通过发电机解决这个问题。

Let’s wrap httpGetAsync inside a separate method called request, shown in Listing 10-14.function request(url) {     httpGetAsync( url, function(response){         generator.next( response );     } ); } Listing 10-14

请求功能

We have removed the callback with the generator’s next call, very similar to our previous section. Now let’s wrap our requirement inside a generator function; again we call it main, as shown in Listing 10-15.function *main() {     let picturesJson = yield request( "www.reddit.com/r/pics/.jso…" );     let firstPictureData = yield request(picturesJson.data.children[0].data.url+".json")     console.log(firstPictureData) } Listing 10-15

主发电机功能

这个主函数看起来非常类似于我们在清单 10-11 中定义的主函数(唯一的变化是方法调用细节)。在代码中,我们对两个请求调用让步。正如我们在 setTimeout 示例中看到的,在请求时调用 yield 将使它暂停,直到请求通过发送回 AJAX 响应来调用生成器。第一个 yield 会得到图片的 JSON,第二个 yield 通过调用 request 分别得到第一个图片数据。现在我们已经使代码看起来像同步代码,但实际上,它以异步方式工作。

我们也使用生成器逃离了回调地狱。现在代码看起来很干净,清楚地说明了它在做什么。这对我们来说更有力量!

Try running it:generator = main() generator.next()

它将按要求打印数据。我们已经清楚地看到了如何使用生成器将任何期望回调机制的函数转换成基于生成器的函数。反过来,我们得到处理异步操作的干净代码。

ECMAScript 2017 中的异步函数

到目前为止,我们已经看到了多种异步运行函数的方法。最初,执行后台任务的唯一方式是使用回调,但是我们刚刚了解了它们是如何导致回调地狱的。生成器或序列提供了一种使用 yield 操作符和生成器函数解决回调问题的方法。作为 ECMA8 脚本的一部分,引入了两个新的操作符,称为 async 和 await。这两个新操作符通过引入使用 Promise 创作异步代码的现代设计模式,解决了回调地狱问题。

承诺

If you are already aware of Promises you can skip this section. A Promise in JavaScript world is piece of work that is expected to complete (or fail) at some point in the future. For example, parents might Promise to give their child an XBOX if they get an A+ on an upcoming test, as represented by the following code.let grade = "A+"; let examResults = new Promise(     function (resolve, reject) {         if (grade == "A+")             resolve("You will get an XBOX");         else             reject("Better luck next time");     } ); Now, the Promise examResults when consumed can be in any of three states: pending, resolved, or rejected. The following code shows a sample consumption of the preceding Promise.let conductExams = () => {     examResults     .then(x => console.log(x)) // captures resolve and logs "You will get an XBOX"     .catch(x => console.error(x)); // captures rejection and logs "Better luck next time" }; conductExams();

现在,如果你已经成功地重新学习了承诺的哲学,我们就能理解 async 和 wait 做什么了。

等待

await 是一个关键字,如果函数返回一个 Promise 对象,可以将它添加到函数的前面,从而使它在后台运行。通常使用一个函数或另一个承诺来消费一个承诺,而 await 通过允许承诺在后台解析来简化代码。换句话说,await 关键字等待承诺解决或失败。一旦承诺得到解决,由承诺返回的数据——无论是已解决的还是被拒绝的——都可以被使用,但同时应用程序的主要流程可以畅通无阻地执行任何其他重要任务。当承诺完成时,剩下的执行就展开了。

异步ˌ非同步(asynchronous)

使用 await 的函数应该标记为 async。

Let us understand the usage of async and await using the following example.function fetchTextByPromise() {     return new Promise(resolve => {         setTimeout(() => {             resolve("es8");         }, 2000);     }); } Before ES8 can consume this Promise, you might have to wrap it in a function as shown in the preceding example or use another Promise as shown here.function sayHello() {     return new Promise((resolve, reject) => fetchTextByPromise()   .then(x => console.log(x))         .catch(x => console.error(x))); } Now, here is a much simpler and cleaner version using async and await.async function sayHello() {     const externalFetchedText = await fetchTextByPromise();     console.log(Response from SayHello: Hello, ${externalFetchedText}); } We can also write using arrow syntax as shown here.let sayHello = async () => {     const externalFetchedText = await fetchTextByPromise();     console.log(Response from SayHello: Hello, ${externalFetchedText}); // Hello, es8 } You can consume this method by simply callingsayHello()

链接回调

在我们看到远程 API 调用的一些示例使用之前,async 和 await 的优点是很难理解的。下面是一个例子,我们调用一个远程 API 来返回一个 JSON 数组。我们静静地等待数组到达并处理第一个对象,然后进行另一个远程 API 调用。这里要学习的重要一点是,当所有这些发生时,主线程可以处理其他事情,因为远程 API 调用可能需要一些时间;因此,网络调用和相应的处理在后台进行。

Here is the function that invokes a remote URL and returns a Promise.// returns a Promise const getAsync = (url) => {     return fetch(url)         .then(x => x)         .catch(x =>             console.log("Error in getAsync:" + x)         ); } The next function consumes getAsync .// 'async' can only be used in functions where 'await' is used async function getAsyncCaller() {     try {         // jsonplaceholder.typicode.com/users is a sample API which returns a JSON Array of dummy users         const response = await getAsync("jsonplaceholder.typicode.com/users"); // pause until Promise completes         const result = await response.json(); //removing .json here demonstrates the error handling in Promises         console.log("GetAsync fetched " + result.length + " results");         return result;     } catch (error) {         await Promise.reject("Error in getAsyncCaller:" + error.message);     } } The following code is used to invoke the flow.getAsyncCaller()     .then(async (x) => {         console.log("Call to GetAsync function completed");         const website = await getAsync("http://" + x[0].website);         console.log("The website (http://" + x[0].website + ") content length is " + website.toString().length + " bytes");     })     .catch(x => console.log("Error: " + x)); // Promise.Reject is caught here, the error message can be used to perform custom error handling Here is the output for the preceding invocation: This message is displayed while waiting for async operation to complete, you can do any compute here... GetAsync fetched 10 results Call to GetAsync function completed The website (hildegard.org) content length is 17 bytes As you can see, the code execution continues and prints the following console statement, which is the last statement in the program, while the remote API call is happening in the background. Any code following this also gets executed.console.log("This message is displayed while waiting for async operation to complete, you can do any compute here..."); The following result is available when the first await completes; that is, the first API call is completed, and the results are enumerated. This message is displayed while waiting for async operation to complete, you can do any compute here... GetAsync fetched 10 results Call to GetAsync function completed At this point the control returns to the caller, getAsyncCaller in this case, and the call is again awaited by the async call, which makes another remote call using the website property. Once the final API call is completed, the data are returned to the website object and the following block is executed:        const website = await getAsync("http://" + x[0].website);         console.log("The website (http://" + x[0].website + ") content length is " + website.toString().length + " bytes");

您可以观察到,我们已经异步地进行了相关的远程 API 调用,但是代码看起来是扁平的和可读的,因此调用层次结构可以增长到任何程度,而不涉及任何回调层次结构。

异步调用中的错误处理

As explained earlier, Promises can be rejected as well (say the Remote API is not available or the JSON format is incorrect). In such cases the consumer’s catch block is invoked, which can be used to perform any custom exception handling, as shown here.        await Promise.reject("Error in getAsyncCaller:" + error.message); The error can be bubbled to the caller’s catch block as well, as shown next. To simulate an error, remove the .json function getAsyncCaller (read the comments for more details). Also, observe the async usage in the then handler here. Because the dependent remote call uses await the arrow function can be tagged as async.getAsyncCaller()     .then(async (x) => {         console.log("Call to GetAsync function completed");         const website = await getAsync("http://" + x[0].website);         console.log("The website (http://" + x[0].website + ") content length is " + website.toString().length + " bytes");     })     .catch(x => console.log("Error: " + x)); // Promise.Reject is caught here, the error message can be used to perform custom error handling The new asynchronous pattern is more readable, includes less code, is linear, and is better than the previous ones, making it an instinctive replacement for the previous patterns. Figure 10-5 shows the browser support at the time of writing. For latest information, you can check the browser support from caniuse.com/#feat=async-functions .img/429083_2_En_10_Fig5_HTML.jpg Figure 10-5

异步浏览器支持。来源:https://caniuse.com/#feat=async-functions

传输到生成器的异步函数

Async and await have an awfully close relationship with generators. In fact, Babel transpiles async and await to generators in the background, which is quite evident if you look at the transpiled code.let sayHello = async () => {     const externalFetchedText = await new Promise(resolve => {         setTimeout(() => {             resolve("es8");         }, 2000)});     console.log(Response from SayHello: Hello, ${externalFetchedText}); }

例如,前面的 async 函数将被编译成下面的代码,您可以使用任何在线 Babel transpiler,如 babeljs.io 来观看转换。transpiled 代码的详细解释超出了本书的范围,但是您可能会注意到,关键字 async 被转换成了一个名为 _asyncToGenerator 的包装函数(第 3 行)。_asyncToGenerator 是 Babel 添加的一个例程。对于任何使用 async 关键字的代码,这个函数都将被拉入 transpiled 代码中。我们前面代码的关键被转换成一个 switch case 语句(第 41–59 行),其中每一行代码都被转换成一个 case,如下所示。

img/429083_2_En_10_Figa_HTML.jpg

然而,async/await 和 generators 是在 JavaScript 中创作线性异步函数的两种最突出的方式。决定使用哪一个纯粹是选择的问题。async/await 模式使异步代码看起来像 sync,因此增加了可读性,而生成器对生成器内的状态变化以及调用者和被调用者之间的双向通信提供了更好的控制。

摘要

这个世界充满了 AJAX 调用。曾经在处理 AJAX 调用时,我们需要传递一个回调来处理结果。回调有其自身的局限性。例如,过多的回调会产生回调地狱问题。我们在本章中已经看到了 JavaScript 中的一种类型,叫做 generator。生成器是可以暂停并使用下一个方法恢复的函数。下一个方法适用于所有生成器实例。我们已经看到了如何使用 next 方法将数据传递给生成器实例。向生成器发送数据的技术有助于我们解决异步代码问题。我们已经看到了如何使用生成器使异步代码看起来同步,这对于任何 JavaScript 开发人员来说都是一项非常强大的技术。生成器是解决回调地狱问题的一种方式,但是 ES8 提供了另一种直观的方式来使用 async 和 await 解决相同的问题。新的异步模式由 Babel 等编译器在后台传输到生成器中,并使用 Promise 对象。Async/await 可用于以简单、优雅的方式编写线性异步函数。Await(相当于 generators 中的 yield)可以与任何返回 Promise 对象的函数一起使用,如果一个函数在函数体中的任何地方使用 await,它应该被标记为 async。新模式还简化了错误处理,因为同步和异步代码引发的异常可以用相同的方式处理。

十一、构建一个类似 React 的库

到目前为止,我们已经学会了编写功能性 JavaScript 代码,并体会到它给应用程序带来的模块化、可重用性和简单性。我们已经看到了诸如组合、过滤器、映射、减少等概念,以及其他诸如异步、等待和管道等特性。尽管如此,我们还没有将这些特性结合起来构建一个可重用的库。这是我们在本章将要学习的内容。在这一章中,我们构建了一个完整的库,它将有助于构建应用程序,就像 React 或 HyperApp(hyperapp.js.org)。本章致力于构建应用程序,而不仅仅是函数。我们将使用到目前为止学到的函数式 JavaScript 编程概念构建两个 HTML 应用程序。我们将学习如何使用中央存储构建应用程序,使用声明性语法呈现用户界面(UI ),以及使用我们的自定义库连接事件。我们将构建一个微型 JavaScript 库,它将能够呈现带有行为的 HTML 应用程序。在下一章中,我们将学习为我们在本章中构建的库编写单元测试。

在开始构建库之前,我们需要理解 JavaScript 中一个非常重要的概念,叫做不变性。

注意

章节示例和库源代码在第十一章分支中。回购的网址是github.com/antsmartian/functional-es8.git

一旦你检查出代码,请检查分支第十一章:

git checkout -b 第十一章来源/第十一章

以管理员身份打开命令提示符,导航到包含 package.json 的文件夹,然后运行

npm 安装

下载代码运行所需的包。

不变

JavaScript functions act on data, which are typically stored in variables like strings, arrays, or objects. The state of data is usually defined as the value of the variable at any given point in time. For example:let x = 5; // the state of x is 5 here let y = x; // the state of y is same as that of x y = x * 2; // we are altering the state of y console.log('x = ' + x); // prints: x=5; x is intact, pretty simple console.log('y = ' + y); // prints: y=10 Now consider string data type:let x = 'Hello'; // the state of x is Hello here let y = x; // the state of y is same as x x = x + ' World'; // altering the state of x console.log('x = ' + x);  // prints: x = Hello World console.log('y = ' + y);  // prints: y = y = Hello ; Value of y is intact So, to conclude JavaScript numbers and strings are immutable. The state of these variable types cannot be altered after it is created. That is not the case with objects and arrays, however. Consider this example:let x = { foo : 'Hello' }; let y = x; // the state of y should be the same as x x.foo +=  ' World'; // altering the state of x console.log('x = ' + x.foo); // prints: x = Hello World console.log('y = ' + y.foo); // prints: y = Hello World; y is also impacted

JavaScript 对象和数组是可变的,可变对象的状态可以在创建后修改。

注意

这也意味着等式对于可变对象不是一个可靠的操作符,因为在一个地方改变一个值将会更新所有的引用。

Here is an example for arrays.let x = [ 'Red', 'Blue']; let y = x; x.push('Green'); console.log('x = ' + x); // prints [ 'Red', 'Blue', 'Green' ] console.log('y = ' + y); // prints [ 'Red', 'Blue', 'Green' ] If you would like to enforce immutability onto JavaScript objects, it is possible by using Object.freeze . Freeze makes the object read-only. For example, consider this code:let x = { foo : 'Hello' }; let y = x; Object.freeze(x); // y.foo +=  ' World'; // uncommenting the above line will throw an error, both x and y are made read-only. console.log('x = ' + x.foo); console.log('y = ' + y.foo); To summarize, Table 11-1 differentiates the mutable and immutable types in JavaScript.Table 11-1

JavaScript 中的数据类型

|

不可变类型

|

可变类型

| | --- | --- | | 数字,字符串 | 对象,数组 |

对于构建可跨项目重用的模块化 JavaScript 库来说,不变性是一个非常重要的概念。应用程序的生命周期由其状态驱动,JavaScript 应用程序主要将状态存储在可变对象中。预测应用程序在任何给定时间点的状态是至关重要的。

在下一节中,我们将构建一个可用作可预测状态容器的库。在这个库中,我们使用了不变性和我们之前学过的各种函数式编程概念。

构建一个简单的 Redux 库

Redux 是一个库,其灵感来自流行的单一应用程序架构,如 Flux、CQRS 和事件源。Redux 帮助您集中应用程序状态,并帮助您构建可预测的状态模式。在理解 Redux 是什么之前,让我们试着理解在少数流行的 JavaScript 框架中状态是如何处理的。让我们以 Angular 为例。Angular 应用依赖于文档对象模型(DOM)来存储状态,数据被绑定到称为视图(或 DOM)的 UI 组件,视图表示模型,反过来模型的变化可以更新视图。当应用程序随着您添加新功能而水平扩展时,预测状态变化的级联效应变得非常具有挑战性。在任何给定的时间点,状态都可能被应用程序或另一个模型中的任何组件更改,这使得确定应用程序状态更改的时间和原因变得非常不可预测。另一方面,React 使用虚拟化的 DOM 工作。给定任何状态,React 应用程序都会创建一个虚拟 DOM,然后可以呈现这个虚拟 DOM。

Redux is a framework-agnostic state library. It can be used with Angular, React, or any other application. Redux is built to address the common problems with application state and how they are influenced by models and views. Redux is inspired by Flux, an application architecture introduced by Facebook. Redux uses a unidirectional flow of data. The following are the design principles of Redux.

  • *单一真值来源:*应用有一个中心状态。

  • *状态只读:*称为动作的特殊事件描述状态变化。

  • *变化由纯函数产生:*动作由 reducer 消耗,reducer 是纯函数,在识别用户动作时可以调用。一次只发生一个变化。

Redux 的关键特征是有一个单一的真理来源(状态)。状态本来就是只读的,所以改变状态的唯一方法是发出一个描述发生了什么的动作。这个动作被 reducer 使用,并创建了一个新状态,这又触发了一个 DOM 更新。这些动作可以被存储和重放,这允许我们做像时间旅行调试这样的事情。如果你仍然困惑,不要担心;继续读下去,当我们开始使用我们到目前为止所学的知识来实现它时,模式会变得更加简单。

Figure 11-1 shows how Redux implements predictable state container.img/429083_2_En_11_Fig1_HTML.jpg Figure 11-1

状态容器的 Redux 实现

img/429083_2_En_11_Fig2_HTML.jpg Figure 11-2

使用 redux 库的例子

Redux 的关键组件是 reducers、动作和状态。有了这个背景,让我们开始构建自己的 Redux 库。

注意

我们在这里构建的 Redux 库还不能用于生产;相反,Redux 示例用于展示函数式 JavaScript 编程的强大功能。

Create a new folder for the Redux library and create a new file called redux.js that will host our library. Copy and paste the code from the following sections into this file. You can use any JavaScript editor of your choice; for example, VS Code. The first and most important part of our Redux library is state. Let’s declare a simple state with one property called counter .let initialState = {counter: 0}; The next key ingredient is reducer, the only function that can alter the state. A reducer takes two inputs: the current state and an action that acts on the current state and creates a new state. The following function acts as reducer in our library:function reducer(state, action) {   if (action.type === 'INCREMENT') {     state = Object.assign({}, state, {counter: state.counter + 1})   }   return state; }

在第四章中,我们讨论了 Object.assign 通过合并旧状态来创建新状态的用法。当您想避开可变性时,这种方法非常有用。reducer 函数负责在不改变当前状态的情况下创建新状态。您可以看到我们如何使用 object.assign 来实现这一点:object.assign 用于通过将两个状态合并为一个状态来创建一个新状态,而不会影响 state 对象。

The action is dispatched by a user interaction; in our example it is a simple button click as shown here.document.getElementById('button').addEventListener('click', function() {     incrementCounter();   }); When the user clicks a button with Id button the incrementCounter is invoked. Here is the code for incrementCounter:function incrementCounter() {   store.dispatch({     type: 'INCREMENT'   }); } What is store? store is the main function that encapsulates behaviors that cause the state to change, invokes listeners for state change like UI, and registers listeners for the actions. A default listener in our case is the view renderer. The following function elaborates how a store looks.function createStore(reducer,preloadedState){   let currentReducer = reducer;     let currentState = preloadedState;     let currentListeners = [];     let nextListeners = currentListeners;     function getState() {       return currentState;     }     function dispatch(action) {         currentState = currentReducer(currentState, action);         const listeners = currentListeners = nextListeners;       for (let i = 0; i < listeners.length; i++) {         const listener = listeners[i];         listener();       }       return action;     }     function subscribe(listener) {       nextListeners.push(listener);     }     return {       getState,       dispatch,       subscribe     }; } The following code is our one and only listener that renders the UI when there is a change in state.function render(state) {   document.getElementById('counter').textContent = state.counter; } The following code shows how the listener is subscribed using the subscribe method .store.subscribe(function() {   render(store.getState()); }); This code is used to bootstrap the application:let store = createStore(reducer, initialState); function loadRedux(){     // Render the initial state     render(store.getState()); } It is time to plug our Redux library into an application, create a new file called index.html under the same folder, and paste in the following code.       

Chapter 11 - Redux Sample

        

-

        Increase          The function loadRedux is invoked on page load. Let us understand the life cycle of our application.

    加载时 : 创建 Redux store 对象,使用 store.subscribe 注册监听器,同时注册 onclick 事件调用 reducer。

    点击 : 调度程序被调用,它创建一个新的状态并调用监听器。

    On render : 监听器(render 函数)获取更新后的状态并呈现新的视图。

This cycle continues until the application is unloaded or destroyed. You can either open index.html in a new file or update package.json with the following code (to see the details of the full package.json, check out the branch mentioned at the beginning of the chapter)."scripts": {     "playground" : "babel-node functional-playground/play.js --presets es2015-node5",     "start" : "open functional-playground/index.html"   } To run the application you can run this command, which opens index.html in the browser:npm run start

注意,在 UI 上执行的每个动作都存储在 Redux store 中,这为我们的项目增加了巨大的价值。如果您想知道应用程序当前状态的原因,只需遍历对初始状态执行的所有操作并重放它们;这个特征也被称为时间旅行。这种模式还可以帮助您在任何时间点撤销或重做状态更改。例如,您可能希望用户在 UI 中进行一些更改,但只基于某些验证提交这些更改。如果验证失败,您可以轻松地撤销状态。Redux 也可以和非 UI 应用一起使用;请记住,它是一个具有时间旅行功能的状态容器。如果你想了解更多关于 Redux 的信息,请访问 redux.js.org/。

构建一个类似 HyperApp 的框架

Frameworks help reduce development time by allowing us to build on something that already exists and to develop applications within less time. The most common assumption with frameworks is that all the common concerns like caching, garbage collection, state management, and DOM rendering (applicable to UI frameworks only) are addressed. It would be like reinventing the wheel if you start to build an application without any of these frameworks. However, most of the frameworks available in the market to build a single-page UI application suffer from a common problem: bundle size. Table 11-2 provides the gzipped bundle size of most popular modern JavaScript frameworks.Table 11-2

流行 JavaScript 框架的捆绑包大小

|

名字

|

大小

| | --- | --- | | 角度 1.4.5 | 51K | | 角度 2 + Rx | 143K | | React 16.2.0 + React DOM | 31.8K | | Ember 2.2.0 | 111K |

来源:gist.github.com/Restuta/cda…

img/429083_2_En_11_Fig3_HTML.jpg Figure 11-3

下图显示了 JSFiddle 编辑器

另一方面,HyperApp 有望成为构建 UI 应用程序可用的最薄的 JavaScript 框架。HyperApp 的 gzip 版本为 1 KB。为什么我们在谈论一个已经建成的图书馆?本节的目的不是介绍或用 HyperApp 构建应用程序。HyperApp 建立在函数式编程概念之上,比如不变性、闭包、高阶函数等等。这是我们学习建立一个类似超级应用程序的库的主要原因。

因为 HyperApp 需要解析 JSX (JavaScript 扩展)语法等等,所以我们将在接下来的章节中学习什么是虚拟 Dom 和 JSX。

虚拟 DOM

DOM is a universally accepted language to represent documents like HTML. Each node in an HTML DOM represents an element in an HTML document. For example:

Hello, Alice

Logged in Date: 16th June 2018

JavaScript frameworks used to build UI applications intend to build and interact with DOM in a most efficient way. Angular, for example, uses a component-based approach. An application built using Angular contains multiple components, each storing part of the applicaion state locally at the component level. The state is mutable, and every state change rerenders the view, and any user interaction can update the state. For example, the preceding HTML DOM can be written in Angular as shown here:

Hello, {{username}}

➔ Component 1

Logged in Date: {{dateTime}}

➔ Component 2
The variables username and dateTime are stored on the component. Unfortunately, DOM manipulations are costly. Although this is a very popular model, it has various caveats, and here are a few.

    *状态不是中心的:*应用程序的状态本地存储在组件中,并在组件间传递,导致整体状态及其在任何给定时间点的转换的不确定性。

    *直接 DOM 操作:*每次状态改变都会触发一次 DOM 更新,所以在一个页面上有 50 个或更多控件的大型应用程序中,对性能的影响是非常明显的。

为了解决这些问题,我们需要一个能够集中存储和减少 DOM 操作的 JavaScript 框架。在上一节中,我们学习了 Redux,它可以用来构建一个中央可预测状态容器。使用虚拟 DOM 可以减少 DOM 操作。

Virtual DOM is an in-memory representation of DOM using JSON. The DOM operations are done on the in-memory representation before they are applied to the actual DOM. Based on the framework, the representation of DOM varies. The HyperApp library we discussed earlier uses Virtual DOM to detect the changes during state change and only re-creates the delta DOM, which leads to an increase in the overall efficiency of the application. The following is a sample representation of DOM used by HyperApp.{   name: "div",   props: {     id: "app"   },   children: [{     name: "h1",     props: null,     children: ["Hello, Alice"]   }] }

React 框架大量使用虚拟 DOM,它使用 JSX 来表示 DOM。

小艾

JSX is a syntax extension of JavaScript that can be used to represent DOM. Here is an example of JSX:const username = "Alice" const h1 =

Hello, {username}

; //HTML DOM embedded in JS React heavily uses JSX but it can live without it, too. You can put any valid JavaScript expression into the JSX expression like calling a function as shown next.const username = "aliCe"; const h1 =

Hello, {toTitleCase(username)}

; let toTitleCase = (str) => {     // logic to convert string to title case here }

我们将不深究 JSX 的概念;引入 JSX 和虚拟 DOM 的目的是让您熟悉这些概念。要了解更多关于 JSX 的信息,请访问 reactjs.org/docs/introd…

js 提琴手

在前面的所有章节中,我们已经执行了来自开发机器的代码。在本节中,我们将介绍一个名为 JS Fiddle(jsfiddle.net)的在线代码编辑器和编译器。JS Fiddle 可用于编码、调试和协作基于 HTML、JavaScript 和层叠样式表(CSS)的应用程序。JS Fiddle 包含现成的模板,它支持多种语言、框架和扩展。如果你打算做快速和肮脏的 POCs(概念证明)或者像这本书一样学习一些有趣的东西,JS Fiddle 是最好的工具。它允许您在线保存工作,并在任何地方工作,使我们不再需要为任何语言、编译器和库的新组合建立合适的开发环境。

Let us start building our library by creating a new JS Fiddle. Click Save on the top ribbon anytime you wish you save the code. As shown in Figure 11-4, in the Language drop-down list box, select Babel + JSX. In the Frameworks & Extensions drop-down list box, select No-Library (Pure JS). Selecting the right combination of language and framework is very important for the library to compile.img/429083_2_En_11_Fig4_HTML.jpg Figure 11-4

下图显示了该代码示例的框架和扩展选择

Our library consists of three main components: state, view, and actions (like HyperApp). The following function acts as a bootstrap for our library. Paste this code into the JavaScript + No-Library (Pure JS) code section.function main() {       app({ view: (state, actions) =>           

             Increase          Decrease          Change Text          

{state.count}

         

{state.changeText}

          
,           state : {               count : 5,           changeText : "Date: " + new Date().toString()           },           actions: {        down: state => ({ count: state.count - 1 }),        up: state => ({ count: state.count + 1 }),        changeText : state => ({changeText : "Date: " + new Date().toString()})      }       }) } The state here is a simple object. state : {               count : 5,               changeText : "Date: " + new Date().toString() } The actions do not change the state directly, but return a new state every time the action is called. The functions down, up, and changeText act on the state object passed as a parameter and return a new state object.actions: {        down: state => ({ count: state.count - 1 }),        up: state => ({ count: state.count + 1 }),        changeText : state => ({changeText : "Date: " + new Date().toString()}) } The view uses JSX syntax representing a Virtual DOM. The DOM elements are bound to the state object and the events are registered to the actions.   
             Increase          Decrease          Change Text          

{state.count}

         

{state.changeText}

The app function shown here is the crux of our library, which accepts state, view, and actions as a single JavaScript object and renders the actual DOM. Copy the following code into the JavaScript + No-Library (Pure JS) section.function app(props){ let appView = props.view; let appState = props.state; let appActions = createActions({}, props.actions) let firstRender = false; let node = h("p",{},"") } The function h is inspired from HyperApp, which creates a JavaScript object representation of DOM. This function is basically responsible for creating an in-memory representation of the DOM that is rendered when the state changes. The following function, when called during pageLoad , creates an empty

node. Copy this code into the JavaScript + No-Library (Pure JS) section.//transformer code function h(tag, props) {   let node   let children = []   for (i = arguments.length; i-- > 2; ) {     stack.push(arguments[i])   }   while (stack.length) {     if (Array.isArray((node = stack.pop()))) {       for (i = node.length; i--; ) {         stack.push(node[i])       }     } else if (node != null && node !== true && node !== false) {       children.push(typeof node === "number" ? (node = node + "") : node)     }   }   return typeof tag === "string"     ? {         tag: tag,         props: props || {},         children: children,         generatedId : id++       }     : tag(props, children) } Please note that for the JSX to call our h function, we would have left the following comment:/** @jsx h */

这由 JSX 解析器读取,并调用 h 函数。

app 函数包含各种子函数,这些子函数将在接下来的章节中解释。这些函数是使用我们已经学过的函数式编程概念构建的。每个函数接受一个输入,对其进行操作,并返回一个新的状态。转换器(即 h 函数)接收标签和属性。该函数由 JSX 解析器调用,通常是在解析 JSX 并将标签和属性作为参数发送时调用。如果我们仔细观察 h 函数,就会发现它使用了基本的函数式编程范例——递归。它以 JavaScript 数据类型递归构建 DOM 的树结构。

For example, calling h('buttons', props) where props is an object carrying other properties attached to the tag like onclick function , the function h would return a JSON equivalent as shown here.{ children:["Increase"] generatedId:1 props:{onclick: ƒ} tag:"button" }

创建操作

The createActions function creates an array of functions, one each for action. The actions object is passed in as a parameter as shown earlier. Notice the usage of Object.Keys, closures, and the map function here. Each object within the actions array is a function that can be identified by its name. Each such function has access to the parent’s variable scope (withActions), a closure. The closure when executed retains the values in the parent scope even though the function createAction has exited the execution context. The name of the function here in our example is up, down, and changeText.function createActions(actions,withActions){       Object.keys(withActions || {}).map(function(name){            return actions[name] = function(data) {                 data = withActions[name];                 update(data)            }       })     return actions   } Figure 11-5 is a sample of how the actions object looks during runtime.img/429083_2_En_11_Fig5_HTML.jpg Figure 11-5

运行时的操作对象

img/429083_2_En_11_Fig6_HTML.jpg Figure 11-6

下图显示了运行时子对象的状态

提出

The render function is responsible for replacing the old DOM with the new DOM.  function render() {     let doc = patch(node,(node = appView(appState,appActions)))     if(doc) {         let children = document.body.children;         for(let i = 0; i <= children.length; i++){             removeElement(document.body, children[i], children[i])       }       document.body.appendChild(doc);       }   }

修补

patch 函数负责在递归中创建 HTML 节点;例如,当 patch 接收虚拟 DOM 对象时,它递归地创建节点的 HTML 等价物。

function patch(node,newNode) {         if (typeof newNode === "string") {             let element = document.createTextNode(newNode)           } else {               let element = document.createElement(newNode.tag);               for (let i = 0; i < newNode.children.length; ) {                     element.appendChild(patch(node,newNode.children[i++]))                }                   for (let i in newNode.props) {                     element[i] = newNode.props[i]           }               element.setAttribute("id",newNode.props.id != undefined ? newNode.props.id : newNode.generatedId);          }     return element;       } }

更新

The update function is a higher order function responsible for updating the old state with a new state and rerendering the application. The update function is invoked when the user invokes an action like clicking any of the buttons shown in Figure 11-7.img/429083_2_En_11_Fig7_HTML.jpg Figure 11-7

下图显示了该示例的最终用户界面

更新函数接收一个函数作为参数;例如,up、down 或 changeText,这使它成为一个高阶函数。这给了我们向应用程序添加动态行为的好处。怎么做?更新函数直到运行时才知道状态参数,这使得应用程序的行为在运行时根据传递的参数来决定。如果通过了 up,则状态递增;如果向下传递,则递减。用更少的代码实现如此多的功能,这就是函数式编程的强大之处。

The current state of the application is passed on to your actions (example, up, down). Actions fundamentally follows the functional paradigm by returning a new state altogether. (Yes, HyperApp strictly follows the concepts of Redux, which in turn is fundamentally based on functional programming concepts.) This is done by the merge function. Once we get a new state, we will call the render function, as shown here.function update(withState) {       withState = withState(appState)       if(merge(appState,withState)){            appState = merge(appState,withState)            render();       }   }

合并

The merge function is a simple function that ensures the new state is merged with the old state.function merge(target, source) {     let result = {}     for (let i in target) { result[i] = target[i] }     for (let i in source) { result[i] = source[i] }     return result } As you can see, where the state is altered, a new state that contains the old state and the state that has changed is created and altered. For example, if you invoke the Increase action, the merge ensures only the count property is updated. If you look closely, the merge function very closely resembles what Object.assign does; that is, it creates a new state from any given state by not affecting the given states. Hence we can also rewrite the merge function as shown here.function merge(target, source) {     let result = {}     Object.assign(result, target, source)     return result }

这就是 ES8 语法的强大之处。

移动

The following functions are used to remove the children from the real DOM.// remove element function removeElement(parent, element, node) {     function done() {       parent.removeChild(removeChildren(element, node))     }     let cb = node.attributes && node.attributes.onremove     if (cb) {       cb(element, done)     } else {       done()     } } // remove children recursively function removeChildren(element, node) {     let attributes = node.attributes     if (attributes) {       for (let i = 0; i < node.children.length; i++) {         removeChildren(element.childNodes[i], node.children[i])       }     }     return element } The UI of the application looks like Figure 11-8. Increase, Decrease, and ChangeText are the actions, the number is 5, and Date is the state.img/429083_2_En_11_Fig8_HTML.jpg Figure 11-8

下图显示了该示例的最终用户界面

库的源代码可以在 checkout 分支的 hyperapp.js 下找到。您可以将它复制粘贴到一个新的 JS Fiddle 中来创建应用程序(记住要选择前面解释过的正确语言)。你也可以在jsfiddle.net/vishwanathsrikanth/akhbj9r8/70/从我的 JS 小提琴上拿叉子。

这样,我们就完成了第二个图书馆的建设。显然,我们的库比 1 KB 小得多,但它能够构建交互式 web 应用程序。我们构建的两个库都只基于函数。所有这些函数只作用于输入,而不是全局状态。函数使用类似高阶函数的概念,使系统更容易维护。我们看到每个函数如何按时接收输入,并只处理该输入,返回新的状态或函数。我们重用了许多高阶函数,比如 map、each、assign 等等。这显示了如何在我们的代码库中重用定义良好的函数。

此外,这两个代码都取自 Redux 和 HyperApp(当然有所调整),但是您可以看到只要遵循函数概念就可以构建出多么受欢迎的库。归根结底都是关于功能的!

尝试使用本书中解释的函数式 JavaScript 概念来构建更多这样的库。

摘要

在这一章中,我们学习了使用函数式 JavaScript 概念来构建一个库。我们已经了解了分布式状态如何随着时间的推移破坏应用程序的可维护性和可预测性,以及类似 Redux 的框架如何帮助我们集中状态。Redux 是一个状态容器,具有集中的只读状态;状态更改仅允许还原器通过传递操作和旧状态来进行。我们还使用函数式 JavaScript 概念构建了一个类似 Redux 的库和一个 HTML 应用程序。我们学习了虚拟 DOM 以及它如何帮助减少 DOM 操作,以及可用于在 JavaScript 文件中表示 DOM 的 JSX 语法。JSX 和虚拟 DOM 概念用于构建 HyperApp 这样的库,HyperApp 是可用于构建单页面应用程序的最薄的库。

十二、测试和最后的想法

所有的代码都是有罪的,直到被证明是无辜的。

—匿名

我们已经介绍了围绕函数式 JavaScript 的大部分概念。我们已经学习了 ES8 规范中的基础知识、先进理念和最新概念。我们的学习完成了吗?我们能断言我们已经写出了可行的代码吗?没有;除非代码经过测试,否则没有代码是完整的。

在这最后一章中,我们将学习为我们已经编写的功能性 JavaScript 代码编写测试。我们将学习使用业界最好的测试框架和编码模式来创作灵活、易于学习的自动化测试。本章讨论的模式和实践可以用来测试所有可能场景的任何功能代码。我们还将学习测试使用高级 JavaScript 的代码,比如 Promises 和异步方法。本章的剩余部分涉及使用各种工具来运行测试,报告测试状态,计算代码覆盖率,以及应用林挺来实施更好的编码标准。最后,我们总结了第二版的一些结论性想法。

注意

章节示例和库源代码在第十二章。回购的网址是github.com/antsmartian/functional-es8.git

一旦你检查出代码,请检查分支第十二章:

git checkout -b 第十二章来源/第十二章

以管理员身份打开命令提示符,导航到包含 package.json 的文件夹,然后运行

npm 安装

下载代码运行所需的包。

介绍

每个开发人员都应该知道,编写测试用例是证明代码运行并确保没有错误路径的唯一方法。测试有很多种——单元测试、集成测试、性能测试、安全/渗透测试等等——每一种都满足代码的某些标准。编写哪些测试完全取决于功能和功能的优先级:这完全取决于投资回报(ROI)。您的测试应该回答这些问题:这个功能对应用程序重要吗?如果我写这个测试,我能证明这个功能工作吗?应用程序的核心功能包含在前面提到的所有测试中,而很少使用的功能可能只需要单元和集成测试。宣扬单元测试并不是本节的主旨。相反,我们将学习在当前 DevOps 场景中创作自动化单元测试的重要性。

DevOps (Development + Operations) is a set of processes, people, and tools together used to define and ensure continuous frictionless delivery of software applications. Now where does testing fit into this model? The answer lies within continuous testing. Every high-performing Agile team with a DevOps delivery model should ensure they follow practices like continuous integration, testing, and delivery. In simple terms, every code check-in done by a developer is integrated into the one single repository, all the tests are run automatically, and the latest code is deployed automatically (provided the tests’ passing criteria are met) to a staging environment. Having a flexible, reliable, and fast delivery pipeline is the key to success for the most successful companies as shown in Table 12-1.Table 12-1

成功公司的交付渠道

|

组织

|

部署

| | --- | --- | | 脸谱网 | 每天 2 次部署 | | 亚马孙 | 每 11.6 秒部署一次 | | 网飞 | 每天 1000 次 |

资料来源:维基百科。

假设您是使用 Node 构建应用程序的敏捷团队的一员,您已经使用本书中解释的最佳实践编写了大量代码,现在您也有责任为您的代码编写测试,以便它达到可接受的代码覆盖率和通过标准。本章的目的是教你如何为 JavaScript 函数编写测试。

Figure 12-1 shows where the continuous testing phase sits in the overall application life cycle.img/429083_2_En_12_Fig1_HTML.jpg Figure 12-1

应用程序生命周期的持续测试阶段

测试类型

The following are the most important categories of tests.

  • 单元测试 : 编写单元测试是为了孤立地测试每一个功能。这将是本章的主要焦点。单元测试通过提供输入并确保输出符合预期来测试单个功能。单元测试模仿依赖行为。本章后面会有更多关于嘲讽的内容。

  • 集成测试 : 集成测试是为了测试端到端的功能而编写的。例如,对于一个用户注册场景,这个测试可能会在数据存储中创建一个用户,并确保它存在。

  • UI(功能测试) : UI 测试是针对 web 应用的;编写这些测试是为了控制浏览器和实现用户旅程。

其他类型的测试包括冒烟测试、回归测试、验收测试、系统测试、飞行前测试、渗透测试和性能测试。有各种框架可用于编写这些类别的测试,但是对这些测试类型的解释超出了本书的范围。本章只讨论单元测试。

BDD 和 TDD

在我们深入研究 JavaScript 测试框架之前,让我们简单介绍一下最著名的测试开发方法,行为驱动开发(BDD)和测试驱动开发(TDD)。

BDD suggests testing the behavior of the function instead of its implementation. For example, consider the following function that just increments a given number by 1.var mathLibrary = new MathLibrary(); var result = mathLibrary.increment(10) BDD advises the test to be written as shown next. Although this looks like a simple unit test, there is a subtle difference. Here we are not worried about the implementation logic (like the initial value of Sum).var expectedValue = mathlibrary.seed + 10; // imagine seed is a property of MathLibrary Assert.equal(result, expectedValue);

断言是帮助我们对照期望值验证实际值的函数,反之亦然。在这里,我们不担心实现细节;相反,我们断言该函数的行为,即将值递增 1。如果种子的值明天改变,我们不必更新函数。

注意

Assert 是大多数测试框架中术语的一部分。它主要用于以各种方式比较预期值和实际值。

TDD suggests you write the test first. For example, in the current scenario we write the following test first. Of course it would fail because there is no MathLibrary or its corresponding function called increment.Assert.equal(MathLibrary.increment(10), 11);

TDD 背后的思想是首先编写满足功能需求的断言,这些断言最初会失败。通过进行必要的修改(编写代码)来通过测试,开发就取得了进展。

JavaScript 测试框架

JavaScript being a vastly adapted language for writing functional code, there are numerous test frameworks available, including Mocha, Jest (by Facebook), Jasmine, and Cucumber, to name a few. The most famous among them are Mocha and Jasmine. To write a unit test for JavaScript functions we need the libraries or tools that can cover the following basic needs.

  • 测试结构,它定义了文件夹结构、文件名和相应的配置。

  • 断言函数,一个可以用来灵活断言的库。

  • Reporter,一个以控制台、HTML、JSON 或 XML 等各种格式显示结果的框架。

  • Mocks,一个可以提供测试替身来伪造依赖组件的框架。

  • 代码覆盖率,所以框架应该能够清楚地说出测试覆盖的行数或函数数。

不幸的是,没有一个测试框架提供所有这些功能。例如,Mocha 没有断言库。幸运的是,像 Mocha 和 Jasmine 这样的大多数框架都是可扩展的;我们可以使用 Babel 的断言库或带有 Mocha 的 expect.js 来执行干净的断言。在 Mocha 和 Jasmine 之间,我们将编写 Mocha 测试,因为我们觉得它比 Jasmine 更灵活。当然,在这一节的最后,我们还会看到 Jasmine 测试的一瞥。

注意

在撰写本文时,Jasmine 不支持对 ES8 特性的测试,这也是偏向 Mocha 的原因之一。

使用摩卡测试

以下部分解释了如何为创作测试设置 Mocha,以及用模拟创作同步和异步测试的本质。我们开始吧。

装置

mocha(mochajs.org)是一个社区支持的、功能丰富的 JavaScript 测试框架,可以在 Node.js 和浏览器上运行。Mocha 自诩让异步测试变得简单有趣,这一点我们一会儿就能见证。

Install Mocha globally and for the development environment as shown here.npm install –global mocha npm install –save-dev mocha Add a new folder called test and add a new file within the test folder called mocha-tests.js . The following is the updated file structure.| functional-playground |------play.js | lib |------es8-functional.js | test | -----mocha-tests.js

简单摩卡测试

Add the following simple Mocha test to mocha-tests.js .var assert = require('assert'); describe('Array', function () {     describe('#indexOf()', function () {         it('should return -1 when the value is not present', function () {             assert.equal(-1, [1, 2, 3].indexOf(4));         });     }); }); Let’s understand this bit by bit. The first line of code is required to import the Babel assertion library. As mentioned earlier, Mocha doesn’t have an out-of-the-box assertion library so this line is required. You can also use any other assertion library like expect.js, chai.js, should.js, or many more.var assert = require('assert'); Mocha tests are hierarchical in nature. The first describe function shown earlier describes the first test category 'Array'. Each primary category can have multiple describes, like '#indexOf'. Here '#indexOf' is a subcategory that contains the tests related to the indexOf function of the array. The actual test starts with the it keyword. The first parameter of the it function should always describe the expected behavior (Mocha uses BDD).it('should return -1 when the value is not present', function(){})

一个子类别中可以有多个 it 职能。以下代码用于断言预期值与实际值。在一个测试用例中也可以有多个断言(这里的 it 功能是一个测试用例)。默认情况下,在多次断言的情况下,测试在第一次失败时停止,但是这种行为是可以改变的。

The following code is added to package.json for running the Mocha tests. Also check the dev dependencies and dependencies section when you check out the branch to understand the support libraries that are pulled in."mocha": "mocha --compilers js:babel-core/register --require babel-polyfill", The switches –compilers and –require here are optional; in this case they are used to compile ES8 code. Running the following command runs the tests.npm run mocha Figure 12-2 shows a sample response.img/429083_2_En_12_Fig2_HTML.jpg Figure 12-2

开关响应示例

观察测试结果呈现的方式。数组是层次结构中的第一级,后面是#indexOf,然后是实际的测试结果。上面的语句 1 通过显示了测试的总结。

Currying、Monads 和 Functors 的测试

我们已经学习了很多函数式编程的概念,比如 currying、函子和单子。在这一节中,我们将学习为我们之前学过的概念编写测试。

Let’s start by authoring unit tests for currying, the process of converting a function with n number of arguments into a nested unary function. Well, that’s the formal definition, but it will probably not help us author unit tests. Authoring unit tests for any function is quite easy. The first step is to list its primary feature set. Here we are referring to the curryN function we wrote in Chapter 6. Let’s define its behavior

    CurryN 应该总是返回一个函数。

    CurryN 应该只接受函数,传递任何其他值都应该抛出错误。

    当使用相同数量的参数调用时,CurryN 函数应该返回与普通函数相同的值。

Now, let us start writing tests for these features.it("should return a function", function(){         let add = function(){}         assert.equal(typeof curryN(add), 'function'); }); This test will assert if curryN always returns a function object.it("should throw if a function is not provided", function(){         assert.throws(curryN, Error);     }); This test will ensure that curryN throws Error when a function is not passed.it("calling curried function and original function with same arguments should return the same value", function(){         let multiply = (x,y,z) => x * y * z;         let curriedMultiply = curryN(multiply);         assert.equal(curriedMultiply(1,2,3), multiply(1,2,3));         assert.equal(curriedMultiply(1)(2)(3), multiply(1,2,3));         assert.equal(curriedMultiply(1)(2,3), multiply(1,2,3));         curriedMultiply = curryN(multiply)(2);         assert.equal(curriedMultiply(1,3), multiply(1,2,3));     }); The preceding test can be used to test the basic functionality of a curried function. Now let’s write some tests for functors. Before that, like we did for currying, let’s review the features of a functor.

    函子是保存值的容器。

    函子是实现函数映射的普通对象。

    像 MayBe 这样的函子应该处理 null 或 undefined。

    像 MayBe 这样的仿函数应该链。

Now, based on how we defined the functor let’s see some tests.it("should store the value", function(){         let testValue = new Container(3);         assert.equal(testValue.value, 3);     }); This test asserts that a functor like container holds a value. Now, how do you test if the functor implements map? There are couple of ways: You can assert on the prototype or call the function and expect a correct value, as shown here.it("should implement map", function(){         let double = (x) => x + x;         assert.equal(typeof Container.of(3).map == 'function', true)         let testValue = Container.of(3).map(double).map(double);         assert.equal(testValue.value, 12);     }); The following tests assert if the function handles null and is capable of chaining.it("may be should handle null", function(){         let upperCase = (x) => x.toUpperCase();         let testValue = MayBe.of(null).map(upperCase);         assert.equal(testValue.value, null);     });     it("may be should chain", function(){         let upperCase = (x) => x.toUpperCase();         let testValue = MayBe.of("Chris").map(upperCase).map((x) => "Mr." + x);         assert.equal(testValue.value, "Mr.CHRIS");     }); Now, with this approach it should be easy to write tests for monads. Where do you start? Here is a little help: Let’s see if you can author tests for the following rules by yourself.

    单子应该实现 join。

    单子应该实现 chain。

    单子应该去掉嵌套。

如果你需要帮助,请查看 GitHub 网址的第十二章分支。

测试函数库

We have authored many functions in the es-functional.js library and used play.js to execute them. In this section we learn how to author tests for the functional JavaScript code we have written so far. Like play.js, before using the functions they should be imported in the file mocha-tests.js, so add the following line to the mocha-tests.js file.import { forEach, Sum } from "../lib/es8-functional.js"; The following code shows the Mocha tests written for JavaScript functions.describe('es8-functional', function () {     describe('Array', function () {         it('Foreach should double the elements of Array, when double function is passed', function () {             var array = [1, 2, 3];             const doublefn = (data) => data * 2;             forEach(array, doublefn);             assert.equal(array[0], 1)         });         it('Sum should sum up elements of array', function () {             var array = [1, 2, 3];             assert.equal(Sum(array), 6)         });         it('Sum should sum up elements of array including negative values', function () {             var array = [1, 2, 3, -1];             assert.notEqual(Sum(array), 6)         });     });

用 Mocha 进行异步测试

Surprise, surprise! Mocha also supports async and await, and it is suprisingly simple to test Promises or async functions as shown here.    describe('Promise/Async', function () {         it('Promise should return es8', async function (done) {             done();             var result = await fetchTextByPromise();             assert.equal(result, 'es8');         })     }); Notice the call to done here. Without the call to the done function, the test will time out because it does not wait for 2 s as required by our promise. The done function here notifies the Mocha framework. Run the tests again using the following command.npm run mocha The results are shown in Figure 12-3.img/429083_2_En_12_Fig3_HTML.jpg Figure 12-3

下图显示了测试结果

重申一下开头的陈述,Mocha 最初可能很难建立,因为它固有的灵活性坚持了这样一个事实,即它几乎可以与任何用于编写优秀单元测试的框架很好地结合在一起,但是最终,回报是丰厚的。

用西农嘲讽

假设你是团队 A 的一部分,团队 A 是一个大的敏捷团队的一部分,这个团队被划分成更小的团队,比如团队 A、团队 B 和团队 c。更大的敏捷团队通常被业务需求或地理区域所划分。假设团队 B 使用团队 C 的库,团队 A 使用团队 B 的函数库,每个团队都应该提交经过全面测试的代码。作为团队 A 的开发人员,在使用团队 B 的功能时,您会再次编写测试吗?不。那么当你依赖于调用团队 B 的函数时,你如何确保你的代码工作?这就是嘲讽图书馆的由来,Sinon 就是这样一个图书馆。如前所述,Mocha 没有开箱即用的嘲讽库,但它与 Sinon 无缝集成。

sinon(Sinonjs.org)是一个独立的框架,为 JavaScript 提供间谍、存根和模仿。Sinon 可以轻松地与任何测试框架集成。

注意

间谍、模拟或存根,虽然它们解决了类似的问题,听起来也相关,但有一些微妙的区别,理解起来很重要。我们建议更详细地了解假货、仿制品和存根之间的区别。本节仅提供摘要。

A fake imitates any JavaScript object like a function or object. Consider the following function.var testObject= {}; testObject.doSomethingTo10 = (func) => {     const x = 10;     return func(x); } This code takes a function and runs it on constant 10. The following code shows how to test this function using Sinon fakes.    it("doSomethingTo10", function () {         const fakeFunction = sinon.fake();         testObject.doSomethingTo10(fakeFunction);         assert.equal(fakeFunction.called, true);     });

如你所见,我们没有创建一个实际的函数来作用于 10;相反,我们伪造了一个函数。断言 fake 是很重要的,因此 assert . equal(fake function . called,true)语句确保调用 fake 函数,它断言函数 doSomethingTo10 的行为。Sinon 在测试函数的上下文中提供了更全面的方法来测试 fake 的行为。有关更多详细信息,请参见文档。

Consider this function:testObject.tenTimes = (x) => 10 * x; The following code shows a test case written using Sinon’s stub. As you notice, a stub can be used to define the behavior of the function. it("10 Times", function () {         const fakeFunction = sinon.stub(testObject, "tenTimes");         fakeFunction.withArgs(10).returns(10);         var result = testObject.tenTimes(10);         assert.equal(result, 10);         assert.notEqual(result, 0);     });

更常见的是,我们编写与外部依赖项交互的代码,如 HTTP 调用。如前所述,单元测试是轻量级的,应该模拟外部依赖,在本例中是 HTTP 调用。

Let’s say we have the following functions:var httpLibrary = {}; function httpGetAsync(url,callback) {       // HTTP Get Call to external dependency } httpLibrary.httpGetAsync = httpGetAsync; httpLibrary.getAsyncCaller = function (url, callback) {   try {       const response = httpLibrary.httpGetAsync(url, function (response) {           if (response.length > 0) {               for (let i = 0; i < response.length; i++) {                 httpLibrary.usernames += response[i].username + ",";               }               callback(httpLibrary.usernames)           }       });   } catch (error) {       throw error   } } If you would like to test only getAsyncCaller without getting into the nitty-gritty of httpGetAsync (let’s say it is developed by Team B), we can use Sinon mocks as shown here.    it("Mock HTTP Call", function () {         const getAsyncMock = sinon.mock(httpLibrary);         getAsyncMock.expects("httpGetAsync").once().returns(null);         httpLibrary.getAsyncCaller("", (usernames) => console.log(usernames));         getAsyncMock.verify();         getAsyncMock.restore();     }); This test case makes sure while testing getAsyncCaller , httpGetAsync is mocked. The following test case tests the same method without using mock.    it("HTTP Call", function () {      httpLibrary.getAsyncCaller("jsonplaceholder.typicode.com/users");     });

在结束编写函数式 JavaScript 代码的测试之前,让我展示一下如何使用 Jasmine 编写测试。

用 Jasmine 测试

Jasmine ( jasmine.github.io ) is also a famous testing framework; in fact, the APIs of Jasmine and Mocha are similar. Jasmine is the most widely used framework when building applications with AngularJS (or Angular). Unlike Mocha, Jasmine comes with a built-in assertion library. The only troublesome area with Jasmine at the point of writing was testing asynchronous code. Let’s learn to set up Jasmine in our code in the next few steps.npm install –save-dev jasmine If you intend to install it globally, run this command:npm install -g jasmine Jasmine dictates a test structure including a configuration file, so running the following command will set up the test’s structure../node_modules/.bin/jasmine init That command creates the following folder structure:|-Spec |-----Support |---------jasmine.json (Jasmine configuration file)

Jasmine.json 包含测试配置;例如,spec_dir 用于指定查找 Jasmine 测试的文件夹,而 spec_files 描述了用于识别测试文件的公共关键字。更多配置详情,请访问jasmine . github . io/2.3/node . html # section-Configuration

让我们在用 init 命令创建的 spec 文件夹中创建一个 Jasmine 测试文件,并将该文件命名为 jasmine-tests-spec.js。(记住,如果没有关键字 spec,Jasmine 将无法找到我们的测试文件。)

The following code shows a sample Jasmine test.import { forEach, Sum, fetchTextByPromise } from "../lib/es8-functional.js"; import 'babel-polyfill'; describe('Array', function () {     describe('#indexOf()', function () {         it('should return -1 when the value is not present', function () {             expect([1, 2, 3].indexOf(4)).toBe(-1);         });     }); }); describe('es8-functional', function () {     describe('Array', function () {         it('Foreach should double the elements of Array, when double function is passed', function () {             var array = [1, 2, 3];             const doublefn = (data) => data * 2;             forEach(array, doublefn);             expect(array[0]).toBe(1)         });  });

如您所见,除了断言之外,代码看起来非常类似于 Mocha 测试。您可以完全使用 Jasmine 来重建测试库,我们让您来决定如何去做。

The following command is added to package.json to execute Jasmine tests."jasmine": "jasmine" Running the following command executes the tests:npm run jasmine img/429083_2_En_12_Fig4_HTML.jpg Figure 12-4

下图显示了使用 Jasmine 的测试结果

代码覆盖率

我们有多确定测试已经覆盖了关键领域?对于任何语言来说,代码覆盖率是唯一能够解释测试所覆盖的代码的度量。JavaScript 也不例外,因为我们可以获得测试覆盖的代码行数或百分比。

Istanbul ( gotwarlost.github.io/istanbul/ ) is one of the best known frameworks that can calculate the code coverage for JavaScript at the statement, Git branch, or function level. Setting up Istanbul is easy. nyc is the name of the command-line argument that can be used to get code coverage, so let us run this command to install nyc:npm install -g --save-dev nyc The following command can be used to run Mocha tests with code coverage, so let us add it to package.json."mocha-cc": "nyc mocha --compilers js:babel-core/register --require babel-polyfill" Run the following command to run the Mocha tests and also get the code coverage.npm run mocha-cc The results are shown in Figure 12-5.img/429083_2_En_12_Fig5_HTML.jpg Figure 12-5

下图显示了使用 Mocha 编写的测试的代码覆盖率

正如你所看到的,除了文件 es8-functional.js 中的第 20 行和第 57 行,我们覆盖了 93%。代码覆盖率的理想百分比取决于几个因素,所有因素都考虑到了投资回报。通常 85%是一个推荐的数字,但是如果代码被任何其他测试覆盖,那么低于这个数字也是可行的。

林挺

代码分析和代码覆盖率一样重要,尤其是在大型团队中。代码分析帮助您实施统一的编码规则,遵循最佳实践,并为可读性和可维护性实施最佳实践。到目前为止,我们编写的 JavaScript 代码可能不符合最佳实践,因为这更适用于生产代码。在这一节中,让我们看看如何将编码规则应用于功能性 JavaScript 代码。

ESLint ( eslint.org/ ) is a command-line tool for identifying incorrect coding patterns in ECMAScript/JavaScript. It is relatively easy to install ESLint into any new or existing project. The following command installs ESLint.npm install --save-dev -g eslint ESLint is configuration driven, and the command that follows creates a default configuration. You might have to answer a few questions as shown in Figure 12-6 here. For this coding sample we are using coding rules recommended by Google.eslint --init img/429083_2_En_12_Fig6_HTML.jpg Figure 12-6

下图显示了 eslint 初始化步骤

Here is the sample configuration file.{     "parserOptions": {         "ecmaVersion": 6,         "sourceType": "module"     },     "rules": {         "semi": ["error", "always"],         "quotes": ["error", "double"]     },     "env": {         "node": true     } } Let's look at the first rule. "semi": ["error", "always"], This rule says a semicolon is mandatory after every statement. Now if we run it against the code file es-functional.js we have written so far, we get the results shown in Figure 12-7. As you can see, we violated this rule in many places. Imposing coding rules or guidelines should be done at the very beginning of the project. Introducing coding rules or adding new rules after accumulating a huge code base results in an enormous amount of code debt, which will be difficult to handle.img/429083_2_En_12_Fig7_HTML.jpg Figure 12-7

下图显示了 eslint 工具的结果

ESLint helps you fix these errors. As suggested earlier, you just have to run this command:eslint lib\es8-functional.js  --fix

所有错误都消失了!你可能并不总是幸运的,所以确保你在开发阶段的早期施加限制。

单元测试库代码

在前一章中,我们学习了如何创建有助于构建应用程序的库。一个好的库是可测试的,所以测试的代码覆盖率越高,消费者信任你的代码的可能性就越大。当您更改某些内容时,测试有助于快速检查代码中受影响的区域。在这一节中,我们为我们在前一章中编写的 Redux 库代码编写 Mocha 测试。

The following code is available in the mocha-test.js file. The mocha-test.js file refers to the code from our Redux library. The following test ensures that initially the state is always empty.it('is empty initially', () => {         assert.equal(store.getState().counter, 0);     }); One of the main functions in our library was to assert if actions can influence state change. In the following state we initiate state change by calling IncrementCounter , which is called when a click event is raised. IncrementCounter should increase the state by 1.// test for state change once     it('state change once', () => {         global.document = null;         incrementCounter();         assert.equal(store.getState().counter, 1);     }); // test for state change twice     it('state change twice', () => {         global.document = null;         incrementCounter();         assert.equal(store.getState().counter, 2);     }); The last function we are going to assert is to check if there is at least one listener registered for state change. To ensure we have a listener we also register a listener; this is also called an Arrange phase .// test for listener count     it('minimum 1 listener', () => {         //Arrange         global.document = null;         store.subscribe(function () {             console.log(store.getState());         });         //Act         var hasMinOnelistener = store.currentListeners.length > 1;         //Assert         assert.equal(hasMinOnelistener, true);     }); You can run npm run mocha or npm run mocha-cc to execute the tests with code coverage. You will notice in Figure 12-8 that we have covered more than 80% of the code we have written in the library.img/429083_2_En_12_Fig8_HTML.jpg Figure 12-8

下图显示了代码覆盖率的结果

有了这个经验,为我们在前一章中构建的类似 HyperApp 的库编写单元测试将是一个很好的练习。

结束语

Another wonderful journey comes to an end. We hope you had fun like we did learning new concepts and patterns in JavaScript functional programming. Here are some closing thoughts.

  • 如果你刚开始一个项目,试着使用本书中的概念。本书中使用的每个概念都有特定的使用范围。在浏览一个用户场景时,分析你是否可以使用任何解释过的概念。例如,如果您正在进行 REST API 调用,您将分析是否可以创建一个库来异步执行 REST API 调用。

  • 如果您正在处理一个现有的项目,该项目包含大量杂乱的 JavaScript 代码,那么分析这些代码,将其中的一些代码重构为可重用的、可测试的功能。最好的学习方法是通过实践,所以扫描你的代码,找到松散的部分,把它们缝合在一起,形成一个可扩展的、可测试的、可重用的 JavaScript 函数。

  • 请继续关注 ECMAScript 更新,因为随着时间的推移,ECMAScript 将继续成熟并变得更好。你可以在github.com/tc39/proposals上关注这些提议,或者如果你有一个新的想法或者想法可以改进 ECMAScript 或者帮助开发者,你可以继续你的提议。

摘要

在这一章中,我们学习了测试的重要性,测试的类型,以及像 BDD 和 TDD 这样的开发模型。我们开始理解 JavaScript 测试框架的需求,并了解了最著名的测试框架 Mocha 和 Jasmine。我们使用 Mocha 编写了简单测试、函数库测试和异步测试。Sinon 是一个 JavaScript 模仿库,它为 JavaScript 提供了间谍、存根和模仿。我们学习了如何将 Sinon 与 Mocha 相结合来模仿依赖行为或对象。我们还学习了使用 Jasmine 为 JavaScript 函数编写测试。伊斯坦布尔与 Mocha 集成得很好,并提供了我们的代码覆盖率,可以作为可靠性的衡量标准。林挺帮助我们编写干净的 JavaScript 代码,在本章中,我们学习了使用 ESLint 定义编码规则。