黑马程序员技术交流社区
标题: 迭代器与 async/await [打印本页]
作者: charcoal 时间: 2021-6-20 10:13
标题: 迭代器与 async/await
前言本文主要介绍的是js中迭代器的实现原理,以及源码分析;async是如何以同步的方式编程的原理分析
前置知识1. 线程、进程
进程是操作系统分配资源的最小单位, 线程事操作系统执行的最小单位。
我的理解就是进程是程序拥有CPU内相关资源的一段时间的描述,强调的是一个时间段内的环境。而线程则是在这个环境中做的某件事情的描述,强调的是做的过程。
2. 用户态与内核态先说说内核,就是直接管理计算机硬件的软件其实就是操作系统(内核)对cpu硬件资源管理上的权限分离。线程、进程的切换是用内核完成的,即内核态。而用户态就是我们程序运行的状态。
协程接下来就讲讲我们的主角:协程。先从比较开始,对比线程:
- 线程的切换在内核态,开销大;协程的切换在用户态,开销小
- 进程中的多线程可以利用cpu的多核,可以并行执行;线程中的多协程执行是串行的,分时复用的方式利用线程中的资源,一个协程挂起将阻塞整个线程
- 多线程在对于共享变量需要加锁机制;协程不需要
迭代器(iterator)es5中的迭代实现我们来看看es5中是怎么实现的迭代功能的for of,看看babel转换的结果:[JavaScript] 纯文本查看 复制代码
let arr = [1,2,3]
for(let it of arr){
console.log(it)
}
babel转换结果如下,仅展示核心代码
[JavaScript] 纯文本查看 复制代码
var arr = [1, 2, 3];
var _iteratorNormalCompletion = true;
// 核心代码
for (var _iterator = arr[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var it = _step.value;
console.log(it);
}
其中最关键的就是实现了数组内部的一个函数:[Symbol.iterator]
- 先是调用[Symbol.iterator]生成一个_iterator对象,并马上执行启动迭代器的方法_step。
- 而执行_step就是执行iterator对象的next()方法,并将结果赋值给_step。
- 以_step对象中的done作为是否跳出循环的条件,true就跳出循环。对应代码:!(_iteratorNormalCompletion = (_step = _iterator.next()).done);
- 执行for of 中的代码,将_step对象的value作为本次循环值返回
- 重置循环条件的值,继续循环,对应代码:_iteratorNormalCompletion = true
了解了for of的实现原理,我们来实现一个可迭代的对象:
[JavaScript] 纯文本查看 复制代码
function iteratorObj( obj={} ){
return {
...obj,
// 1. 构造`Symbol.iterator`函数
[Symbol.iterator](){
const keys = Object.keys(obj)
let index = 0
// 2. 返回一个包含next函数的对象
return {
next: function(){
// 3. 执行next,返回对象
return {
value: obj[keys[index]] || undefined,
done: !obj[keys[index++]] // 4. 执行之后指针后移,继续迭代
}
},
}
}
}
}
// Test case
var itObj = iteratorObj({ a:'1',b:'2' })
for(let it of itObj){
console.log(it)
}
// 1
// 2
es6的迭代器将迭代器之前,我们来看看生成器generator,看一段代码
[JavaScript] 纯文本查看 复制代码
function* fn(){
yield 1;
yield 2;
yield 3;
}
var gen = fn();
gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: false}
gen.next(); // {value: 3, done: false}
gen.next(); // {value: undefined, done: true}
es6引入了生成器就是为了迭代而生的,下面我们用生成器来实现迭代功能,其实只要重写[Symbol.iterator]函数,我们拿上一步的代码改造
[JavaScript] 纯文本查看 复制代码
function iteratorObj( obj={} ){
return {
...obj,
// 1. 构造`Symbol.iterator`生成器函数
*[Symbol.iterator](){
const keys = Object.keys(obj)
// 由链式变成了线性 遍历
for (let i of keys){
yield obj
}
}
}
}
异步编程async/await 初探es6新增的promise是现代异步编程的解决方案。但promise写法仍然避免不了回调嵌套。比如下面:
[JavaScript] 纯文本查看 复制代码
function requestSystemData(){
fetch(url).then((res)=>{
//.....
fetch(url2).then(res=>{
//...
fetch(url3).then(res=>{
// ...
})
})
})
}
于是es7推出了async/await,改写以上代码
[JavaScript] 纯文本查看 复制代码
async function requestSystemData(){
const [res] = await fetch(url)
//...
const [res2] = await fetch(url2)
//...
const [res3] = await fetch(url3)
//...
}
生成器(generator)async 函数是什么?一句话,它就是 Generator 函数的语法糖。最关键的生成器generator函数,下面我们就重点考察
先看看一个简单的生成器函数
[JavaScript] 纯文本查看 复制代码
function * fn(){
console.log(1)
yield 1
console.log(2)
yield 2
console.log(3)
yield 3
}
var gen = fn();
gen.next() //{ value: 1, done: false }
gen.next() //{ value: 2, doen: false }
gen.next() //{ value: 3, doen: false }
gen.next() //{ value: undefined, doen: true }
继续我们看看es5中是如何实现的:
[JavaScript] 纯文本查看 复制代码
"use strict";
var _marked = [fn].map(regeneratorRuntime.mark);
function fn() {
return regeneratorRuntime.wrap(function fn$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
console.log(1)
_context.next = 2;
return 1;
case 2:
console.log(2)
_context.next = 4;
return 2;
case 4:
console.log(3)
_context.next = 6;
return 3;
case 6:
case "end":
return _context.stop();
}
}
}, _marked[0], this);
}
var gen = fn();
gen.next();
gen.next();
gen.next();
继续我们看看es5中是如何实现的:
[JavaScript] 纯文本查看 复制代码
"use strict";
var _marked = [fn].map(regeneratorRuntime.mark);
function fn() {
return regeneratorRuntime.wrap(function fn$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
console.log(1)
_context.next = 2;
return 1;
case 2:
console.log(2)
_context.next = 4;
return 2;
case 4:
console.log(3)
_context.next = 6;
return 3;
case 6:
case "end":
return _context.stop();
}
}
}, _marked[0], this);
}
var gen = fn();
gen.next();
gen.next();
gen.next();
我们简单分析一下,核心的就两点:
- 将生成器中的代码以yield为界分个代码块,并将yield后面的作为返回值
- 利用regeneratorRuntime.wrap构造一个生成器可以迭代执行别分割的各个代码。
那么接下来就简单的模拟实现一下,忽略旁枝末节:
[JavaScript] 纯文本查看 复制代码
// 1. 拿到babel转换分割后的代码
function fn$ (_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 1;
case 2:
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
case 6:
case "end":
return _context.stop();
}
}
}
// 2.实现上下文的包装函数
function regeneratorRuntime(){
const self = this
// 上下文对象
this.context = {
prev:0,
next:0,
done: false,
stop:function(){
self.context.done = true
},
}
this.wrap = function(fn){
return {
next: function(){
return {
value: fn(self.context),
done: self.context.done //如果是context.stop(),则返回
}
}
}
}
}
// Test case
var runtime = new regeneratorRuntime()
var gen = runtime.wrap(fn$)
console.log(gen.next()){ value: 1, done: false }
console.log(gen.next()) //{ value: 2, done: false }
console.log(gen.next()) //{ value: 3, done: false }
console.log(gen.next()) //{ value: undefined, done: true }
小结:
- generator能够看似中断、恢复执行的关键,就是执行上下文的保存。说的具体就是生成器代码块执行位置(prev,next)的保存。
- 分割的每块代码中,都将操作位置,指向下一块代码_context.next = X
- 每次执行next,其实都是执行生成器中的同一份代码,只是根据保存在上下文中的位置信息(prev,next),执行不同的代码块
到了es6,v8底层支持了生成器的写法,所以也不再需要js层面的语法糖(babel转译)。
async/await实现es2017推出了async,其实就是generator + promise的语法糖。
[JavaScript] 纯文本查看 复制代码
async function fn(){
console.log(1)
await new Promise(res=> setTimeout(function(){res(1)},500))
console.log(2)
await new Promise(res=> setTimeout(function(){res(2)},500))
console.log(3)
}
console.log(0)
fn()
console.log(4)
// 0
// 1
// 4
// 2
// 3
下面我们用生成器、promise语法来实现async语法糖代码:
[JavaScript] 纯文本查看 复制代码
// 1. 将async转译成*函数
function fn*(){
console.log(1)
yiel new Promise(res=> setTimeout(function(){res(1)},500))
console.log(2)
yield new Promise(res=> setTimeout(function(){res(2)},500))
console.log(3)
}
//2. 语法糖转化代码
function generatorToAsync(fn){
return function(){
//生成生成器
var gen = fn();
//执行async返回的是一个Promise对象
return new Promise((resolve,reject)=>{
var step = function(){
var info = gen.next()
var { done, value } = info
if(done) return resolve(value)
//将yield后面的值,包装为Promise对象
return Promise.resolve(value).then(value=>{
step()
})
}
//启动生成器
step()
})
}
}
至此,async函数实现了异步方法的同步编程,并且能够顺序执行函数内的所有异步方法。
总结- 协程的作用在于,能够在开发层面的函数的中断、恢复。核心在于函数上下文的保存
- 生成器的实现原理在于:函数上下文的保存。在es5中是语法糖,js层面的构造了上下文对象保存函数执行的位置;而在es6中是v8底层的支持,v8层面的保存了函数上下文。
- 迭代器的实现依赖于生成器,for of的迭代功能在于实现[Symbol.iteratoe]函数
- async是generator加Promise实现
欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) |
黑马程序员IT技术论坛 X3.2 |