碎碎念:
亲爱的读者:你好!我叫 Changlon —— 一个非科班程序员、一个致力于前端的开发者、一个热爱生活且又时有忧郁的思考者。
如果我的文章能给你带来一些收获,你的点赞收藏将是对我莫大的鼓励!
我的邮箱:thinker_changlon@163.com
我的Github: https://github.com/Changlon
参考文章: Generator 函数的含义与用法 MDN Generator
github源码地址:myasync
自从有了Promise 这个好用的技术,我们的异步编程变得更加简洁明了,告别了那个回调地狱的时代。我们只需要不停得调用then 函数就能清爽地组织下一个事件循环中的代码! 那么这样就够了吗?我们能否再进一步,把异步编程变得像同步编程那样? 答案是肯定的。 举个例子:
import {queryData} from '/api'
methods:{
setData() {
const that = this
queryData()
.then(res=>{
that.data = res.data
})
}
}
在上面这个例子中,我们在一个同步函数setData 中调用了 queryData 请求后端数据,queryData 返回一个Promise ,我们在then方法中编写了获取到数据后要调用的方法,即把获取到的数据设置到that.data中去。 这样一看似乎是没有什么问题,但是如果当业务逻辑不断变更,代码量不断增多后,需要处理很多这样的then 方法中的回调有时候修改某个地方的代码就有可能因漏掉一个大括号而寻找半天!可以看出光有Promise 这种异步编程方式还是有一点欠缺的! 因此就诞生了异步函数! 还是上面那个例子我们看一下异步函数是如何写的。
import {queryData} from '/api'
methods:{
async setData() {
const result = await queryData()
if(result && result.data) this.data = result.data
}
可以很明显得看出异步函数编写的优势,以一种同步的代码书写方式我们就可以写出异步回调执行的代码!上面的代码在执行到
const result = await queryData()
这一句时就会进入等待模式,当queryData 函数成功返回数据后再接着往下执行后面的代码!
那么本章我们的任务是通过手写一个async_ 函数来了解async ,await 的执行原理!为什么加了一个await 就能实现一个类似阻塞的效果!
在了解原理之前我们要先来补充一个知识点叫做: Generator 生成器 如果已经有了解的同学就请直接跳过。
首先看一个实例:
function * gen() {
yield 1
yield 2
yield 3
}
let g = gen()
let result = g.next()
while(!result.done) {
console.log(result.value)
result = g.next()
}
上面的代码会打印出// 1,2,3 。 当我们的函数以function * 形式定义的时候,它就是一个函数生成器。当我们执行这个函数生成器后可以得到一个可以操作函数执行迭代的对象。可以简单的理解为函数的执行器。它的原型上有一个非常重要的方法:Generator.prototype.next() 作用是返回一个由 yield 表达式生成的值。 返回的数据格式是:{value,done} value 是yield 后面的值,done 是表示该函数迭代器是否执行到底。 所以上面的例子通过done 判断是否执行到生成器的底部,通过value 打印出每个yield 后面的值。所以就打印了 // 1,2,3
其实next 函数我们还可以给它传递一个参数值 如g.next(1) ,这个参数值在函数迭代器g 执行时赋给yield表达式前面的变量。 如下代码:
function * gen() {
console.log(1)
let v = yield
console.log(v)
console.log(2)
yield
console.log(3)
yield
}
let g = gen()
console.log(g.next('a'))
console.log(g.next('a'))
最终打印的结果为: 1,{value: undefined, done: false},a,2,{value: undefined, done: false} 为什么会出现上面这样的结果呢?我们来简单分析一下: 1.首先打印执行的是 console.log(g.next('a')) ,先会执行第一个yield 前的所有语句代码 console.log(1) //打印出1,之后把 'a' 赋给 v ,最后返回yield 后的信息{value: undefined, done: false} 并打印,所以第一个console打印的是:
1,{value: undefined, done: false}
- 接着执行第二个
console.log ,首先是g.next('a') 它会执行第二个yield 之前,第一个yield 之后的代码即打印v和2,由于v 之前被赋值为 'a' 所以第二个console打印的数据是:
a,2,{value: undefined, done: false}
综上就解释了为何会出现这样的打印数据,我们也基本理解的生成器的用法,简单来说就是通过代码中的yield 来控制迭代函数的执行! 有了上面的铺垫我们就可以来实现我们自己的async,await了。
首先给出我们自己的async_ 函数的异步使用方法,然后我们自己去编写async_ 的实现!
import {async_,promise} from "../index.js"
async_(function *() {
console.log(`测试async_ ... `)
let a = 1
let b = 2
let c =yield new promise((resolve,reject)=>{
setTimeout(()=>{
resolve(a+b)
},1000)
})
console.log(`计算结果a+b = ${c}`)
console.log(`请求后台数据...`)
let result = yield new promise((resolve,reject)=>{
let r = Math.floor(Math.random()*10)
if(r%2==0) {
resolve({
code:200,
msg:'请求成功!',
data:[
{name:'changlon',age:21},
{name:'jack',age:25}
]
})
}else{
reject(new Error('网络请求失败!'))
}
})
if(result && result.code==200) {
console.log(`数据请求成功:${result.data}`)
}else{
console.log(`数据请求失败:${result}`)
}
console.log(`测试 async_ 完毕。`)
})
要想让上面的这段代码已异步的方式去执行我们首先得定义一个 async_ 函数,这个函数接受一个 生成器函数Generator 作为参数。
function async_(generator) {
}
好的,我们的函数就定义好了。下面的重点是如何运用Generator 生成器迭代执行异步方法。 我们的大概思路就是: 每次调用next 函数返回的value 值作为参数传递给Promise.resolve 方法,在then 函数中将返回的值作为下一次 next迭代函数的参数传入。这样递归的执行下去,直至结束。
下面给出实现方法:
function async_(generator) {
const isValid = generator && "[object Generator]" === Object.prototype.toString
.call(generator.prototype)
if(!isValid) throw new Error(`async_ typeError: 请传入生成器函数!错误类型 => ${generator}`)
const gen = generator()
excutor( gen ,gen.next() )
function excutor(gen,result) {
if(result.done) return void 0
promise.resolve(result.value).then(val=>{
excutor(gen,gen.next(val))
})
.catch(err=>{
excutor(gen,gen.next(err))
})
}
}
需要注意的是我们上面的Promise是使用的上一次我们自己封装的一个promise,这样我们大概就实现了自己的async,await。浏览器中的async,await原理也是相同的。
|