JS异步编程:Iterator迭代器原理、Generator函数与async/await原理

758 阅读11分钟

Iterator迭代器原理

ECMAScript 6 入门-Iterator 和 for...of 循环

JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了MapSet。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是MapMap的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的作用有三个:

  • 一是为各种数据结构,提供一个统一的、简便的访问接口;
  • 二是使得数据结构的成员能够按某种次序排列;
  • 三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

image.png

Iterator仅仅是一种机制,这种机制有如下规定:

  • 拥有 next 方法用于依次遍历数据结构的成员
  • 每一次遍历返回的结果是一个对象 {done:false,value:xxx}
    • done:记录是否遍历完成
    • value:当前遍历的结果

例如我们想通过这套机制遍历数组,我们可以自己实现这套机制:

  1. 传入集合
  2. 执行一次 next ,就能拿到集合中的每一项

例如以下效果:

let itor = new Iterator([10, 20, 30, 40]);
console.log(itor.next()); //->{value:10,done:false}
console.log(itor.next()); //->{value:20,done:false}
console.log(itor.next()); //->{value:30,done:false}
console.log(itor.next()); //->{value:40,done:false}
console.log(itor.next()); //->{value:undefined,done:true}
//后面再执行,就是{value:undefined,done:true}
class Iterator {
    constructor(assemble) {//传入集合
        let self = this;
        self.assemble = assemble;
        self.index = 0;
    }
    next() {
    //执行一次next就能拿到集合中的每一项
        let self = this,
            assemble = self.assemble;
        if (self.index > assemble.length - 1) {
            return {
                done: true,
                value: undefined
            };
        }
        return {
            done: false,
            value: assemble[self.index++]
        };
    }
}

以上都是通过js自己创造了一套数组的迭代器规范。通过 new Iterator 创造出来的实例对象就是遍历器,因为它实现了遍历器所规定的规范。

以上是我们自己实现的遍历器机制,ES6当中有很多默认的数据结构就具备这套机制。所有拥有 Symbol.iterator 属性的数据结构(值),被称为可被遍历的,可以基于 for of 循环处理。 for of 是按照iterator规范进行迭代的

  • 数组
  • 部分类数组:arguments/NodeList/HTMLCollection...
  • String
  • Set
  • Map
  • generator object
  • ...

对象默认不具备 Symbol.iterator ,属于不可被遍历的数据结构

image。png

数组的 for of 运行原理的举例:


let arr = [10, 20, 30, 40];
//arr[Symbol.iterator]=function...
//arr[Symbol.iterator]() -> 返回一个具备iterator规范的对象「拥有next方法的对象」 itor
for (let item of arr) {
    // 每一轮循环都执行一次next
    // itor.next()  item->value  done如果是false则进行下一轮循环
    // ...
    // done成为true,则整个循环结束
    console.log(item);
} 
  1. 数组有一个内置对象 arr[Symbol.iterator]=function...
  2. arr[Symbol.iterator]() -> 返回一个具备iterator规范的对象,即拥有 next 方法的遍历器对象
  3. 每一轮循环都执行一次 next ,循环的值 item 就是返回值的 valuedone 如果是 false 则进行下一轮循环
  4. done 成为 true ,则整个循环结束

手写一下 arr[Symbol.iterator] 实现:


let arr = [10, 20, 30, 40];

arr[Symbol.iterator] = function () {
    let assemble = this,
        index = 0;
    return {
        next() {
            if (index > assemble.length - 1) {
                return {
                    done: true,
                    value: undefined
                };
            }
            return {
                done: false,
                value: assemble[index++]
            };
        }
    };
}; 
for (let item of arr) {
    console.log(item);
} 

或者我们刚实现了一个Iterator类

arr[Symbol.iterator] = function () {
    return new Iterator(this);
}

我们可以重构 Symbol.iterator 来实现自己想实现的遍历,例如在返回值里面乘以 10

实现对象遍历器

实现对象遍历器,使其可以用for of 迭代

 let obj = {
    name: 'xxx',
    age: 12
};
for (let value of obj) {
    console.log(value);
} 

image。png

for of 无法迭代对象

 let obj = {
    name: 'xxx',
    age: 12
};
Object.prototype[Symbol.iterator] = function () {
    let assemble = this,
        keys = Object.keys(assemble).concat(Object.getOwnPropertySymbols(assemble)),
        index = 0;
    return {
        next() {
            if (index > keys.length - 1) {
                return {
                    done: true,
                    value: undefined
                };
            }
            return {
                done: false,
                value: assemble[keys[index++]]
            };
        }
    };
};
for (let value of obj) {
    console.log(value);
} 

自己构建的类数组对象,也可以利用数组的迭代器迭代类数组对象。

  
let obj = {
    0: 10,
    1: 20,
    2: 30,
    length: 3
};
obj[Symbol.iterator] = Array.prototype[Symbol.iterator];
for (let value of obj) {
    console.log(value);
} 

Generator函数

Generator 函数-ES6入门

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是, function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式,定义不同的内部状态( yield 在英语里的意思就是“产出”)。

Generator函数与普通函数的区别

普通函数

function fn() {
    console.log(this);//window
}
let gen = fn();
console.log(gen);//undefined
console.log(typeof fn); //->"function"
console.log(fn instanceof Function); //->true 

Generator 函数:

function* fn() {
    console.log(this);
}
let gen = fn(); //->虽然看上去fn后面加小括号了,但是和我们理解的fn执行是不一样的「函数体中代码并没有执行」
console.log(gen); //->返回的结果不是undefined,而是具备迭代器规范的一个对象「生成器函数执行返回一个迭代器」Generator类的一个实例
console.log(typeof fn); //->"function"
console.log(fn instanceof Function); //->true 

Object.prototype.toString.call(gen)//"[object Generator]"

image。png

Generator函数和普通函数区别:

  1. function 关键字后面加一个 *

  2. 如上例,console.log(gen) ,虽然看上去 fn 后面加小括号执行了,但是和普通函数的 fn 执行是不一样。函数执行了,但是函数体中代码并没有执行,没有打印出 this。函数执行返回的结果不是 undefined ,而是是具备迭代器规范的对象,即生成器函数执行返回一个迭代器。(Generator类在浏览器不可主动访问)。函数执行返回的实例,原型链如下: gen.__proto__ -> fn.prototype -> GeneratorFunction.prototype (其有 next / return / throw / Symbol.toStringTag 这些属性)-> xxx.prototype (其有 Symbol.iterator 属性,说明有迭代器相关的特点) -> Object.prototype

  3. Generator函数的返回值是当前类(函数)的一个实例(并没有 new )。看上去当做普通函数执行,其实返回的是当前类的实例。

    测试:

    function* fn() {
    }
    fn.prototype.query = function () {};
    let gen = fn();
    console.log(gen)
    

    给原型上添加一个属性,结果返回的实例也有这个属性。 image。png

  4. 不可以 new 执行 image。png

Generator函数的使用

如何让生成器函数里的代码进行执行呢?

执行方式:

  1. Generator函数会返回一个迭代器对象,每一次执行 next 方法都会去函数体中执行代码
  2. 从开始或者上一次 yeild 结束的位置继续向下执行,直到遇到新的 yeild 结束
  3. 每次返回的对象
    • value 的值是 yeild 后面的值或 return 后面的值(最后一次)
    • done 的值是 false / true (只有遇到 returntrue ,因为是最后一次)

举例:

如果 return undefined

function* generator(x) {
    console.log('x',x)
}
let itor = generator(100);
console.log('next',itor.next());  
//-> 输出x:100  ,next:{value:undefined,done:true}

image。png

如果有 return 其他

function* generator(x) {
    console.log(x);
    return x * 10;
}
let itor = generator(100);
console.log('next',itor.next());
//next:{value:1000,done:true}

image。png

执行一次 next ,函数体中的代码就执行了


function* generator(x) {
    return x * 10;
}
let itor = generator(100);
console.log('next',itor.next());
console.log('next',itor.next());

image。png


function* generator() {
    console.log('A');
    yield 10;
    console.log('B');
    yield 20;
    console.log('C');
    yield 30;
    console.log('D');
    return 100;
}
let itor = generator();
console.log(itor.next()); //->{value:10,done:false}
console.log(itor.next()); //->{value:20,done:false}
console.log(itor.next()); //->{value:30,done:false}
console.log(itor.next()); //->{value:100,done:true}
console.log(itor.next()); //->{value:undefined,done:true}

image。png

所以生成器函数返回的对象,通过迭代器规范,可以控制函数体当中代码执行的步骤

itor.return()

itor.return() 的作用:直接使遍历结束

function* generator() {
    console.log('A');
    yield 10;
    console.log('B');
    yield 20;
    console.log('C');
    yield 30;
    console.log('D');
    return 100;
}
let itor = generator();
console.log(itor.next()); //->{value:10,done:false}
console.log(itor.return('@return')); //->{value:'@return',done:true} 
console.log(itor.next()); 
console.log(itor.next()); 
console.log(itor.next()); 

itor.throw()

itor.throw() 的作用:直接抛出异常信息,没有返回结果,下面代码也不会再执行了

function* generator() {
    console.log('A');
    yield 10;
    console.log('B');
    yield 20;
    console.log('C');
    return 30;
}
let itor = generator();
console.log(itor.next()); //->{value:10,done:false}
console.log(itor.return('@return')); //->{value:"@return",done:true} 直接控制遍历结束
console.log(itor.throw('@throw')); //直接抛出异常信息,没有返回结果,下面代码也不会再执行了
console.log(itor.next()); //->{value:undefined,done:true}

image。png

itor.next()

如果 itor.next() 传值:

  1. 第一次传递的值没有用
  2. 之后,每一次执行 next 的传递的值,作为上一次 yeild 的返回值处理
function* generator() {
    let x1 = yield 10;
    console.log(x1);
    let x2 = yield 20;
    console.log(x2);
    return 30;
}
let itor = generator();
itor.next('@1'); //第一次传递的值没有用
itor.next('@2'); //每一次执行next的传递的值,是作为上一次yeild的返回值处理的
itor.next('@3'); 

image。png

Generator函数嵌套

yield 不加 *

function* generator1() {
    yield 10;
    yield 20;
}

function* generator2() {
    yield 10;
    yield generator1();
    yield 20;
}
let itor = generator2();
console.log(itor.next()); //value:10  done:false
console.log(itor.next()); //value:itor->generator1 done:false
console.log(itor.next()); //value:20  done:false
console.log(itor.next()); //value:undefined done:true 

image。png

yeild 后面加 * :如果 后面跟着一个新的 itor ,后期执行到这的时候,会进入到新的generator中执行

function* generator1() {
    yield 10;
    yield 20;
}

function* generator2() {
    yield 10;
    yield* generator1(); //yeild* 后面跟着一个新的itor,后期执行到这的时候,会进入到新的generator中执行
    yield 20;
}
let itor = generator2();
console.log(itor.next()); //value:10  done:false
console.log(itor.next()); //value:10 done:false
console.log(itor.next()); //value:20  done:false
console.log(itor.next()); //value:20 done:false
console.log(itor.next()); //value:undefined done:true 

image。png

async / await 原理

需求:

const query = interval => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(interval);
        }, interval);
    });
};

需求:串行请求。有三个请求,所用时间分别是1000/2000/3000,而且实现的需要时“串行”(第一个请求成功,再发第二个请求,第二个请求成功,再发第三个请求 ->都成功需要的总时间是6000ms)

使用Promise的 then 链机制:

query(1000).then(result => {
    console.log( ` 第一个请求成功,结果是:${result} ` );
    return query(2000);
}).then(result => {
    console.log( ` 第二个请求成功,结果是:${result} ` );
    return query(3000);
}).then(result => {
    console.log( ` 第三个请求成功,结果是:${result} ` );
}); 

另一个思路:使用Generator函数实现:

function* generator() {
    let result;
    result = yield query(1000);
    console.log( ` 第一个请求成功,结果是:${result} ` );

    result = yield query(2000);
    console.log( ` 第二个请求成功,结果是:${result} ` );

    result = yield query(3000);
    console.log( ` 第三个请求成功,结果是:${result} ` );
}
let itor = generator();
// console.log(itor.next()); //value:promise  done:false
itor.next().value.then(result => {
    itor.next(result).value.then(result => {
        itor.next(result).value.then(result => {
            itor.next(result);
        });
    });
}); 

把上次一的Promise的实例的返回结果拿到,然后传入 next ,最后成为函数体中的上一个 yelid 的返回值

image。png

上面展现了一下Generator函数实现串行原理,这样的好处是Generator函数体中的串行代码比较清晰。但是我们现实写代码完成业务当中,不知道到底要执行多少次 next ,所以我们需要一个特殊的循环逻辑来进行处理。这样我们只需要只写函数体中的代码,即可实现串行。下面我们来实现一下

原理:创建一个asyncfunction,传入Generator函数,然后在asyncfunction中得到Generator函数返回的迭代器,然后利用迭代器机制,循环执行 next (或者递归执行),直到全部执行完 next 为止。

Generator函数+递归实现:

const query = interval => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(interval);
        }, interval);
    });
};
function isPromise(x) {
    if (x == null) return false;
    if (/^(object|function)$/i.test(typeof x)) {
        if (typeof x.then === "function") {
            return true;
        }
    }
    return false;
}
// 依次去执行生成器函数中每一个操作:itor.next()  co.js
//   + generator:要处理的生成器函数
//   + params:存储给生成器函数传递的实参信息
function AsyncFunction(generator, ...params) {
    let itor = generator(...params);
        const next = x => {
            let {value,done} = itor.next(x);
            if (done) {
                return;
            }
            if (!isPromise(value)) value = Promise.resolve(value);//让其可以支持yield 1000这种类型的语句
            value.then(result => next(result))//成功之后让他走下一个迭代器的next方法,并且把值传给上一个yield
                .catch(reason => itor.throw(reason));//如果某个请求失败,不往下走,抛出异常即可
        };
        next();
}

AsyncFunction(function* generator(x, y) {
    console.log(x, y);
    let result = yield query(1000);
    console.log( ` 第一个请求成功,结果是:${result} ` );

    result = yield query(2000);
    console.log( ` 第二个请求成功,结果是:${result} ` );

    result = yield query(3000);
    console.log( ` 第三个请求成功,结果是:${result} ` );
})

这样处理之后就不需要手动一个个去写 next 了,不管有多少个 yield ,都可以执行 image。png

如果我想当整个串行流程都结束,AsyncFunction也返回一个Promise实例,用来表示流程结束:Generator函数+promise实现

function isPromise(x) {
    if (x == null) return false;
    if (/^(object|function)$/i.test(typeof x)) {
        if (typeof x.then === "function") {
            return true;
        }
    }
    return false;
}
function AsyncFunction(generator, ...params) {
    return new Promise(resolve => {
        let itor = generator(...params);
        const next = x => {
            let {value,done} = itor.next(x);
            if (done) {
                resolve(value);//把最后一次value值传给他
                return;
            }
            if (!isPromise(value)) value = Promise.resolve(value);
            value.then(result => next(result))
                .catch(reason => itor.throw(reason));
        };
        next();
    });
}

AsyncFunction(function* generator(x, y) {
    console.log(x, y);
    let result = yield query(1000);
    console.log( ` 第一个请求成功,结果是:${result} ` );

    result = yield query(2000);
    console.log( ` 第二个请求成功,结果是:${result} ` );

    result = yield query(3000);
    console.log( ` 第三个请求成功,结果是:${result} ` );
}, 100, 200).then(() => {
    // generator处理完成,执行这个操作
    console.log('全部处理完成');
});

image.png

以上借鉴了co.js库的实现原理,co.js是基于es6的Generator函数实现的,相当于一Generator函数的自动执行器

上面就是 async / await 的实现原理, async / await 是 Generator函数+promise 的语法糖

使用 async / await 书写如下:

(async () => {
    let result = await query(1000);
    console.log( ` 第一个请求成功,结果是:${result} ` );

    result = await query(2000);
    console.log( ` 第二个请求成功,结果是:${result} ` );

    result = await query(3000);
    console.log( ` 第三个请求成功,结果是:${result} ` );
})();