导读:对于接触JavaScript这门编程语言没有多久的本菜鸡而言,在相当长的一段时间内,我都完全无法理解这门语言中的异步编程,不明白什么叫异步编程以及为什么需要异步编程。为什么顺序执行程序就不行了呢?非要使用异步回调的方式来去做?经过一段时间的学习和探究,我算是初步了解了其中的道理和内涵。如果你像我一样也是一个JavaScript的小白,希望你看完我写的这篇文章之后,也可以解答你内心的很多困惑,并对JavaScript这门编程语言可以有一个更为深入的了解。

从单线程语言讲起

很长一段时间,我对JavaScript语言的困惑来自于我不清楚什么是**单线程编程语言**,而这个特性对于JavaScript走向异步编程的方式至关重要。在我们开始去讲单线程编程语言之前,有必须先去了解什么是进程,什么是线程。对这两个概念如果用纯文字讲解略显苍白,这里推荐一个B站Up主对线程和进程讲解的视频。我就是看完这个视频之后才明白进程和线程之间的关系的,相信你看完之后也可以理解。

有了之前的概念基础,下面就来谈谈什么是单线程编程语言。单线程顾名思义就是一个进程里面只会有一个线程。这一个线程有的时候也会被称之为主线程。如果一个进程里面只能有一个线程的话,那它就只能串行的执行程序,就是只能执行完A任务,然后再去执行B任务。反之,如果一个进程里面有多个线程的话,那么它看起来好像是可以并行的执行多个任务。对于单核CPU来说,系统会在多个线程之间快速不停地切换,就可以给你一种错觉好像是多个线程在并行的执行,但这只是一种错觉而已。真正的并行应该是多核才能实现。关于**串行、并行和并发**这几个概念,引用知乎某大佬一个形象的回答:

你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是『同时』。

多线程就类似于并发这样的一个方式。只有多核才能做到真正的并行。

那么为什么JavaScript会采用这种单线程编程语言的方式呢?可以设想一下,如果JavaScript是一门支持多线程编程语言(比如Java),而多线程实际上是CPU在多个线程之间来回快速切换而已,当JavaScript运行在浏览器中,这时一个线程说要删除一个图片,另外一个线程说要添加一个图片,如果有上百个线程都在争抢CPU的运行时间的话,你的浏览器页面就会有很大的不确定性了,我们并不希望自己的页面加载、更新有很大不确定性。而如果一个进程只有一个线程的话,就不会有上述问题,任务一个接一个顺序执行就可以了。

单线程语言的问题

单线程的确可以不用考虑烦人的哪个任务先执行,哪个任务后执行的问题了,也不需要考虑多线程之间不同线程数据同步的问题。但是线程有他自身的问题。因为单线程是按顺序执行,如果一个任务执行的很慢的话,后面的任务全部都会被阻塞,执行不了。

想象你去超市买东西,如果前面有一个人结账很慢,那你也只能干等着,别的什么事也做不了。对于任务也是同样的道理,对于浏览器而言。我们通常需要各种各样的网络请求,去获取图片,视频等网络资源。如果一个网络请求很慢,比如某一个图片加载很慢,那后面的所有的数据都只能等着前面图片加载完之后才能再去访问网络加载。但我们实际用浏览器访问网页体验好像并不是这样。比如有的时候可能某一个图片加载很长时间也没有加载出来,但这并没有影响后面的图片或者视频等资源的加载。我们是如何做到这一点的呢?答案是使用异步编程(终于讲到异步编程了^ -- ^)

想不清楚是因为忘了考虑宿主环境!

我们首先引用一下lynnelv关于同步和异步的定义。

老实说,看完之后我其实是挺疑惑的!JavaScript是一个单线程语言,你比如说你有一个调用栈,就像在超市排队结账一样,按顺序执行程序,这很好理解。但怎么又出现了一个任务队列!单线程还能一边搞调用栈还能一边去做任务队列吗?!,举一个例子:

function updateAsync() {var i = 0;function updateLater() {document.getElementById('output').innerHTML = (i++);if (i < 1000) {setTimeout(updateLater, 0);}}updateLater();
}

比如上面代码展示的setTimeout是一个异步执行程序(书上就是这么说的),但是!单线程怎么可能会做到异步呢?假设你去排队结账,而且因为单线程,所以只能有一个结账通道,那么前面人结账再久,你也只能等着。你不能换一个通道去结账。但这个任务队列就是告诉你,我们还有一个结账通道。比如这个`setTimeout`是异步函数,他的延时计时,不会影响你的主线程执行。可是如果你只有一个线程,要么去执行程序,要么去计时,怎么可能一边计时,一边还执行程序呢?

后来我才明白,原来是这么回事!除了JavaScript本身之外,还有一个宿主环境。

JavaScript本身是不能自己单独执行的,要么在浏览器中,要么是在Node.js中,而上述两个就是宿主环境。JavaScript本身是单线程编程语言这没错,但JavaScript的宿主环境给他提供了额外的并发功能。(也就是上面说的消息队列)

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

上图和对应的文字内容引用自[JavaScript 运行机制详解:再谈Event Loop](JavaScript 运行机制详解:再谈Event Loop),图中的WebAPIs就是JavaScript宿主环境所提供的线程。setTimeout会在该线程中执行,并不会影响JavaScript自身的主线程。`setTimeout`的延时时间结束之后,它就会进入到图中的回调消息队列中,当主线程空闲时,消息队列中的内容就会进入到主线程中去执行。

有了异步编程这样一个东西之后,JavaScript即使是单线程编程,也可以有并发的特性了。将网络请求和I/O操作等耗时的任务都交给异步编程来实现,主线程把控整体流程,这样主线程就不会被耗时任务拖累而发生阻塞。

Ajax

我们之前已经花了很大的篇幅来介绍异步编程和背后的思想。在这一部分我们谈谈对于浏览器而言,最重要的异步编程应用AJAX。

Ajax 即“Asynchronous Javascript And XML”(异步 JavaScript 和 XML),是指一种创建交互式、快速动态网页应用的网页开发技术,无需重新加载整个网页的情况下,能够更新部分网页的技术。
通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

因为异步操作往往需要用到callback,如果不太清楚callback的话,可以看一下[这篇文章](方应杭:「每日一题」Callback(回调)是什么?),里面的关于callback的比喻很形象。因为异步操作不是同步任务,把异步请求或者操作发送出去之后就继续执行同步任务了,所以很适合用这种执行完之后“打电话”回调的方式执行任务。

知道callback的原理之后我们继续回到AJAX。下面这段代码是模拟AJAX请求的代码:

function ajax(url, callback) {// 1、创建XMLHttpRequest对象var xmlhttpif (window.XMLHttpRequest) {xmlhttp = new XMLHttpRequest()} else { // 兼容早期浏览器xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')}// 2、发送请求xmlhttp.open('GET', url, true)xmlhttp.send()// 3、服务端响应xmlhttp.onreadystatechange = function () {if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {var obj = JSON.parse(xmlhttp.responseText)// console.log(obj)callback(obj)}}
}

当服务器有响应之后,把响应数据,通过回调函数返回回来。注意之前文章中说的回调函数特点,是程序执行完把响应回调回来。在服务器响应这种异步的情况下,是很适合使用回调函数的。我们执行上述ajax的代码可以如下:

var url = 'https://getman.cn/mock/route/to/demo'
ajax(url, res => {console.log(res)
})

这个URL路径是一个在线测试网站,你可以[点击这里](MockServer 在线API接口模拟 - Getman)查看该网站详情。如上所示,通常情况下我们定义回调函数的时候,是写成箭头函数的形式,在该函数里面具体定义要执行什么样的回调动作。为了便于理解,我把箭头函数改成普通函数的形式,代码如下:

ajax(url, function(res) {console.log(res)
})

最后再强调一下,我们是把一个函数作为参数传递进去了。这个函数就是回调函数。

理解了AJAX和回调函数之后,我们再来看下面一种情况。在实际编程中,我们经常会遇到执行某些请求,然后有了反馈再出发a动作,a动作执行完,有了反馈再触发b动作......。举一个例子,比如客户端请求一段视频内容,这时候服务器不会将整段视频全部传过去,而是切分成很多小段,一段一段传给客户端。客户端得到一段视频之后确认无误,会再次发送请求,服务端就会再传过来下一段内容。而这些操作都是要异步完成,如果我们写成回调函数,大概可能会长这样:

ajax(url, res => {dosomethingajax(url, res => {dosomethingajax(url, res => {dosomething......})})
})

很可能会嵌套非常多的层数,从可读性来讲,非常的差,这种代码结构称之为**回调地狱**。对于这种回调地狱的写法问题,我们可以通过promise来解决。

Promise

ES6中新增一个引用类型称之为Promise,可以通过new操作符来实例化

我们来举一个简单例子:

let p = new Promise(() => {setTimeout(() => {console.log('wait one second')}, 1000)
})

但需要特别注意的,特别是对于初学者而言,我上面虽然写的setTimeout是异步执行,如果去执行代码的话,也的确会停留1s之后再去输出。但这并不表示new Promise里面的所有代码都会异步执行。我们下面举一个例子:

let p = new Promise(() => {console.log(1)
})
console.log(2)

如果你打印输出的话,会发现,先输出的1,后输出的2。显然,执行console.log(2)是同步任务,如果Promise是异步的话,应该先打印2后打印1。因此,需要特别注意的是,Promise里面的代码是同步任务,是立刻执行的。可能这时候你有点糊涂了,我了解同步代码同步执行,异步代码异步执行(比如setTimeout)。那Promise有什么用呢?又怎么实现异步操作?别急,关于这一点,我们后面就会谈到。

对于Promise而言,它最重要的特点在于其具有**状态管理**的功能。它一共有三种状态:

pending

resolved

rejected

Promise的一个特点就是它只能从pending状态到resolved状态,或者从pending状态到rejected状态。不能反过来。或者它也可能一直处于pending状态,当我们new一个Promise的时候,它就是pending状态。

那这个状态有什么用?举一个简单例子:

let p = new Promise((resolve, reject) => {console.log('进入Promise')resolve('成功')// 因为promise状态是不可逆的,所以reject实际是执行不了的reject('失败')
}).then(res => {console.log(res)
}, err => {console.log(err)
})

你可以执行一下上述代码,看看会输出什么。

我们在new Promise的时候传入两个参数,分别是resolve和reject(需要注意的是这两个参数都是函数)。调用resolve()会把状态切换成resolved,调用reject()会把状态切换成rejected(同时会抛出一个错误)。就像我们前面说的,状态是不可逆的。所以上述代码执行完resolve()之后,reject()是不会被执行的。

在上面的代码中我们还看到`.then`方法,`Promise.prototype.then()`是为Promise实例添加处理程序的主要方法。执行上述代码我们也可以看到,resolve()或者reject()所传递的参数会进入到then中进行对应的处理。

通过上面的例子,我们就可以大致勾勒出Promise的应用场景。比如客户端通过Promise发送一个异步请求给服务器。服务器如果响应成功,就在resolve所对应的then中去执行对应代码,如果响应失败,就进入到reject,在reject对应的then中去执行对应代码。

另外,关于请求失败的代码,除了用then的第二个参数捕获之外,还可以写成`catch`的形式:

let p = new Promise((resolve, reject) => {console.log('进入Promise')resolve('成功')reject('失败')
}).then(res => {console.log(res)
}).catch(err => {console.log(err)
})

以上,关于Promise最为基础的部分介绍的已经足够多了,下面我们尝试用Promise来调用一下AJAX请求。

let p = new Promise((resolve, reject) => {console.log('客户端向服务端发送请求')ajax(url, res => {console.log('第一次获得的响应结果是', res)resolve('成功')})
}).then(resolveRes => {console.log('请求结果:', resolveRes)return new Promise((resolve, reject) => {ajax(url, res => {console.log('第二次获得的响应结果是', res)resolve('成功')})    })
}).then(resolveRes => {console.log('请求结果:', resolveRes)return new Promise((resolve, reject) => {ajax(url, res => {console.log('第三次获得的响应结果是', res)resolve('成功')})    })
})

上述代码模拟的是比如客户端多次向服务器发送视频请求的功能代码。看起来一定程度上减轻了回调地狱这种糟糕写法,将嵌套结构变成了链式结构。但看起来好像非常糟糕。一方面是大量重复代码,非常冗余,另一方面,从理解层面也没有改善多少。关于第二点,后面我们会介绍一种新的语法结构:`async`,`await`。不过现在我们还是继续回到Promise中。

上面的代码,需要特别强调的一点是,每次我们new一个Promise,都会加一个`return`,为什么要写return呢?

比如在第二次请求的那个对应的Promise中不写return的话,意味着就不会有返回值。后面的then执行,相当于对战原有的(就是最开始的那个)Promise对象的then继续再执行then(返回的会是一个空的promise)。只有return 之后,才意味着对新new出来的Promise再执行then。因此在书写代码的时候一定要记得加return,否则如果发生多层Promise嵌套的时候很有可能出现逻辑错误。

下面我们把上面代码中重复部分进行抽离,精简一下代码可以写成这样:

function getPromise(url) {return new Promise((resolve, reject) => {ajax(url, res => {console.log('获得的响应结果是', res)})resolve('成功')})
}getPromise(url).then(resolveRes => {console.log('请求结果:', resolveRes)return getPromise(url)
}).then(resolveRes => {console.log('请求结果:', resolveRes)return getPromise(url)
}).then(resolveRes => {console.log('请求结果:', resolveRes)return getPromise(url)
})

经过抽离之后,代码看起来简洁很多。同样的,我们还是要再强调一下这个return问题。明明定义getPromise函数的时候已经写了return了,为什么后面再调用的时候还要再加return?因为函数定义部分可以看到,调用函数之后返回的相当于是一个new Promise,你可以对照之前写的比较复杂的代码,这个new Promise还需要再返回一下才行。

需要注意的一点是,在上面的例子中,then是平级的,也就是说,即使前一层请求失败,也不会影响后一层的then执行。如果你想对不同的请求失败做统一的处理,可以这样写代码:

getPromise(url).then(resolveRes => {console.log('请求结果:', resolveRes)return getPromise(url)
}).then(resolveRes => {console.log('请求结果:', resolveRes)return getPromise(url)
}).then(resolveRes => {console.log('请求结果:', resolveRes)return getPromise(url)
}).catch(err => {console.log(err)
})

这样的话,只要有一个请求失败,就会直接触发catch,别的then都不会被执行了。

此外,Promise还有一些静态方法,我这里就不演示了。具体可以看MDN的Promise文档。

async / await

除了使用Promise之外,我们还可以使用async/await来实现异步编程。

async关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上。此外需要注意的是,如同我们在介绍Promise中强调的一样,虽然async关键字可以让函数具有异步特征,但里面的同步代码还是会同步执行。只有遇到异步执行的函数时才会异步执行。

async的函数在执行后都会自动返回一个Promise对象,所以,我们也可以在async函数后面接then来做处理。不过我们还有一种更好的方式是使用await关键字。await可以获取后面Promise对象成功状态传递出来的参数。

我们下面举一个具体例子,还是调用之前在讲ajax部分定义的函数。

function getPromise(url) {return new Promise((resolve, reject) => {ajax(url, res => {console.log('获得的响应结果是', res)})resolve('成功')})
}// 改造后
function getPromise(url) {return new Promise((resolve, reject) => {ajax(url, res => {resolve(res)})})
}

我们把之前定义的getPromise函数略微改造一下。然后还是对之前的客户端向服务端多次请

async function getData(){const res1 = await getPromise(url)console.log(res1)const res2 = await getPromise(url)console.log(res2)const res3 = await getPromise(url)console.log(res3)
}
getData()

通过await等待,我们会在每次得到数据之后再发送下一次请求。这样异步编程看起来就像同步代码了,使代码变得更容易理解。

因为async返回的是Promise,所以,对于失败的请求,我们还是可以通过then的第二个参数或者catch来捕获:

getData().catch(err => {console.log(err)
})

参考资料

[1] [JavaScript异步编程](熊建刚:JavaScript异步编程)

[2] [说一说javascript的异步编程](说一说javascript的异步编程 - 陈术芳 - 博客园)

[3] [线程进程 |两个简单例子告诉你什么是进程和线程 | 进程线程原来如此简单](线程进程 |两个简单例子告诉你什么是进程和线程 | 进程线程原来如此简单_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili)

[4] [并发与并行的区别是什么?](并发与并行的区别是什么?)

[5] [深入理解js事件循环机制(浏览器篇)](深入理解js事件循环机制(浏览器篇) - lynnelv's blog)

[6] [JavaScript 运行机制详解:再谈Event Loop](JavaScript 运行机制详解:再谈Event Loop)

[7] 《JavaScript高级程序设计(第4版)》

[8] 《深入浅出Nodejs》

[9] [ajax](ajax(Ajax 开发)_百度百科)

[10] [Callback(回调)是什么?](方应杭:「每日一题」Callback(回调)是什么?)

网页javascript加载不出_写给初学者的JavaScript异步编程和背后思想相关推荐

  1. 写给初学者的JavaScript异步编程和背后思想

    导读: 对于接触JavaScript这门编程语言没有多久的本菜鸡而言,在相当长的一段时间内,我都完全无法理解这门语言中的异步编程,不明白什么叫异步编程以及为什么需要异步编程.为什么顺序执行程序就不行了 ...

  2. 微软登录界面加载不出_微软要硬了,做出这个目前最好用的浏览器

    众所周知,微软能打造出一个全球通用的系统,但就是做不出一个能用的浏览器. IE 浏览器的黑历史就不提了,即使后来洗心革面,推出了基于 Win 10 的 Edge 浏览器,也还是难以挽尊 市场份额甚至不 ...

  3. axios vue 加载效果动画_在vue中通过axios异步使用echarts

    现实的工作中, 数据不可能是像之前的demo演示的那样把数据写死的. 所有的数据都应该通过发送请求进行获取, 所以, 这篇文章, 我将在Vue项目中使用Echarts: 在Vue中引入Echarts中 ...

  4. ssm框架验证码图片加载不出_基于SSM框架的文件图片上传/下载功能实现

    前一段时间很多做毕业设计的同学问:如何写图片和文件的上传下载功能,今天正好有时间,所以就做了一个案例,详细的讲解这个功能. 框架结构: 对于很多做过开发的而言,上传功能肯定都用过,而且用到的场景很多, ...

  5. 动态加载子节点_简易数据分析 10 | Web Scraper 翻页—抓取「滚动加载」类型网页...

    这是简易数据分析系列的第 10 篇文章. 原文首发于博客园:简易数据分析 10. 友情提示:这一篇文章的内容较多,信息量比较大,希望大家学习的时候多看几遍. 我们在刷朋友圈刷微博的时候,总会强调一个『 ...

  6. Matlab在win10运行不出图片,win10系统网页图片加载不出来的六种原因及解决方法...

    大家经常会使用浏览器来浏览一些站点查看图片,最近有的用户说win10系统打开网页时发现页面图片加载不出来,只显示一个×标志,让人百思不得其解,其实出现此问题可能是网速过低,或者浏览器设置里面没有开启相 ...

  7. vue网页预加载页面_页面预加载效果

    vue网页预加载页面 View demo 查看演示Download Source 下载源 Today we want to show you how to create a very simple p ...

  8. Node 本地网站服务器网页加载不出图片的解决方法

    文章目录 前言 遇到的问题 源代码 前言 最近在自学 Node.js 准备入门前端 在用 Node 配置本地网站服务器的时候连上了自己期末的前端综合实验设计的作业 遇到了一些问题来记录一下. 遇到的问 ...

  9. html游戏加载不出图片吗,网页图片加载不出来

    很多小伙伴在用打开网页的时候,发现网页的图片加载不出来,显示一个×的标志,这是怎么一回事呢?可能是你的网速过低,等待一会就可以了,也可能是设置里面没有开启相关模式,具体的解决方法下面一起来看看吧. 网 ...

最新文章

  1. android — JNI注册方法说明
  2. H5添加QQ好友的链接
  3. 一步步部署基于Windows系统的Jenkins持续集成环境
  4. ubuntu 解决“无法获得锁 /var/lib/dpkg/lock -open (11:资源暂时不可用)”的方法...
  5. mysql replication延迟_深入mysql主从复制延迟问题的详解
  6. PHP CodeBase: 求最近一个周一和上周一的日期
  7. weiit-saas搭建部署教程
  8. 关于windriver WD_NO_LICENSE错误和解决办法
  9. C#基础代码笔记(四)
  10. 服务器使用笔记本代理上网
  11. 当公司倒闭时,你在干什么?
  12. Centi和HandCash共同开发支付握手协议
  13. 第三届VueConf将于2019年6月8日在上海举行 转发评论送门票
  14. 中创算力|“商鼎云”助力元宇宙底层建设
  15. python读取加密word_Python 实现加密过的PDF文件转WORD格式
  16. c语言用字符输出李字,二级C语言历年真题汇总__第10篇对文件的输入输出-李赛红.doc...
  17. 函数(python)
  18. 用python画枫叶-Python中的用for,while循环遍历文件实例
  19. 复杂场景下的权限系统该怎么玩?ABAC权限模型帮你搞定它!
  20. qt槽函数如何传递多个参数_Qt中connect函数不能传递参数的两种解决方法

热门文章

  1. 快速入门虚拟机+linux安装(附带视频)
  2. 00002-两数之和-leetcode-1.暴力法(枚举法),2.哈希表法,目前更新了枚举法
  3. 计算机视觉——图像预处理及边缘检测
  4. 7-1 是否同一棵二叉搜索树 (30分)
  5. Leetcode--150. 逆波兰表达式求值
  6. java--自动装箱,拆箱
  7. linux系统安装显卡驱动卡顿,关于Ubuntu16.04上N卡驱动导致滚动屏幕卡顿问题
  8. java 佳博打印机,佳博蓝牙打印的java 连接
  9. 6位顺序号生成_分布式id生成策略,我和面试官扯了一个半小时
  10. OpenCV学习(二十) :分水岭算法:watershed()