Generator 的异步使用技巧

正式介绍下,Generator 配合异步的操作,异步方式变成同步方式,神奇!这是什么魔法代码!

异步

JavaScript 界,异步是获取数据的主旋律,大概有以下几种方式:

  • callback 回调
  • Promise
  • event 事件

简单说说以上 3 种模式:

回调

将需要执行的函数传入异步调用内,在异步调用结束后,主动执行传入的函数。

function doSomeAsync(callback) {
    let data = {/* 数据 */};
    setTimeout(() => {
        callback(data);
    });
}

doSomeAsync((data) => {
    // do some thing
    doSomeAsync((data) => {
        // do some thing
    });
});

但试想一下,如果说回调之中还有回调,那么将会形成一个类似金字塔的结构(回调地狱),代码将变难以维护。

Promise

出现了回调地狱,我们得解决,这就出现了 Promise,将上面回调的例子改写成 Promise 的形式。

let promise = () => {
    return new Promise((resolve, reject) => {
        let data = {/* 数据 */}
        setTimeout(() => {
            resolve();
        })
    })
};

promise().then(res => {
    // do some thing
    return promise()
}).then(res => {
    // do some thing
    return promise()
})

虽然这样写代码上看起来和回调的形式差不多,但如果在一个异步操作之后还要继续进行异步操作的话,在调用一次 then 方法即可。

事件

事件是 发布/订阅 模式的实现,具体的过程如下:

  1. 在异步操作结束前,定义操作结束后的事件名以及对应的事件处理函数。
  2. 异步操作结束时,触发相应事件。

关于事件类该如何实现,可以参考我自己实现的一个事件类 Event

ok 把上面的例子改一改:

let event = new Event();
event.$on('asyncEnd', () => {
    // do some thing
})

setTimeout(() => {
    // do some thing
    event.$emit('asyncEnd');
})

ok 主流的异步解决方式都已经说完,我们来看看 Generator 函数在异步中的应用。

Generator 的异步使用

先来个 Generator 函数

function* gen(x) {
  let y = yield x + 2;
  console.log(y);
  
  let z = yield y + 3;
  console.log(z);
  return z
}

然后调用

let g = gen(1)
gen.next()              // {value: 3, done: false}
gen.next(3)             // {value: 6, done: false}
gen.next(6)             // {value: 6, done: true}

注意:next 中传的参数即为上一次调用 next 返回值中的 value 也就是 yield 表达式的返回值。如果对于 next 参数的使用有疑问,请参考 Generator 中的 next

接下来,我们来写一个函数让 next 方法自动执行。

function autoGen(gen){
    let genReturn = {
        value: undefined, 
        done: false
    };
    do {
        genReturn = gen.next(genReturn.value);
    } while(!genReturn.done)
}

autoGen(gen(1))
// 3
// 6

这是同步模式下 Generator 函数自动化执行的实现,那我们现在来改一改,将 yield 后的表达式改为一个异步调用( Promise )。

function createPromise(num){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num);
        })
    });
};

function* genPromise(x) {
  let y = yield createPromise(x + 2);
  console.log(y);
  
  let z = yield createPromise(y + 3);
  console.log(z);
  return z;
}

按照之前的调用形式,得到的 value 将会是一个 Promise,而且对于变量 yz 来说,更想要的应该是异步结束后的结果,那我们调整一下自动执行函数的内部逻辑,使得 next 方法中的参数是异步结束后的结果。

function autoGenForPromise(gen){
    // 获得第一个 promise
    let promiseChain = gen.next().value;

    function next(){
        // promise.then 能拿到异步的结果
        // 所以将 next 方法放在 then 方法内执行
        promiseChain.then(res => {
            let innerReturn = gen.next(res);
            // 判断 Generator 函数是否已经执行完毕
            if(!innerReturn.done){
                // 获取下一个 promise
                promiseChain = innerReturn.value;
                // 递归调用
                next();
            }
        })
    };
    
    next();
}

autoGenForPromise(genPromise(1));
// 3
// 6

同样的只是在不断的调用 next 方法,区别只是同步模式下用循环,异步模式下是递归而已。

我们实现了基于 PromiseGenerator 函数的自动化,那么该考虑考虑,如何进行基于回调(callback)的自动化了。

具体如何实现的过程不做过多的解读,主要看代码

// 参数和回调分两个步骤传入异步中
function createCallback(num){
    return function(callback){
        setTimeout(() => {
            callback(num);
        })
    }
}

function* genCallback(x) {
  let y = yield createCallback(x + 2);
  console.log(y);
  
  let z = yield createCallback(y + 3);
  console.log(z);
  return z;
}

不同于 Promise 的代码,genCallback 生成的 Generator 函数调用 next 返回的 value 是一个需要执行的函数,函数执行后会进行真正的异步调用。

接着我们来让它实现异步自动化执行

function autoGenForCallback(gen){
    // 获取第一个 callback
    let nextResutl = gen.next();
    
    function next(){
        // 返回的 value 允许我们传入一个函数
        nextResutl.value((res) => {
           nextResutl = gen.next(res);
            // 判断是否结束
           if(!nextResutl.done){
                next();
            }
        })
    }
    
    next();
}

autoGenForCallback(genCallback(1));
// 3
// 6

至此,我们基本上实现了对异步形式的控制,不管是如何实现自动化的,其 Generator 函数大致是不会变的,都是如下的格式:

function* gen(x) {
  let y = yield;        // 一个表达式
  
  // 对于上一个 yield 的返回值进行的操作
  console.log(y);
  
  let z = yield;        // 一个表达式
  
  // 对于上一个 yield 的返回值进行的操作
  console.log(z);
  return z;
}

而我们之所以将不同的形式拆成不同的方法,其实也是为了方便理解,想象一下,如果我们在自动化函数中做了对不同形式的兼容,不就成了一个完美的 Generator 函数异步自动执行的方案了?

至于这个方案的具体代码,可以查看 npm 上一个叫 co 的模块,感兴趣的可以去了解下。

总结

有了 Generator 函数加上一个良好的自动化执行函数后,我们就可以像写同步代码那样写异步代码了,大体的结构就是上面说的那个样子。

function* gen(x) {
  let y = yield;            // 一个表达式
  
  // 对于上一个 yield 的返回值进行的操作
  console.log(y)
  let z = yield             // 一个表达式
  
  // 对于上一个 yield 的返回值进行的操作
  console.log(z)
  return z
}

autoGen(gen(/* initValue */))

其实这段代码像极了 ES7 中的 async/await ,只不过这里是 */yield/autoGen。其实在 ES7async/await 仅仅不过是一层语法糖罢了,其本质是 Promise 的使用,在 JavaScript 中,是不可能实现正真的同步,设计如此,而这些语法的实现,不得不说思维的伟大。

Read more

Gitlab 搭建

Gitlab 搭建

为什么? 想要自己搭建一个代码仓库无非是以下几点原因: 1. 公司内部项目 2. 自己的项目,但不适合在公网 3. 大部分的 git 仓库虽然有私有服务,但价格都不便宜,甚至不如一台云服务器来的便宜 配置及安装文档 Gitlab * 由于 gitlab 会用到 22 端口端口转发的化就走不了 git clone 的默认配置,且占用内存较高,不推荐使用 docker 进行部署; * 由于 gitlab 自带 nginx 默认情况下会与属主机的 nginx 冲突,因此推荐只使用 gitlab 自带的 nginx 进行端口转发; 最小化配置 # path /etc/gitlab/gitlab.rb external_url 'http://git.

By breeze
NPM 私服

NPM 私服

verdaccio 私服搭建,搭建过程中的一些问题以及解决: * docker compose 启动后,可能会报错:配置找不到,添加 config.yaml 文件到映射的 ./conf 目录即可,config.yaml 具体内容可在官网找到,下方也有最小化配置。 services: verdaccio: image: verdaccio/verdaccio container_name: 'verdaccio' networks: - node-network environment: - VERDACCIO_PORT=4873 - VERDACCIO_PUBLIC_URL=http://npm.demo.com ports: - '10001:4873'

By breeze