网站源码整站打包,网页设计用到的软件,网站建设历程,什么是网站开发框架导读#xff1a;对于接触JavaScript这门编程语言没有多久的本菜鸡而言#xff0c;在相当长的一段时间内#xff0c;我都完全无法理解这门语言中的异步编程#xff0c;不明白什么叫异步编程以及为什么需要异步编程。为什么顺序执行程序就不行了呢#xff1f;非要使用异步回…导读对于接触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它们在任务队列中加入各种事件clickloaddone。只要栈中的代码执行完毕主线程就会去读取任务队列依次执行那些事件所对应的回调函数。上图和对应的文字内容引用自[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来解决。PromiseES6中新增一个引用类型称之为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而言它最重要的特点在于其具有**状态管理**的功能。它一共有三种状态pendingresolvedrejectedPromise的一个特点就是它只能从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(成功)}) })
})
上述代码模拟的是比如客户端多次向服务器发送视频请求的功能代码。看起来一定程度上减轻了回调地狱这种糟糕写法将嵌套结构变成了链式结构。但看起来好像非常糟糕。一方面是大量重复代码非常冗余另一方面从理解层面也没有改善多少。关于第二点后面我们会介绍一种新的语法结构asyncawait。不过现在我们还是继续回到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事件循环机制浏览器篇 - lynnelvs blog)[6] [JavaScript 运行机制详解再谈Event Loop](JavaScript 运行机制详解再谈Event Loop)[7] 《JavaScript高级程序设计第4版》[8] 《深入浅出Nodejs》[9] [ajax](ajaxAjax 开发_百度百科)[10] [Callback回调是什么](方应杭「每日一题」Callback回调是什么)