我们知道 JS 语言的执行环境是"单线程",所谓"单线程",就是指一次只能完成一件任务,这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。为了解决这个问题,JS 语言将任务的执行模式分成两种:同步 (Synchronous)和异步 (Asynchronous)

下面就来讲一讲异步为什么很重要?如何使用异步来有效处理潜在的阻塞操作?

为什么需要异步?

通常来说,程序都是顺序执行,同一时刻只会发生一件事。如果一个函数依赖于另一个函数的结果,它只能等待那个函数结束才能继续执行,从用户的角度来说,整个程序才算运行完毕.

你可能知道,Javascript语言的执行环境是"单线程"(single thread)。

所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

比如

Mac 用户可能会经历过这种旋转的彩虹光标(常称为沙滩球),操作系统通过这个光标告诉用户:“现在运行的程序正在等待其他的某一件事情完成,才能继续运行,都这么长的时间了,你一定在担心到底发生了什么事情”。


这是令人沮丧的体验,没有充分利用计算机的计算能力 — 尤其是在计算机普遍都有多核CPU的时代,坐在那里等待毫无意义,你完全可以在另一个处理器内核上干其他的工作,同时计算机完成耗时任务的时候通知你。这样你可以同时完成其他工作,这就是异步编程的出发点。你正在使用的编程环境(就web开发而言,编程环境就是web浏览器)负责为你提供异步运行此类任务的API。

1. 阻塞

异步技术非常有用,特别是在web编程。当浏览器里面的一个web应用进行密集运算还没有把控制权返回给浏览器的时候,整个浏览器就像冻僵了一样,这叫做 阻塞;这时候浏览器无法继续处理用户的输入并执行其他任务,直到web应用交回处理器的控制。

我们来看一些 阻塞 的例子。

例子: simple-sync.html

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Simple synchronous JavaScript example</title>
</head>
<body><button>Click me</button><script>const btn = document.querySelector('button');btn.addEventListener('click', () => {let start = new Date();let end;for (let i = 0; i < 10000000; i++) {let date = new Date();end = date;}let time = end - start;console.log('计算1千万个日期总耗时:' + time + 'ms');let pElem = document.createElement('p');pElem.textContent = '计算1千万个日期总耗时:' + time + 'ms';document.body.appendChild(pElem);});</script>
</body>
</html>

在按钮上添加了一个事件监听器,当按钮被点击,它就开始运行一个非常耗时的任务(计算1千万个日期,并在console里显示最终的耗时),然后在DOM里面添加一个段落。

运行这个例子的时候,打开JavaScript console,然后点击按钮 — 你会注意到,直到日期的运算结束,最终的耗时在console上显示出来,段落才会出现在网页上。

效果如下:

代码按照源代码的顺序执行,只有前面的代码结束运行,后面的代码才会执行。

Note: 这个例子不现实:在实际情况中一般不会发生,没有谁会计算1千万次日期,它仅仅提供一个非常直观的体验.

2. 同步

要理解什么是 异步 JavaScript ,我们应该从确切理解 同步 JavaScript 开始。

我们学的很多知识基本上都是同步的:运行代码,然后浏览器尽快返回结果。先看一个简单的例子

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Simple synchronous JavaScript example</title></head><body><button>Click me</button><script>const btn = document.querySelector('button');btn.addEventListener('click', () => {alert('You clicked me!');let pElem = document.createElement('p');pElem.textContent = 'This is a newly-added paragraph.';document.body.appendChild(pElem);});</script></body>
</html>

效果如下:

这段代码, 一行一行的顺序执行:

  1. 先取得一个在DOM里面的 <button> 引用。

  2. 点击按钮的时候,添加一个 click 事件监听器:

    1. alert() 消息出现。
    2. 一旦alert 结束,创建一个<p> 元素。
    3. 给它的文本内容赋值。
    4. 最后,把这个段落放进网页。

每一个操作在执行的时候,其他任何事情都没有发生 — 网页的渲染暂停. 因为前篇文章提到过 JavaScript 是单线程. 任何时候只能做一件事情, 只有一个主线程,其他的事情都阻塞了,直到前面的操作完成。

所以上面的例子,点击了按钮以后,段落不会创建,直到在alert消息框中点击ok,段落才会出现,你可以自己试试

Note: 请记住,这个很重要,alert()在演示阻塞效果的时候非常有用,但是在正式代码里面,它就是一个噩梦。

3. 解决

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)

  • "同步模式"就是前面讲到的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

  • "异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。

异步编程的几种方法

1. 回调函数

回调函数是异步编程最基本的方法。

回调函数的概念:

A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.

译过来就是:

回调函数是作为参数传递给另一个函数并在其父函数完成后执行的函数。

下面是一个回调函数的例子:

function doSomething(msg, callback){alert(msg);if(typeof callback == "function") callback();}
doSomething("回调函数", function(){alert("匿名函数实现回调!");});

我们再来看几个经典的回调函数代码,我保证你一定用过他们:

◾ 1. 异步请求的回调函数:

$.get("/try/ajax/demo_test.php",function(data,status){alert("数据: " + data + "\n状态: " + status);
});

◾ 2. 数组遍历的回调函数

array1.forEach(element => console.log(element));

等等

回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。

回调函数 最致命的缺点,就是容易写出 回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:

ajax(url, () => {// 处理逻辑ajax(url1, () => {// 处理逻辑ajax(url2, () => {// 处理逻辑})})
})

2. 事件监听

另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

事件监听的回调函数:

element.addEventListener("click", function(){ alert("Hello World!");
});

上面这行代码的意思是,当 element 发生click事件,就执行传入的 function。

这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

3. 发布/订阅

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。

  • 订阅者(Subscriber)把自己想订阅的事件 注册(Subscribe)到调度中心(Event Channel);
  • 发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由 调度中心 统一调度(Fire Event)订阅者注册到调度中心的处理代码。

◾ 例子

比如我们很喜欢看某个公众号号的文章,但是我们不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。

上面一个看似简单的操作,其实是一个典型的发布订阅模式,公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。

◾ 发布/订阅模式的优点是对象之间解耦,异步编程中,可以更松耦合的代码编写;缺点是创建订阅者本身要消耗一定的时间和内存,虽然可以弱化对象之间的联系,多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护。

想要手写实现发布/订阅模式的童鞋可以看我发的这篇文章:从零开始带你手写一个“发布-订阅者模式“ ,保姆级教学

4. Promise

Promise 是一种处理异步代码(而不会陷入回调地狱)的方式。

多年来,promise 已成为语言的一部分(在 ES2015 中进行了标准化和引入),并且最近变得更加集成,在 ES2017 中具有了 async 和 await。

异步函数 在底层使用了 promise,因此了解 promise 的工作方式是了解 asyncawait 的基础。

Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)

一个 Promise 必然处于以下几种状态之一:

  • 待定 (pending): 初始状态,既没有被兑现,也没有被拒绝。
  • 已成功 (fulfilled): 意味着操作成功完成。
  • 已拒绝 (rejected): 意味着操作失败。

当 promise 被调用后,它会以处理中状态 (pending) 开始。 这意味着调用的函数会继续执行,而 promise 仍处于处理中直到解决为止,从而为调用的函数提供所请求的任何数据。

被创建的 promise 最终会以被解决状态 (fulfilled)被拒绝状态 (rejected) 结束,并在完成时调用相应的回调函数(传给 thencatch)。

● Promise 的链式调用

Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

getJSON("/posts.json").then(function(json) {return json.post;
}).then(function(post) {// ...
});

上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

getJSON("/post/1.json").then(function(post) {return getJSON(post.commentURL);
}).then(function (comments) {console.log("resolved: ", comments);
}, function (err){console.log("rejected: ", err);
});

上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,就调用第一个回调函数,如果状态变为rejected,就调用第二个回调函数。

如果采用箭头函数,上面的代码可以写得更简洁。

getJSON("/post/1.json").then(post => getJSON(post.commentURL)
).then(comments => console.log("resolved: ", comments),err => console.log("rejected: ", err)
);

如果想要更详细的学习 Promise ,可以参考我发的这几篇文章:

  • 通俗易懂的Promise知识点总结,检验一下你是否真的完全掌握了promise?
  • 手把手一行一行代码教你“手写Promise“,完美通过 Promises/A+ 官方872个测试用例
  • 看了就会,手写 Promise 全部 API 教程,包括处于 TC39 第四阶段草案的 Promise.any()

5. async/await

asyncawait 关键字是最近添加到JavaScript语言里面的。它们是ECMAScript 2017 的一部分,简单来说,它们是基于promises的语法糖,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码,因此它们非常值得学习。

如果想要更详细的学习 async/await ,可以参考我发的这篇文章:

  • JS 异步编程终极解决方案 async/await 的使用手册

❤️ 结尾

如果这篇文章 对你的学习 有所 帮助,欢迎 点赞

JS 异步编程的 5 种解决方案相关推荐

  1. js异步编程的三种模式

    写在前面 javascript语言的执行环境是"单线程"(single thread),就是指一次只能完成一件任务.如果有多个任务,就必须排队,等前面一个任务完成,再执行后面一个任 ...

  2. JS异步编程的解决方案

    js解决异步编程有6种方案: 1.1 回调函数 异步编程的最基本方法,把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数. 优点:简单.容易理解和实现. 缺点:多次调用 ...

  3. JS 异步编程方法:6种方案

    前言: javascript语言的执行环境是"单线程".也就是指一次只能完成一件任务.如果有多个任务,就必须排队,前面一个任务完成,在执行后面一个任务 这种模式虽然实现起来比较简单 ...

  4. js 异步执行_JS Asynchronous — JS 异步编程极简史

    Asynchronous JS 异步编程极简史,这个故事网上已经很多人有了自己的讲述. Event Loop 解释了 Node.js 为何以及如何实现单线程服务模型和 Event Loop.对于 JS ...

  5. JS 异步编程六种方案

    前言 我们知道Javascript语言的执行环境是"单线程".也就是指一次只能完成一件任务.如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务. 这种模式虽然实现起来 ...

  6. JS 异步编程都有哪些方案?

    什么是同步? 所谓的同步就是在执行某段代码时,在该代码没有得到返回结果之前,其他代码暂时是无法执行的,但是一旦执行完成拿到返回值之后,就可以执行其他代码了.换句话说,在此段代码执行完未返回结果之前,会 ...

  7. 【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(二、JavaScript 异步编程)

    [学习笔记]Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程.手写 Promise(课前准备) [学习笔记]Part1·JavaScript·深度剖析-函数式编程与 JS 异步 ...

  8. JS 异步编程都有哪些方案

    JS 异步编程都有哪些方案   先一起来回想一下,我们在日常开发中都用过哪些 JS异步编程的方式?总结起来无外乎有这几种:回调函数.事件监听.Promise.Generator.async/await ...

  9. 57 Node.js异步编程

    技术交流QQ群:1027579432,欢迎你的加入! 欢迎关注我的微信公众号:CurryCoder的程序人生 1.Node.js异步编程 1.1 Node.js中的异步API 如果异步API后面的代码 ...

最新文章

  1. 无意中发现一位大佬的算法刷题pdf笔记
  2. LiveVideoStack线上分享第三季(二):对话机器人与儿童电子消费品
  3. 方法重载,new,override
  4. 通俗理解TCP握手次数是三次?
  5. 怎么查看笔记本内存条型号_内存条,手把手教你怎么查看内存条的型号
  6. 青少年CTF wp合集
  7. 入侵WIN2003 PHP服务器的另类技术
  8. vue实现九宫格抽奖
  9. 几本关于斯多葛主义 (Stoicism) 的书
  10. Android神兵利器之黄油刀的使用(ButterKnife)
  11. 2004年下半年 网络工程师 上下午试卷【附带答案】
  12. 小步快跑,快速迭代:安全运营的器术法道
  13. html5 xdwlnjs cn,最近需要调用一个网站的js,但是发现是加密的,有大佬来解密下吗?...
  14. Web前端知识CSS(清浮动的方法、CSS精灵图、滑动门)
  15. U盘插电脑有提示音但不显示盘符怎么办?
  16. 深度学习实例第二部分:OpenCV
  17. MUM System
  18. go build 无文件_用一句话描述Go语言的命令
  19. xampp修改mysql密码_XAMPP环境下mysql的root用户密码修改方法_MySQL
  20. 三千烦恼,不如淡然一笑

热门文章

  1. 年轻人如何创业?有什么好的建议?
  2. 元宇宙是什么?元宇宙真的会改变我们的生活吗?
  3. 超完整 Python基础入门知识教程
  4. 高低温霍尔效应测试系统
  5. Subquery returns more than 1 row
  6. PKUWC2019 赛前模拟赛总结
  7. 4项技术将会帮助人类拯救海洋
  8. 传输层安全加密协议之Noise 协议(Noise Protocol Framework)
  9. 东华大学 oj92——字符串统计
  10. c语言输出数以空格格开,C语言输入输出函数格式详解.docx