第一个问题:同步和异步
通常情况下,我们所编写的JS代码都是同步运行的。
console.log(123)
someFunction()// 需要执行30秒钟
document.write("Hello")
上例中的代码就是同步代码,console.log()
先执行、然后是someFunction()
最后是document.write()
。同步意味着代码的执行顺序为自上向下一行一行执行,执行完一行才会执行下一行。如果代码中有某一行执行速度比较慢,比如someFunction()
执行速度较慢,需要30秒钟,那么其后的所有代码都要等待它的执行才能够继续运行。
这样一来,当我们调用一些执行速度比较慢的方法时,会由于它一个而影响到整个程序的执行。最典型的便是通过JS向服务器发送请求。发送请求时受限于网络的速度和数据的多与少,执行速度可慢可快。如果依然使用同步的编程方式,将大大的影响到程序的执行性能。
如何解决呢?首先我们来看看发送请求的过程:①JS向服务器发送请求;②请求到达服务器,服务器处理请求并返回数据;③JS(浏览器)收到并处理数据。整个过程中JS要处理的是向服务器发送数据和接收服务器返回的数据,也就是①和③。服务器处理数据,返回响应(也就是②)这一过程是无需JS参与的。但是如果我们依然使用同步的方式来编写代码,那么就必须要等待服务器处理完毕才能继续运行后边的程序,并且在等待过程中用户什么事情也做不了,只能干等。
如果是其他的编程语言,完全可以在开一个新的线程,一个线程负责等待,一个线程去做别的事情。但是JS不行,JS是单线程的,只有一个线程,没有其他线程可供选择。那么摆在我们面前的有两条路,一条路是让线程一直等待,直到服务器返回数据才继续向下执行代码。第二条路,线程不等待,先去运行别的代码,等到服务器数据返回了,再回来处理数据。显然,第一条路不是一个好的选择。而第二条路中代码的运行方式就是异步。
所谓异步,指代码并不按照固定的顺序一行执行完再执行下一行,当遇到一些需要等待其他进程执行的操作时(比如:发送请求,访问用户设备等)不再等待,而是继续向下执行其他功能,直到那些需要等待的操作完成后,才回来完成后续工作。
同步代码流程:
- 发送请求
- 等待服务器
- 获得响应数据,渲染组件
- 其他操作
异步代码流程:
- 发送请求
- 其他操作
- 当服务器返回响应时:获得响应数据,渲染组件
Ajax
Ajax是早期的JS中发送异步请求的方式,示例代码:
var xhr = new XMLHttpRequest()
xhr.onload = () => {
console.log(JSON.parse(xhr.response))
document.body.insertAdjacentHTML("beforeend", xhr.response)
}
xhr.open("get","https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json")
xhr.send()
Ajax的异步处理方式是基于事件的,在创建玩xhr对象后,会为其onload事件(或onreadystatechange事件)绑定一个回调函数,事件会在服务器的响应数据返回后触发。这样当我们向服务器发送请求后,代码不会被阻塞,而是继续运行(期间不会影响到用户的操作)。直到服务器数据正常返回,onload事件被触发,回调函数才会执行。
回调函数的困扰
通过回调函数实现异步时,回调函数会在那些需要等待的操作完成后才执行。只要这样才能确保功能正常执行,这是因为回调函数的执行,通常会依赖操作完成后所产生的数据,比如在Ajax的回调函数中就需要编写处理响应数据的代码,响应数据回来之前函数是不能执行的。
如果是简单的回调还好,但是一旦遇到复杂的多次回调,事情也许就不那么美妙了。比如:有四个函数fn1()
、fn2()
、fn3()
和fn4()
,四个函数互相依赖,2需要1的返回值,3需要2的返回值,4需要3的返回值。像是这样:
function fn1(num){
return num + 1
}
function fn2(num){
return num + 2
}
function fn3(num){
return num + 3
}
function fn4(num){
return num + 4
}
let result = fn1(10)
result = fn2(result)
result = fn3(result)
result = fn4(result)
几个函数并没有什么意义,单纯结构上的展示。修改为回调函数的版本,就是这样的:
function fn1(num, callback){
return callback(num + 1)
}
function fn2(num, callback){
return callback(num + 2)
}
function fn3(num, callback){
return callback(num + 3)
}
function fn4(num, callback){
return callback(num + 4)
}
fn1(10, n=>{
fn2(n, n=>{
fn3(n, n => {
fn4(n, n => {
console.log(n)
})
})
})
})
我就问你,看见这种代码你恶心不恶心。这种代码结构被称为“回调地狱”或者“末日金字塔”,听这个名字就十分的晦气,这里我就不过多的解释了。过多的回调增加了代码的复杂度,非常的不易于维护。可是异步本身又十分依赖于回调函数,所以要写好异步代码必须先处理好回调函数的问题。
Promise
回调函数的问题主要在于嵌套,回调函数之所以嵌套是因为当前的回调函数依赖于上一个回调函数的执行结果,为了确保在上一个回调函数执行完毕后当前回调函数才执行,我们只能在上一个回调函数中调用当前的回调函数。怎么能避免这个问题呢?Promise应运而生。
Promise直译过来为“承诺”。它可以用来存储异步代码(同步代码亦可),并且它可以确保无论异步代码执行成功或失败都会给我们返回一个结果。如此一来,我们便可以直接将函数的代码直接存储到Promise中,然后将Promise作为返回值返回,这样回调函数可以直接通过Promise来获取上一个函数的执行结果,并且Promise会确保只有得到结果时,才会调用回调函数。
示例:
const myPromise = new Promise((resolve) => {
// 异步代码...
resolve(10) // 返回运行结果
})
myPromise
.then(result => console.log(result))
.catch(e => console.log("代码出错", err))
首先,使用new Promise()
来创建一个Promise对象,它需要一个回调函数作为参数,可以将异步的代码直接编写到回调函数中。回调函数的第一个参数是一个函数,这里命名为resolve,可以在异步功能完成后对其进行调用,然后将期望返回的结果设置为它的实参。
可以将Promise对象理解为存储异步代码结果的容器,异步代码的执行结果都存储到了该对象中,那么如何读取呢?可以直接调用promise对象的then方法,并传递一个回调函数作为参数,执行结果会通过回调函数的第一个参数返回。如果异步代码执行失败,then的回调函数不会执行,而是执行catch()方法的回调函数。关于Promise的内容还有挺多,但是这节课主要还是介绍Promise所以这里不再过多强调。
Fetch
Fetch是Ajax的进化版,直接使用了Fetch API,相较于Ajax更加简便:
fetch("https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json")
.then(res => res.json())
.then(data => {
console.log(data)
document.body.insertAdjacentHTML("beforeend", JSON.stringify(data))
})
Axios
Axios是一个第三方的发送请求的工具,主要是用来代替的Ajax,同样支持Promise,且功能也较Ajax和Fetch要强大一些,只是使用时需要引入第三方库:
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
axios
.get(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json"
)
.then(function (response) {
console.log(response)
})
.catch(function (error) {
console.log(error)
})
</script>
总结:
技术 | 原生 | 作用 |
Ajax | √ | 发送请求 |
Fetch | √ | 发送请求 |
Axios | × | 发送请求 |
Promise | √ | 为异步提供支持 |
源码:
链接:https://pan.baidu.com/s/1LJwOqOqRvocX5Y48wCJ3BA?pwd=ehp8
提取码:ehp8
李老师??