介绍
有很多关于异步编程的讨论,但究竟有什么大不了的呢?最重要的是我们希望我们的代码是非阻塞的。
可以阻止我们的应用程序的任务包括发出 HTTP 请求、查询数据库或打开文件。一些语言,如 java,通过创建多个线程来处理这个问题。然而,javascript只有一个线程,所以我们需要设计我们的程序,以便没有任务阻塞 流。
异步编程解决了这个问题。它允许我们稍后执行任务,这样我们就不会等待整个程序等待任务完成。当我们想要确保任务按顺序执行时,它也很有帮助。
在本教程的第一部分,我们将学习同步和异步代码背后的概念,并了解如何使用回调函数来解决异步问题。
内容
线程
同步与异步
回调函数
概括
资源
线程
我想让你记住你上次去杂货店购物是什么时候。可能有多个收银机打开以结帐客户。这有助于商店在相同的时间内处理更多的交易。这是并发的一个例子。
简单来说,并发就是同时做不止一项任务。您的操作系统是并发的,因为它同时运行多个进程。进程是执行环境或正在运行的应用程序的实例。例如,您的浏览器、文本编辑器和防病毒软件都是计算机上同时运行的进程。
应用程序也可以是并发的。这是通过线程实现的。我不会太深入,因为这超出了本文的范围。如果您想深入了解 JavaScript 如何在底层工作,我建议您观看此视频。
线程是执行代码的进程中的一个单元。在我们的商店示例中,每个结帐行都是一个线程。如果我们店里只有一条结账线,那将改变我们处理客户的方式。
您是否曾经排队并且有什么事情阻碍了您的交易?也许您需要检查价格,或者必须见经理。当我在邮局试图寄送包裹时,我没有填写我的标签,收银员让我靠在一边,同时他们继续检查其他客户。当我准备好时,我回到队伍的最前面去检查。
这类似于异步编程的工作方式。收银员本来可以等我的。有时他们会这样做。但最好不要排队并检查其他客户。关键是客户不必按照排队的顺序结账。同样,代码不必按照我们编写它的顺序执行。
同步与异步
很自然地想到我们的代码从上到下顺序执行。这是同步的。然而,在 JavaScript 中,一些任务本质上是异步的(例如 setTimeout),而一些我们设计为异步的任务是因为我们提前知道它们可能会阻塞。
让我们看一个在 node.js 中使用文件的实际示例。如果您想试用代码示例并需要了解如何使用 Node.js,您可以从本教程中找到入门说明。在此示例中,我们将打开一个帖子文件并检索其中一个帖子。然后我们将打开一个评论文件并检索该帖子的评论。
这是同步方式:
index.js
const fs = require('fs'); const path = require('path'); const postsUrl = path.join(__dirname, 'db/posts.json'); const commentsUrl = path.join(__dirname, 'db/comments.json'); //return the data from our file function loadCollection(url) { try { const response = fs.readFileSync(url, 'utf8'); return JSON.parse(response); } catch (error) { console.log(error); } } //return an object by id function getRecord(collection, id) { return collection.find(function(element){ return element.id == id; }); } //return an array of comments for a post function getCommentsByPost(comments, postId) { return comments.filter(function(comment){ return comment.postId == postId; }); } //initialization code const posts = loadCollection(postsUrl); const post = getRecord(posts, "001"); const comments = loadCollection(commentsUrl); const postComments = getCommentsByPost(comments, post.id); console.log(post); console.log(postComments);
数据库/posts.json
[ { "id": "001", "title": "Greeting", "text": "Hello World", "author": "Jane Doe" }, { "id": "002", "title": "JavaScript 101", "text": "The fundamentals of programming.", "author": "Alberta Williams" }, { "id": "003", "title": "Async Programming", "text": "callbacks, promises and Async/Await.", "author": "Alberta Williams" } ]
数据库/comments.json
[ { "id": "phx732", "postId": "003", "text": "I don't get this callback stuff." }, { "id": "avj9438", "postId": "003", "text": "This is really useful info." }, { "id": "gnk368", "postId": "001", "text": "This is a test comment." } ]
该readFileSync
方法同步打开文件。因此,我们也可以以同步方式编写初始化代码。但这不是打开文件的最佳方式,因为这是一个潜在的阻塞任务。打开文件应该异步完成,以便执行流程可以是连续的。
Node 有一个readFile
方法可以用来异步打开文件。这是语法:
fs.readFile(url, 'utf8', function(error, data) { ... });
我们可能很想在这个回调函数中返回我们的数据,但我们无法在loadCollection
函数中使用它。我们的初始化代码也需要更改,因为我们没有正确的值来分配给我们的变量。
为了说明这个问题,让我们看一个更简单的例子。你认为下面的代码会打印什么?
function task1() { setTimeout(function() { console.log('first'); }, 0); } function task2() { console.log('second'); } function task3() { console.log('third'); } task1(); task2(); task3();
此示例将打印“second”、“third”,然后是“first”。该setTimeout
函数的延迟为 0 并不重要。它是 JavaScript 中的异步任务,因此它总是会延迟到以后执行。该firstTask
函数可以表示任何异步任务,例如打开文件或查询我们的数据库。
让我们的任务按照我们想要的顺序执行的一种解决方案是使用回调函数。
回调函数
要使用回调函数,您将一个函数作为参数传递给另一个函数,然后在任务完成时调用该函数。如果您需要有关如何使用高阶函数的入门知识,reactivex 有一个交互式教程,您可以尝试一下。
回调让我们强制任务按顺序执行。当我们的任务依赖于先前任务的结果时,它们也会帮助我们。使用回调,我们可以修复最后一个示例,使其打印“first”,“second”,然后是“third”。
function first(cb) { setTimeout(function() { return cb('first'); }, 0); } function second(cb) { return cb('second'); } function third(cb) { return cb('third'); } first(function(result1) { console.log(result1); second(function(result2) { console.log(result2); third(function(result3) { console.log(result3); }); }); });
我们不是在每个函数中打印字符串,而是在回调中返回值。当我们的代码被执行时,我们打印传递给我们回调的值。这就是我们的readFile
函数使用的。
回到我们的文件示例,我们可以更改我们的loadCollection
函数,以便它使用回调以异步方式读取文件。
function loadCollection(url, callback) { fs.readFile(url, 'utf8', function(error, data) { if (error) { console.log(error); } else { return callback(JSON.parse(data)); } }); }
这就是我们的初始化代码使用回调的样子:
loadCollection(postsUrl, function(posts){ loadCollection(commentsUrl, function(comments){ getRecord(posts, "001", function(post){ const postComments = getCommentsByPost(comments, post.id); console.log(post); console.log(postComments); }); }); });
在我们的函数中要注意的一件事loadCollection
是,我们不是使用try/catch
语句来处理错误,而是使用if/else
语句。catch 块将无法捕获从readFile
回调返回的错误。
在我们的代码中包含错误处理程序是一种很好的做法,这些错误是由外部影响而不是编程错误导致的错误。这包括访问文件、连接到数据库或发出 HTTP 请求。
在修改后的代码示例中,我没有包含任何错误处理。如果任何步骤发生错误,程序将无法继续。提供有意义的说明会很好。
错误处理很重要的一个例子是如果我们有一个任务来登录用户。这涉及从表单中获取用户名和密码,查询我们的数据库以查看它是否是有效的组合,然后如果我们成功则将用户重定向到他们的仪表板。如果用户名和密码无效,如果我们不告诉它该怎么做,我们的应用程序就会停止运行。
更好的用户体验是向用户返回错误消息并允许他们重试登录。在我们的文件示例中,我们可以在回调函数中传递一个错误对象。函数就是这种情况readFile
。然后,当我们执行代码时,我们可以添加一条if/else
语句来处理成功的结果和拒绝的结果。
任务
使用回调方法,编写一个程序,该程序将打开一个用户文件,选择一个用户,然后打开一个帖子文件并打印用户的信息和他们的所有帖子。
概括
异步编程是我们代码中使用的一种方法,用于将event推迟到以后执行。当您处理异步任务时,回调是为我们的任务计时的一种解决方案,以便它们按顺序执行。
如果我们有多个任务依赖于先前任务的结果,一种解决方案是使用多个嵌套回调。然而,这可能会导致一个被称为“回调地狱”的问题。Promise 解决了回调地狱的问题,而异步函数让我们以同步的方式编写代码。在本教程的第 2 部分中,我们将了解它们是什么以及如何在我们的代码中使用它们。
- 内容
- index.js
- 数据库/posts.json
- 数据库/comments.json
- 任务