共计 3772 个字符,预计需要花费 10 分钟才能阅读完成。
你能学到什么
- 如何使用
Generator
+Promise
实现异步编程 - 异步编程的原理解析
前言
结合 上一篇文章,我们来聊聊 Generator
基础原理
说到异步编程,你想到的是async
和 await
,但那也只是 Generator
的语法糖而已。dva 中有一个 Effect
的概念,它就是使用 Generator
来解决异步请求的问题,我们也来聊一聊 Generator
+ Promise
如何异步编程:
开始之前,我们需要了解一些基本的概念:
-
Generator
作为ES6
中使用协程的解决方案来处理异步编程的具体实现,它的特点是:Generator
中可以使用yield
关键字配合实例gen
调用next()
方法,来将其内部的语句分割执行。简言之 :next()
被调用一次,则yield
语句被执行一句,随着next()
调用,yield
语句被依次执行。 -
Promise
表示一个异步操作的最终状态(完成或失败),以及其返回的值。参考 Promise-MDN
所以, 异步编程使用 Generator
和 Promise
来实现的原理是什么呢?
- 因为
Generator
本身yield
语句是分离执行的,所以我们利用这一点,在yield
语句中返回一个Promise
对象 - 首次调用
Generator
中的next()
后, 假设返回值叫result
, 那么此时result.value
就是我们定义在yield
语句中的Promise
对象
注意:在这一步,我们已经把原来的执行流程暂停,转而执行 Promise
的内容, 已经实现了控制异步代码的执行,因为此时我们如果不继续执行 next()
则 generator
中位于当前被执行的 yield
后面的内容,将不会继续执行, 这已经达到了我们需要的效果
-
接下来我们就是在执行完当前
Promise
之后,让代码继续往下执行,直到遇到下一个yield
语句:这一步是最关键的 所以我们怎么做呢:
步骤 1:在当前的
Promise
的then()
方法中,继续执行gen.next()
步骤 2:当
gen.next()
返回的结果result.done === true
时, 我们拿到result.value
【也就是一个新的Promise
对象】再次执行并且在它的then()
方法中继续上面的步骤 1,直至result.done === false
的时候。这时候调用resolve()
使promise
状态改变,因为所有的yield
语句已经被执行完。- 步骤 1 保证了我们可以走到下一个
yield
语句 - 步骤 2 保证了下一个
yield
语句执行完不会中断,直至Generator
中的最后一个yield
语句被执行完。
- 步骤 1 保证了我们可以走到下一个
流程示意图:
具体实现
co 是著名大神 TJ 实现的
Generator
的二次封装库,那么我们就从co
库中的一个 demo 开始,了解我们的整个异步请求封装实现:
co(function*() {yield me.loginAction(me.form);
...
});
在这里我们引入了 co
库,并且用 co
来包裹了一个generator
(生成器)对象。
接下来我们看下 co
对于包裹起来的 generator
做了什么处理
function co(gen) {
// 1. 获取当前 co 函数的执行上下文环境, 获取到参数列表
var ctx = this;
var args = slice.call(arguments, 1);
// 2. 返回一个 Promise 对象
return new Promise(function(resolve, reject) {// 判断并且使用 ctx:context(上下文环境)和 arg:arguments(参数列表)初始化 generator 并且复制给 gen
// 注意:
// gen = gen.apply(ctx, args)之后
// 我们调用 gen.next() 时,返回的是一个指针,实际的值是一个对象
// 对象的形式:{done:[false | true], value: ''}
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// 当返回值不为 gen 时或者 gen.next 的类型不为 function【实际是判断是否为 generator】时
// 当前 promise 状态被设置为 resolve 而结束
if (!gen || typeof gen.next !== 'function') return resolve(gen);
// 否则执行 onFulfilled()
onFulfilled();});
}
总结一下这里发生了什么
- 返回一个
promise
-
promise
中将被包裹的generator
实例化为一个指针,指向generator
中第一个yield
语句 - 判断
generator
实例化出来的指针是否存在:如果没有yield
语句则指针不存在
判断指针gen.next()
方法是否为function
:如果不为function
证明无法执行gen.next()
条件有一项不满足就将promise
的状态置为resolve
否则执行onFulfilled()
接下来我们看下 onFulfilled()
的实现
function onFulfilled(res) {// 在执行 onFulfilled 时,定义了一个 ret 来储存 gen.next(res)执行后的指针对象
var ret;
try {ret = gen.next(res);
// 在这里,yield 语句抛出的值就是{value:me.loginAction(me.form), done:false}
} catch (e) {return reject(e);
}
// 将 ret 对象传入到我们定义在 promise 中的 next 方法中
next(ret);
return null;
}
总结一下,onFulfilled
最主要的工作就是
- 执行
gen.next()
使代码执行到yield
语句 - 将执行后返回的结果传入我们自定义的
next()
方法中
那么我们再来看 next()
方法
function next(ret) {
// 进入 next 中首先判断我们传入的 ret 的 done 状态:
// 情况 1:ret.done = true 代表我们这个 generator 中所有 yield 语句都已经执行完。// 那么将 ret.value 传入到 resolve()中,promise 的状态变成解决,整个过程结束。if (ret.done) return resolve(ret.value);
// 情况 2: 当前 ret.done = false 代表 generator 还未将所有的 yield 语句执行完,那么这时候
// 我们把当前上下文和 ret.value 传入 toPromise 中,将其转换为对应的 Promise 对象 `value`
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 当 value 确实是一个 promise 对象的时候,return value.then(onFulfilled,onRejected)
// 我们重新进入到了 generator 中,执行下一条 yield 语句
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object,'
+ 'but the following object was passed:"' + String(ret.value) + '"'));
}
总结一下,next
主要工作
- 判断上一次
yield
语句的执行结果 - 将
yield
的result
的value
值【其实就是我们要异步执行的Promise
】 - 执行
value
的then
方法,重新进入到onFulfilled
方法中,而在onFulfilled
中,我们又将进入到当前方法,如此循环的调用,实现了generator
和Promise
的执行切换,从而实现了Promise
的内容按照我们所定义的顺序执行。
有同学可能对这里的 toPromise
方法有一些疑惑,我先把代码贴出来
function toPromise(obj) {if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
其实这个函数做的事情就是,根据不同的类型进行转换,使得最后输出的类型都是一个 Promise
。那具体的转换细节,大家可以参考 co 库的源码。
至此实现异步操作的控制。
最后
这里是 Dendoink,奇舞周刊原创作者,掘金 [联合编辑 / 小册作者]。
对于技术人而言:技 是单兵作战能力,术 则是运用能力的方法。得心应手,出神入化就是 艺。在前端娱乐圈,我想成为一名出色的人民艺术家。扫码关注公众号 前端恶霸 我在这里等你: