前言
说到 Javascript 异步编程,在前端实践中,可以说是非常重要的一点,它的演变过程也是非常快的,从最初的回调函数再到后来的回调函数可以说是发生了质的改变,为什么我这么说 ? 因为尽管各种语法的演变,进化,其实最终还是回到了callback(回调函数),目前比较流行异步编程解决方案基本可以分为4类
- callback 异步解决方案(回调函数)
- Promise 异步解决方案
- async await 语法糖
- Generator 异步解决方案(遍历对象生成器)
在分享异步编程之前,我们首先先要搞懂什么是同步,什么是异步
同步模式
大白话的描述可能不是那么容易理解,我通过一段代码来表达下
console.log(1)
function hs () {
console.log(3)
}
function hs2 () {
console.log(2)
hs()
}
hs2()
console.log(4)
// 结果相信大家都知道 1,2,3,4
那它是怎么执行的呢,这个呢 Js 是有一定执行规则的,由于 Js 是单线程,在执行中,同步任务和异步任务分别进入不同线程,同步的进入主线程,异步的进入 Event Table (事件列表) 当指定的异步任务完成时,Event Table 会将这个函数移入 Event Queue (事件队列)。 当主线程内的任务执行为空时,会去 Event Queue (任务队列) 读取对应的函数,进入主线程执行。 上述过程会不断重复,也就是我们常说的 Event Loop (事件循环)。感兴趣的童鞋可以去我另外一篇文章了解一下 深入理解Js事件循环EventLoop
所以这个同步模式的示例,其实就是直接进入主线程,依次自上而下执行,故输出:1,2,3,4
异步模式
同样,用一段代码来表达一下
console.log(1)
setTimeout(function hs () {
console.log(2)
},2000)
setTimeout(function hs2 () {
console.log(3)
setTimeout(function hs3 () {
console.log(4)
},3000)
},1000)
console.log(5)
结果相信很多童鞋也都能说出来 1,5,3,2,4
那它又是怎么执行的呢 ? 这就是上述所表达的 event loop 事件循环,我们捋一下逻辑 (抛开全局调用栈)
- Js 单线程自上而下执行,走到 console.log(1),入栈 > 执行调用栈
打印 1> 出栈 - 然后,走到第一个 setTimeout 执行入栈操作 > 执行调用栈(在事件列表 Event Table 中注册事件 2s 后执行 hs 函数)> 出栈
注意,没有任何代码执行,只是在事件列表中注册了对应事件 - 紧跟着继续往下走,走到第二个 setTimeout 执行入栈操作 > 执行调用栈(在事件列表 Event Table 中注册事件 1s 后执行 hs2 函数)> 出栈
跟步骤2同理 - 最后走到 console.log(5) 同理,入栈 > 执行调用栈
打印 5> 出栈 - 结束,此时整个示例代码,调用栈中没有代码可执行了,控制台依次打印了 1,5
调用栈是没有了,可是我们的事件列表中还有呀,步骤2,3
- 所以 Js 会怎么做呢,它会将事件列表 Event Table 中的注册事件根据执行先后依次移入事件队列 Event Queue 中,移入事件队列中干什么,主角来了
Event Loop事件循环, 那么Event Loop什么时候执行 ? 看步骤 5 , 调用栈中没有可执行的任务了,此时会从事件队列中读取对应的函数执行,ok - 通过
Event loop事件循环,从事件队列Event Queue中读取对应的函数进入执行栈 - 就是 1s 后执行 hs2 函数,入栈 > 执行调用栈·
打印 3(在事件列表 Event Table 中注册事件 3s 后执行 hs3 函数) > 出栈 - 就是 2s 后执行 hs 函数,入栈 > 执行调用栈
打印 2> 出栈 - 结束,此时调用栈里又没有代码可执行了,同理,再去事件列表中找找看
- 哎呀,有一个 3s 后执行函数 hs3,移入事件队列,通过事件循环,入栈 > 调用执行栈
打印 4> 出栈 - over
这 12 步骤,详细的描述了事件循环 Event Loop 的执行规则,也清晰的表达了上述代码的执行逻辑,细心看两遍,准会,哈哈,我是个懒人,不太喜欢画图,So ~
oK, 通过两个简单的示例,了解了同步和异步的区别,回到主题,异步解决方案到底有哪些,都是怎么实现的,我们一一探讨
回调函数
假设我们需要请求一个接口,然后将接口返回数据经过加工处理渲染到页面上;正常我们是不是要在成功回调里写渲染逻辑呀,为了让逻辑代码更加简洁,思路更加清晰,回到函数就起到了质的作用
常规思路编码
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if(this.readyState === 4){
if(this.status === "200") {
// 拿到 this.responseText 返回值
// do something 做自己的业务逻辑
}
}
}
xhr.open("get","http://www.xxx.com")
xhr.send()
回调函数思路
什么叫回到函数 ? 就是在异步里边,回去调用你之前定义好的函数,这里我用一个简单的方式来模拟一下,封装一个 ajax 的请求,传入参数请求地址 url 和回调函数 callback
function myAjax (url,callback){
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if(this.readyState === 4){
if(this.status === "200") {
callback(this.responseText)
}
}
}
xhr.open("get",url)
xhr.send()
}
怎么使用 ?
myAjax("http://www.xxx.com",function (res) {
// 拿到返回值,做逻辑处理
console.log(res)
})
是不是一目了然,业务逻辑简单,通透,那么它真的好嘛 ? 我们换一个场景,假设我需要请求一个详细地址,逻辑市通过省,查询到市,通过市查询到区,通过区查询到街道,通过街道查询到具体地址,好,顺理成章,写下代码
myAjax("http://www.获取省.com",function (res) {
// 忽略判断
myAjax("http://www.获取市.com?id=省ID",function (res) {
// 忽略判断
myAjax("http://www.获取区.com?id=市ID",function (res) {
// 忽略判断
myAjax("http://www.获取街道.com?id=区ID",function (res) {
// 忽略判断
myAjax("http://www.获取街道.com?id=街道ID",function (res) {
// OK 查询详细地址了
})
})
})
})
})
若有所思,写的挺有规则,就是看起来不太雅观,这就是传说中的回调地狱了,依赖上一层的结果做下一件事儿,因此 Promise 出场了
Promise
什么是 Promise ? 它其实主要是用来解决回调地狱的,目前也是主流的异步解决方案之一
详细了解Promise使用及原理,快速通道,手写Promsie
这里举一个简单的例子看一下它怎么使用的 ?
new Promise((resolve,reject) => {
setTimeout(() => {
resolve("hellow promise")
},1000)
})
.then(
res => { console.log(`成功 ${res}`)}
)
Promise {<pending>} VM696:6 成功 hellow promise // 1s 后打印
其实就是 new 一个 Promise 实例,然后调用成功回调 resolve 改变状态,触发成功回调 then 方法。我们用 Promsie 对上述的省,市,区,街道,详细地址做一个简单的优化
首先我们改写一下 myAjax 方法,之前我们是传入一个回调函数 callback,现在不传了,我们直接在myAjax 方法里返回一个 promise ,并且在请求成功之后,触发 promise 的成功回调,那这样是不是就可以触发 Promise 的then 方法了 这里不懂的小伙伴需要去学习一下 Promise 的使用或者看我上边的链接文章 手写Promise
function myAjax (url){
return new Promise((resolve,reject) => {
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if(this.readyState === 4){
if(this.status === "200") {
resolve(this.responseText)
}
}
}
xhr.open("get",url)
xhr.send()
})
}
好 myAjax 经过一个小小的改动,我们实际的代码就发生了质的改变
myAjax("http://www.获取省.com")
.then(res => {
// res.resId (省ID)
return myAjax("http://www.获取市.com?id=resId")
})
.then(res => {
// res.resId (市ID)
return myAjax("http://www.获取区.com?id=resId")
})
.then(res => {
// res.resId (区ID)
return myAjax("http://www.获取街道.com?id=resId")
})
.then(res => {
// res.resId (街道ID)
return myAjax("http://www.获取详细地址.com?id=resId")
})
.catch(err => {
// 异常处理
})
嗯哼,看起来优雅多了,其实代码并没有发现变少哦 ~ 一路的 .then 链式调用,那有没有更好的办法来解决这一问题呢 ? 答案当然是有的 Async await 来了
Async await
ES2017 标准引入了 async 函数, 那么它到底是个什么玩意儿,又解决了什么问题,其实它只是一种语法糖,Generator 函数的语法糖,那什么又是 Generator 函数 ? 这里引发了两个东西 async await 和 Generator
先来看看 async await , async 函数其实跟普通函数没有什么大的区别,在使用上基本也同样,只是在函数声明前方添加一个 async 关键字,举个例子
function foo () {return "我是普通函数"}
foo() // "我是普通函数"
async function foo () {return "我是async函数"}
foo() // Promise {<fulfilled>: "我是async函数"}
通过一个简短的代码,可以知道,普通函数执行,直接返回函数结果,而 async 函数执行则是返回一个 promise 并且状态默认是 fulfilled 成功,返回成功结果,所以在面对 async 函数执行的时候,我们外部可以用 .then 的方式来获取函数返回的结果,是这样子的
async function foo () {return "我是async函数"}
foo() // Promise {<fulfilled>: "我是async函数"}
foo().then(c => {console.log(c)})
// 我是async函数
其实上边的 foo 函数实体,本质是返回一个 promise ,所以便有了下边这样的代码
async function foo () {
return new Promise( resolve => {
resolve("我是async函数")
})
}
foo().then(c => {console.log(c)})
// 我是 async 函数
说完 async ,再来说说 await 有什么作用 ? async 表示函数里有异步操作,await 则是表示紧跟在后面的表达式需要等待结果, 来实践一下
这里用一个 promise + setTImeout 来模拟一下异步睡眠延迟执行,await 后边也可以跟一个异步表达式,如:网络请求等
// 睡眠 function
let sleep = time => new Promise(f => setTimeout(f,time))
async function foo () {
await sleep(2000)
console.log("2s 后我进来了")
return "2s 后我进来了"
}
foo()
// 2s 后执行 console.log("2s 后我进来了")
//扩展:可以通过 then 方法回调 async 函数返回的结果
foo().then(c => {console.log(c)})
// 2s 后我进来了
总结下来就是将异步的任务写成同步的模式罢了 !
好了,简单的了解过 async await 的用法之后,我们回到主题,来优化一版上述的省,市,区,街道,详细地址
原本的 myAjax 封装不变,因为 async 函数执行本身就返回的是一个 promise , 而我们的 myAjax 也是返回一个 promise , 这一点,没毛病
function myAjax (url){
return new Promise((resolve,reject) => {
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if(this.readyState === 4){
if(this.status === "200") {
resolve(this.responseText)
}
}
}
xhr.open("get",url)
xhr.send()
})
}
所以优化改动的就是我们的业务代码块了,这里不考虑请求失败和异常处理
// 定义一个执行Async函数方法
async function excutorAsync () {
let s0 = await myAjax("http://www.获取省.com")
let s1 = await myAjax("http://www.获取市.com?id="+s0)
let s2 = await myAjax("http://www.获取区.com?id="+s1)
let s3 = await myAjax("http://www.获取街道.com?id="+s2)
let s4 = await myAjax("http://www.获取详细地址.com?id="+s3)
}
看到这里也许有的童鞋有点懵,这什么鬼 ? 别激动,我们捋一捋
- let s0 = myAjax("http://www.获取省.com") myAjax 方法返回一个 promise
- 同等于 let s0 = new Promsie(resolve => resolve("对应请求成功的结果"))
- 加上
await: let s0 = await new Promsie(resolve => resolve("对应请求成功的结果")) - 上边介绍 async 函数第一个例子,标红字段落,执行 async 函数返回一个 promise 默认状态 fulfilled 的成功结果
- 所以:let s0 = ( await 后边异步表达式的请求成功结果,即省Id )
- 同理,异步任务依次同步模式自上而下执行
emmm ~ over ,这个看起来就乐观多了。
Generator 函数
刚才说 async await 是 Generator 函数的语法糖,既然有了糖,那原味的 Generator 肯定就被大众逐渐忘掉口味了,所以在 Js 目前主流的异步解决方案中也缺少了它的身影,逐渐被 async await 所替代,今天我们也简单的看一下它的用法
Generator 函数是 ES6 提供的一种异步编程解决方案,与普通函数有两个小小区别,
function关键字与函数名之间有一个星号 *- 函数体内部使用
yield表达式
来一段代码看看
function* foo () {
yield "hello"
yield "Grenerator"
return "over"
}
foo() // foo {<suspended>}
let g = foo()
g.next() // {value: "hello", done: false}
g.next() // {value: "Grenerator", done: false}
g.next() // {value: "over", done: true}
看着打印信息挺有意思的,单独执行 foo 函数,却没有执行任何代码,而是返回一个指向内部状态的指针对象(遍历器对象 Iterator Object),那要怎么执行内部代码 ?
引用阮一峰老师的描述就是:
需要调用遍历器对象的 next 方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
那么执行遍历器对象的 next 方法后,返回值表示什么 ?
{
value: "hello", // value: yield 后边表达式的结果
done: false // done: 表示遍历器遍历是否结束
}
经过分析之后,了解了它的执行逻辑与规则,再回到上述的例子,来实践实践
function* foo () {
let s0 = yield myAjax("http://www.获取省.com")
let s1 = yield myAjax("http://www.获取市.com?id="+s0)
let s2 = yield myAjax("http://www.获取区.com?id="+s1)
let s3 = yield myAjax("http://www.获取街道.com?id="+s2)
let s4 = yield myAjax("http://www.获取详细地址.com?id="+s3)
return s4
}
var g = foo()
// 定义
function co (res) {
if(res.done) return;
res.value.then(data => {
co(g.next())
})
}
co(g.next())
- 定义一个
Generator函数 foo - 按照同步的思路,写下请求的逻辑代码
- 获取遍历器对象
g - 定义遍历器对象循环执行方法
co, 初次调用co(g.next()) - 每次调用
co(g.next()), 按照myAjax方法封装则是返回一个promise, 所以返回值为{ value:promsie,done:true/false } - 所以
co函数的递归处理则是res.value.then(...), 并传入下一次g.next(),直到done为true
结束,有没有似曾相识的感觉 ? Generator 函数的 foo 和 async await 函数的 excutorAsync 基本是一样的,只是将 * 换成了 async , yield 换成了 await ,所以才说 async await 是 Generator 的语法糖,这样写的好处是,不用自己调用遍历器对象的 next 方法,而且更加语义化。
结束 ,Js 的异步解决方案是前端选手必备的核心知识点,你都掌握了嘛 ?
小小鼓励,大大成长,欢迎点赞