Silentdoer

导航

C# ES6 yield的实现原理和await/async实现原理

创建测试项目的方式:

1.创建一个空目录如es6test,cd到这个目录

2.npm init -y,可以看到会生成package.json文件

3.在es6test目录(项目根目录)下创建src目录,在该目录下创建test.js,里面写的是es6的语法;

4.安装babel转译工具:npm install babel-cli babel-preset-env -D

5.执行转译命令:npx babel src/ -d dist/ --presets=babel-preset-env【npx命令是npm自带的】

6.可以看到在dist目录下生成了转译后的test.js

 

我们先看es6里的是这样子:

//Generator函数
function* chain(){
    yield 'a';
    console.log('sss');
    yield 'b';
    yield 'c';
    return 'end';
    // 这两行代码不会执行
    console.log("sss附件打开")
    yield 99;
}
let exp=chain()
// exp的constructor的值是Function [GeneratorFunction] {}【在js里对象的constructor就类似object的具体类型。。】
console.log(typeof(exp))
//value是yield后面的值,done表示的是当前函数是否执行完毕
console.log(exp.next())//{value: "a", done: false}
console.log("附件打开");
// 注意,执行b的next的时候,才打印sss
console.log(exp.next())//{value: "b", done: false}
console.log(exp.next())//{value: "c", done: false}
console.log(exp.next())//{value: "end", done: true}

然后用babel将上面的代码生成es5是这样:

'use strict';

var _marked = /*#__PURE__*/regeneratorRuntime.mark(chain);

//Generator函数
function chain() {
    return regeneratorRuntime.wrap(function chain$(_context) {
        while (1) {
            switch (_context.prev = _context.next) {
                case 0:
                    _context.next = 2;
                    return 'a';

                case 2:
                    console.log('sss');
                    _context.next = 5;
                    return 'b';

                case 5:
                    _context.next = 7;
                    return 'c';

                case 7:
                    return _context.abrupt('return', 'end');

                case 8:
                case 'end':
                    return _context.stop();
            }
        }
    }, _marked, this);
}
var exp = chain();
//value是yield后面的值,done表示的是当前函数是否执行完毕
console.log(exp.next()); //{value: "a", done: false}
console.log("附件打开");
// 注意,执行b的next的时候,才打印sss
console.log(exp.next()); //{value: "b", done: false}
console.log(exp.next()); //{value: "c", done: false}
console.log(exp.next()); //{value: "end", done: true}

可以看到,编译器/运行时会对yield进行转换/打洞,将每个yield拆分到多个块里(yield行和之上行的代码[不包括上一次yield]会作为一个计算块);

这里其实regeneratorRuntime.wrap(...)返回的就类似C# IEnumerable<Object>一样的状态机/迭代器/生成器;

里面有context对象,context对象又记录了prev和next两个"游标",当执行next的时候(C#里的for xx in iter本质上也是执行了iter的next())会将自己的context作为参数调用chain$,然后根据游标值来看这次next是要执行哪个计算块;

上面自动生成的generator其实可以自己写一个简化版本的:

class GeneratorWrap {
    prev$ = null;
    next$ = 0;
    stoped$ = false;
    
    next() {
        while (!this.stoped$) {
            switch (this.prev$ = this.next$) {
                case 0:
                    this.next$ = 2;
                    return {value: 'a', done: this.stoped$}
                case 2:
                    this.next$ = 4;
                    return {value: 'b', done: this.stoped$}
                case 4:
                    this.stoped = true;
                    return {value: 'c', done: this.stoped$}
                default:
                    return {value: undefined, done: true}
            }
        }
        return {value: undefined, done: true}
    }
}

 

然后是C#的代码,无yield:

// See https://aka.ms/new-console-template for more information

IEnumerable<int> fibonaccis = Fibonacci(4);
foreach (var f in fibonaccis)
{
    Console.Write("{0} ", f);
}
//计算斐波拉契数据
IEnumerable<int> Fibonacci(int count)
{
  int p= 1;
  int c= 1;
  List<int> result = new List<int>();
  for (int i = 0; i < count; i++)
  {
    result.Add(p);
    Thread.Sleep(2000);
    int temp = p+ c;
    p= c;
    c= temp;
  }
  return result;
}

// yield: 1 1 2 3 Hello, World!Program+<<<Main>$>g__Fibonacci|0_0>d
// 无yield: 1 1 2 3 Hello, World!System.Collections.Generic.List`1[System.Int32]
Console.WriteLine("Hello, World!" + fibonaccis.GetType());

有yield:

// See https://aka.ms/new-console-template for more information

IEnumerable<int> fibonaccis = Fibonacci(4);
foreach (var f in fibonaccis)  // 每次in赋值给f时,都是执行了类似next获取下一个元素的方法,而这个next的方法就类似es5里的chain$
{
    Console.Write("{0} ", f);
}
//计算斐波拉契数据
IEnumerable<int> Fibonacci(int count)
{
  int p= 1;
  int c= 1;
  for (int i = 0; i < count; i++)
  {
    yield return p;
    Thread.Sleep(2000);
    int temp = p+ c;
    p= c;
    c= temp;
  }
}

// yield: 1 1 2 3 Hello, World!Program+<<<Main>$>g__Fibonacci|0_0>d
// 无yield: 1 1 2 3 Hello, World!System.Collections.Generic.List`1[System.Int32]
Console.WriteLine("Hello, World!" + fibonaccis.GetType());

可以看出有yield和无yield的IEnumerable<int>的实现类是不一样的,必须的不一样,因为fibonaccis提取下一个元素的方法(假设叫next())next在非yield里其实就类似从集合里取值,而yield的next方法则是内部又调用了一个类似es5里的chain$的方法来取值(会传context给chain$),即两种得到IEnumerable<int>对象的取值逻辑都不一样,自然其实现类是不会一样的;

这里yield最重要的实现方式之一就是编译器会自动对每个yield进行拆计算块,即第一个计算块是函数开始到第一个yield那行算一个计算块(这个是编译器/运行时会去拆,至于为什么它能拆的出来就涉及编译器/运行时实现太高深了),第二个计算块则是第一个yield后面的代码到第二个yield那行代码;第三个计算块则是第二个yield后面的代码到第三个yield那行,以此类推(每个计算块会做额外操作,参考es5里还有对context.next赋值的操作);

如果遇到了yield break (类似js的return),则这个计算块会添加对context终止的额外代码,会导致iter.hasNext()为false,从而不会再执行next()来去对下一个元素提取;

 

而async和await的原理的前提是基于在没有这两个关键字之前,是通过回调(Promise、Future)来实现的,即Promise本身就是一个状态机,提交任务是提交Promise对象,然后事件循环会拿出Promise对象来执行里面的function,执行完毕后再执行里面的回调函数:

let aa = (a promise obj);

async function fff() {
    console.log('aaa');
    let ee = await aa;
    console.log('bbb');
    // 假设其值是4
    console.log(ee);
    /*let oo = await otherAwait;
    console.log('fff');*/
}
fff();
console.log('ccc');

// 则最终输出为:
aaa
ccc
bbb
4

它转换为es5的回调方式为:

let aa = (a promise obj);

function fff() {
    console.log('aaa');
    aa.then(e => {
        console.log('bbb');
        console.log(e);
        // otherAwait.then(o => {console.log('fff')});
    });
}

fff();
console.log('ccc');

可以看到,await其实就是将await后面的代码做了then的改造;

如果是多个await,原理也是一样,先改造第一个await,然后第二个await肯定是第一个Promise对象的then里,然后再改造第二个await又是then(即then里有then)

 

C#的Task也是有类似的机制(当然它不像js一样底层就是事件循环机制),具体为new Task(() => {....}),这个就是可以理解为“js里的主函数”,然后还可以task.ContinueWith((task) => {..});,这里的就是Promise的then,伪代码为:

new Task(() => {
    let aa = new Task(() => {
        return 4;
    });
    
    fff();
    print('ccc');
    
    // 这里只是定义,不执行
    void fff() {
        print('aaa');
        aa.ContinueWith((e) => {
            print('bbb');
            print(e);
        }
    }
});

 

posted on 2022-11-06 22:01  Silentdoer  阅读(183)  评论(0编辑  收藏  举报