es6 之 Generator(二)

本文探讨了ES6中Generator函数的应用,特别是结合Thunk函数处理异步编程的方式。介绍了传统异步方法的不足之处,并详细解释了Thunk函数的原理及其实现。最后展示了如何利用Generator和Thunk函数改善异步编程的可读性和可维护性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

es6 之 Generator(二)

Generator的异步应用

包括thunk和柯里化的一些知识

一、传统的异步方法

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise对象
1.什么是异步?

异步和同步相对,指的是,代码不是连续执行。可能执行到片段A,因为A是一个特别耗时的工作,此时主程序需要暂时挂起片段A,先执行下面的片段B,
等到片段A的代码执行完毕,主程序再回过头来处理片段A的结果。
异步编程其实在一定程度上,大大提高了程序的运行效率。众所周知,Javascript是”单线程”的,因此只有异步编程才能够它满足繁重的计算需求,相比于
多线程同步,各有各的优点。脚本语言通常选择单线程异步。想要深入理解Javascript的运行机制,还是推荐阮一峰老师<<JavaScript 运行机制详解:再谈Event Loop>>的文章。

2.回调函数

如果说主程序是一个行军长队,有两个侦察兵A、B出去探风。同步编程就是行军长队停下来等待A探风回来,保证原来的行军顺序。
异步编程呢,就是行军长队按照原来的速度前进,侦察兵B在探风结束后,打了个电话回来,告诉部队前方情况如何。此时部队再根据当前情况进行调整策略。
这个电话 就可以称作为 “回调函数(callback)”

二、Thunk函数

所有的文章,介绍Generator函数都一定会带上Thunk函数。他们仿佛一对好基友,至于为啥thunk函数如此重要,我也很懵逼啊,我们一起往下看吧….TnT

从阮一峰老师的文章中,可以知道,thunk函数真的有点年头啊,据说在很久很久以前,几个研究计算机科学的老头坐在一起讨论,函数参数的问题,例如下面的例子。

    function foo(a){
        return a * 2;
    }
    var x = 10;
    foo(x + 1);
  • 1.计算机在处理以下代码的时候,究竟是应该先把(x+1)算出来得到11,再把11带入函数foo。这种方式被称为”传值调用“,C语言就采用这种方式。这种方式看起来感觉更加简单,
  • 2.还是应该把(x+1)直接带入函数foo。得到式子(x + 1)*2,然后再取到x的值进行计算
    对于这个例子,我们似乎不能够得到一个明确的答案,究竟哪种方式更好?

事实上,似乎”传值调用”除了实现更简单一些外,往往会造成一些性能损失。

    function foo(a){
        return 2;
    }
    var x = 2;
    foo(Math.pow(x, 100));

上面这个例子,就能够很好的说明这点,函数foo里面根本没有使用到参数a,但是这段代码通常是计算机的噩梦。
——无缘无故算了x的100次幂。对于一个比较复杂的表达式作为参数,函数体内根本没有用到这个参数,实际上是有很大的性能损失的。
所以有许多科学家赞成第二种方式”传名调用”。也就有了我们今天的Thunk函数。

    function thunk(){
        return x + 1;
    }
    foo(thunk);
    function foo(a){
        return a() + 2;
    }

以上js代码简单的实现了一个thunk函数,正好能体现出”传名调用”的原理。就是将参数放到一个临时的函数中,再将临时函数传入目标函数中去。
这个函数就叫做 “Thunk“函数。

javascript中的Thunk函数

在javascript中,thunk函数的作用其实就是,将多参数函数转化为单一参数为回调函数的函数。好长的一句话,可能有点绕。
说白了他就是一种简化代码的手段,减少重复代码,降低代码的冗余度。把一些经常用到的参数配置进去,之后再使用这个函数的时候就不用写那些默认项了,类似于记住密码。嗯..

来看一个thunk函数的简单实现
在这里比较推荐es6的写法,比较清楚

    // es6
    const Thunk = function(fn) {
        return function (...args) {
            return function (callback) {
                return fn.call(this, ...args, callback);
            }
        };
    };
    // es5
    var Thunk = function(fn){
        return function (){
            var args = Array.prototype.slice.call(arguments);
            return function (callback){
                args.push(callback);
                return fn.apply(this, args);
            }
        };
    };

这是nodeJS中的一个方法

    fs.readFile('data1.json', 'utf-8', (err, data) => {
        // 获取文件内容
    })

经过thunk的洗礼,变成了这个样子

    const readFileThunk = Thunk(fs.readFile);
    readFileThunk('data1.json', 'utf-8')(function(err, data){
        // ...
    })

乍一看有一种变复杂的感觉,其实这个感觉没错。对于个体来说,他确实是变复杂了呃。而且他也等于这样一段代码。

    const readFileBind = fs.readFile.bind(null, 'data1.json', 'utf-8');
    readFileBind(function(err, data){
        // ...
    })

后来tm发现一件很无语的事情…原来bind方法其实就是函数柯里化的一种实现。TnT,还顺带着能够绑定一下作用域,那还要thunk干啥…….可能是thunk方法更有针对性一些吧
附带一篇文章来介绍bind方法和柯里化——<

    function square(i) {
        return i * i;
    }

    function dubble(i) {
        return i *= 2;
    }

    function map(handeler, list) {
        return list.map(handeler);
    }

    // 数组的每一项平方
    map(square, [1, 2, 3, 4, 5]);
    map(square, [6, 7, 8, 9, 10]);
    map(square, [10, 20, 30, 40, 50]);
    // ......

    // 数组的每一项加倍
    map(dubble, [1, 2, 3, 4, 5]);
    map(dubble, [6, 7, 8, 9, 10]);
    map(dubble, [10, 20, 30, 40, 50]);

这个是正常的状态。
柯里化完了之后呢。。是这样

    var mapSQ = currying(map, square);
    mapSQ([1, 2, 3, 4, 5]);
    mapSQ([6, 7, 8, 9, 10]);
    mapSQ([10, 20, 30, 40, 50]);
    // ......

    var mapDB = currying(map, dubble);
    mapDB([1, 2, 3, 4, 5]);
    mapDB([6, 7, 8, 9, 10]);
    mapDB([10, 20, 30, 40, 50]);

就不用每次都写square和dubble方法啦。。。少写了总共72个字母呢
实际语义没有什么变化,引用大佬的原话

我们缩小了函数的适用范围,但同时提高函数的适性

这些差不多就是thunk函数带来的好处,之前是如何喜欢bind的,现在就如何喜欢thunk把

总结一下thunk函数有几个特征:
1. 1.只有一个参数是回调函数,回调函数的第一个参数是”err”,第二个参数是”data”
2. 2.thunk函数和bind都是是柯里化的子集,具体什么是柯里化可以看看张鑫旭老师的这篇文章,还蛮有趣的…呵呵

在生成环境中,我们通常使用thunkify这个库
关于柯里化,我们之后在另一篇文章中再一起探讨一下。

终于,感觉总算把thunk函数介绍的七七八八了,接下来再回到我们今天的主角——————
Generator

三、thunk函数在Generator函数中的的应用

由于异步编程的弊端,我们现在要做的是尽量将异步编程同步化, 使异步编程拥有同步那样清晰的语义,和可维护性。

最最直接、迫切的需求,就是能够像同步那样,直接通过(=)赋值语句,来拿到异步函数的返回值。像下面这样:

    const readFileThunk = thunkify(fs.readFile)
    const gen = function* () {
        const r1 = yield readFileThunk('data1.json')
        console.log(r1)
        const r2 = yield readFileThunk('data2.json')
        console.log(r2)
    }

以上代码除了有(yield)关键字,语义非常清晰。我们看起来貌似是在一个”顺序”的流程下,分别拿到(r1)、(r2)作为两次读取文件的结果。啊….太舒服了。
这是一个熟悉同步编程的程序员都能看懂的代码,完全不要动脑筋,没有一层一层的回调函数。不过别高兴的太早,上述代码还需要个发动机,他才能run起来。

纯手动的驱动方式
    const g = gen();
    g.next().value((err, data1) => {
        g.next(data1).value((err, data2) => {
            g.next(data2)
        })
    })

还是得通过回调函数,现在突然感觉,js的整个异步模型,就是基于回调函数的。
- 执行三次next方法,第一次传参无用,第二次传入读取结果data1,第三次传入读取结果data2,天衣无缝,非常完美。
我们是不是可以把驱动函数封装一下,能够自动识别有几个yield,需要执行几次next方法。于是就有了下面这段代码:

封装run方法
    function run(gen){
        const g = gen();
        function next(err, data){
            const result = g.next(data);
            if(data.done) return;
            result.value(next);
        }
        next()
    }
    run(gen);

看懂这段代码还是要费一些劲的,只要明确一点,next方法返回的value是一个thunk函数,thunk函数只接受唯一的回调函数作为参数就好了。
回调函数真是一个神奇的东西,上下文的转移,全都靠他了,嘿嘿,我感觉这篇文章应该改个名————<<论回调函数(callback)对js异步模型的重要性>>

另外强调一点,thunk函数其实并不是Generator函数自动执行的唯一方案,只要能够做到”转移上下文“(移交程序的控制权)的方案都能够实现这一点。
Promise也可以哦。

安利一波co模块

大佬们频频提起的co模块,也是Generator全家桶的重要一员。只需要简单的两行

    var co = require('co');
    co(gen);

我们编写好的,Generator函数就能够跑起来啦。

    co(gen).then(function (){
      console.log('Generator 函数执行完成');
    });

co函数返回一个Promise对象,因此可以用then方法添加回调函数。

….此处占个空,来日回来补上基于Promise的实现

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值