1. 首页
  2. 前端进阶
  3. ECMAScript6

第3篇:状态机之细说Async专题

Generator简单讲就是一个状态机。但它和Promise不一样,它可以维持无限个状态,并且提出它的初衷并不是为了解决异步编程的某些问题。

一个线程一次只能做一件任务,并且任务与任务之间不能间断。而Generator开了挂,它可以暂停手头的任务,先干别的,然后在恰当的时机手动切换回来。

这是一种纤程或者协程的概念,相比线程切换更加轻量化的切换方式。

往期:

  1. 第1篇:事件循环之细说Async专题
  2. 第2篇:迟到的承诺之细说Async专题
  3. 第3篇:状态机之细说Async专题
  4. 第4篇:也许是终极异步解决方案之细说Async专题

Iterator

在讲Generator之前,我们要先和Iterator遍历器打个照面。

Iterator对象是一个指针对象,它是一种类似于单向链表的数据结构。JavaScript通过Iterator对象来统一数组和类数组的遍历方式。

const arr = [1, 2, 3];
const iteratorConstructor = arr[Symbol.iterator];
console.log(iteratorConstructor);

// ƒ values() { [native code] }
const obj = { a: 1, b: 2, c: 3 };
const iteratorConstructor = obj[Symbol.iterator];
console.log(iteratorConstructor);

// undefined
const set = new Set([1, 2, 3]);
const iteratorConstructor = set[Symbol.iterator];
console.log(iteratorConstructor);

// ƒ values() { [native code] }

我们已经见到了Iterator对象的构造器,它藏在Symbol.iterator下面。接下来我们生成一个Iterator对象来了解它的工作方式吧。

const arr = [1, 2, 3];
const it = arr[Symbol.iterator]();

console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }
console.log(it.next()); // { value: undefined, done: true }

既然它是一个指针对象,调用next()的意思就是把指针往后挪一位。挪到最后一位,再往后挪,它就会一直重复我已经到头了,只能给你一个空值

Generator

Generator是一个生成器,它生成的到底是什么呢?

对咯,他生成的就是一个Iterator对象。

function *gen() {
    yield 1;
    yield 2;
    return 3;
}

const it = gen();

console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }
console.log(it.next()); // { value: undefined, done: true }

Generator有什么意义呢?普通函数的执行会形成一个调用栈,入栈和出栈是一口气完成的。而Generator必须得手动调用next()才能往下执行,相当于把执行的控制权从引擎交给了开发者。

所以Generator解决的是流程控制的问题。

它可以在执行过程暂时中断,先执行别的程序,但是它的执行上下文并没有销毁,仍然可以在需要的时候切换回来,继续往下执行。

最重要的优势在于,它看起来是同步的语法,但是却可以异步执行。

yield

对于一个Generator函数来说,什么时候该暂停呢?就是在碰到yield关键字的时候。

function *gen() {
    console.log('a');
    yield 13 * 15;
    console.log('b');
    yield 15 - 13;
    console.log('c');
    return 3;
}

const it = gen();

看上面的例子,第一次调用it.next()的时候,碰到了第一个yield关键字,然后开始计算yield后面表达式的值,然后这个值就成了it.next()返回值中value的值,然后停在这。这一步会打印a,但不会打印b

以此类推。return的值作为最后一个状态传递出去,然后返回值的done属性就变成true,一旦它变成true,之后继续执行的返回值都是没有意义的。

这里面有一个状态传递的过程。yield把它暂停之前获得的状态传递给执行器。

那么有没有可能执行器传递状态给状态机内部呢?

function *gen() {
    const a = yield 1;
    console.log(a);
    const b = yield 2;
    console.log(b);
    return 3;
}

const it = gen();

当然是可以的。

默认情况下,第二次执行的时候变量a的打印结果是undefined,因为yield关键字就没有返回值。

但是如果给next()传递参数,这个参数就会作为上一个yield的返回值。

it.next('biu');

别急,第一次执行没有所谓的上一个yield,所以这个参数是没有意义的。

it.next('piu');

// 打印 piu。这个 piu 是 console.log(a) 打印出来的。

第二次执行就不同了。a变量接收到了next()传递进去的参数。

这有什么用?如果能在执行过程中给状态机传值,我们就可以改变状态机的执行条件。你可以发现,Generator是可以实现值的双向传递的。

为什么要作为上一个yield的返回值?你想啊,作为上一个yield的返回值,才能改变当前代码的执行条件,这样才有价值不是嘛。这地方有点绕,仔细想一想。

自动执行

好吧,既然引擎把Generator的控制权交给了开发者,那我们就要探索出一种方法,让Generator的遍历器对象可以自动执行。

function* gen() {
    yield 1;
    yield 2;
    return 3;
}

function run(gen) {
    const it = gen();
    let state = { done: false };
    while (!state.done) {
        state = it.next();
        console.log(state);
    }
}

run(gen);

不错,竟然这么简单。

但想想我们是来干什么的,我们是来探讨JavaScript异步的呀。这个简陋的run函数能够执行异步操作吗?

function fetchByName(name) {
    const url = `https://api.github.com/users/${name}/repos`;
    fetch(url).then(res => res.json()).then(res => console.log(res));
}

function *gen() {
    yield fetchByName('veedrin');
    yield fetchByName('tj');
}

function run(gen) {
    const it = gen();
    let state = { done: false };
    while (!state.done) {
        state = it.next();
    }
}

run(gen);

事实证明,Generator会把fetchByName当做一个同步函数来执行,没等请求触发回调,它已经将指针指向了下一个yield。我们的目的是让上一个异步任务完成以后才开始下一个异步任务,显然这种方式做不到。

我们已经让Generator自动化了,但是在面对异步任务的时候,交还控制权的时机依然不对。

什么才是正确的时机呢?

在回调中交还控制权

哪个时间点表明某个异步任务已经完成?当然是在回调中咯。

我们来拆解一下思路。

  • 首先我们要把异步任务的其他参数和回调参数拆分开来,因为我们需要单独在回调中扣一下扳机。
  • 然后yield asyncTask()的返回值得是一个函数,它接受异步任务的回调作为参数。因为Generator只有yield的返回值是暴露在外面的,方便我们控制。
  • 最后在回调中移动指针。
function thunkify(fn) {
    return (...args) => {
        return (done) => {
            args.push(done);
            fn(...args);
        }
    }
}

这就是把异步任务的其他参数和回调参数拆分开来的法宝。是不是很简单?它通过两层闭包将原过程变成三次函数调用,第一次传入原函数,第二次传入回调之前的参数,第三次传入回调,并在最里一层闭包中又把参数整合起来传入原函数。

是的,这就是大名鼎鼎的thunkify

以下是暖男版。

function thunkify(fn) {
    return (...args) => {
        return (done) => {
            let called = false;
            args.push((...innerArgs) => {
                if (called) return;
                called = true;
                done(...innerArgs);
            });
            try {
                fn(...args);
            } catch (err) {
                done(err);
            }
        }
    }
}

宝刀已经有了,咱们去屠龙吧。

const fs = require('fs');
const thunkify = require('./thunkify');

const readFileThunk = thunkify(fs.readFile);

function *gen() {
    const valueA = yield readFileThunk('/Users/veedrin/a.md');
    console.log('a.md 的内容是:\n', valueA.toString());
    const valueB = yield readFileThunk('/Users/veedrin/b.md');
    console.log('b.md 的内容是:\n', valueB.toString());
}

function run(gen) {
    const it = gen();
    const state1 = it.next();
    state1.value((err, data) => {
        if (err) throw err;
        const state2 = it.next(data);
        state2.value((err, data) => {
            if (err) throw err;
            it.next(data);
        });
    });
}

run(gen);

卧槽,老夫宝刀都提起来了,你让我切豆腐?

这他妈不就是把回调嵌套提到外面来了么!我为啥还要用Generator,感觉默认的回调嵌套挺好的呀,有一种黑洞般的简洁和性感…

别急,这只是Thunk解决方案的PPT版本,接下来咱们真的要造车并开车了哟,此处@贾跃亭。

const fs = require('fs');
const thunkify = require('./thunkify');

const readFileThunk = thunkify(fs.readFile);

function *gen() {
    const valueA = yield readFileThunk('/Users/veedrin/a.md');
    console.log('a.md 的内容是:\n', valueA.toString());
    const valueB = yield readFileThunk('/Users/veedrin/b.md');
    console.log('b.md 的内容是:\n', valueB.toString());
}

function run(gen) {
    const it = gen();
    function next(err, data) {
        const state = it.next(data);
        if (state.done) return;
        state.value(next);
    }
    next();
}

run(gen);

我们完全可以把回调函数抽象出来,每移动一次指针就递归一次,然后在回调函数内部加一个停止递归的逻辑,一个通用版的run函数就写好啦。上例中的next()其实就是callback()呢。

在Promise中交还控制权

处理异步操作除了回调之外,我们还有异步容器Promise。

和在回调中交还控制权差不多,于Promise中,我们在then函数的函数参数中扣动扳机。

我们来看看威震海内的co

function co(gen) {
    const it = gen();
    const state = it.next();
    function next(state) {
        if (state.done) return;
        state.value.then(res => {
            const state = it.next(res);
            next(state);
        });
    }
    next(state);
}

其实也不复杂,就是在then函数的回调中(其实也是回调啦)移动Generator的指针,然后递归调用,继续移动指针。当然,需要有一个停止递归的逻辑。

以下是暖男版。

function isObject(value) {
    return Object === value.constructor;
}

function isGenerator(obj) {
    return typeof obj.next === 'function' && typeof obj.throw === 'function';
}

function isGeneratorFunction(obj) {
    const constructor = obj.constructor;
    if (!constructor) return false;
    if (constructor.name === GeneratorFunction || constructor.displayName === 'GeneratorFunction') return true;
    return isGenerator(constructor.prototype);
}

function isPromise(obj) {
    return typeof obj.then === 'function';
}

function toPromise(obj) {
    if (!obj) return obj;
    if (isPromise(obj)) return obj;
    if (isGenerator(obj) || isGeneratorFunction(obj)) {
        return co.call(this, obj);
    }
    if (typeof obj === 'function') {
        return thunkToPromise.call(this, obj);
    }
    if (Array.isArray(obj)) {
        return arrayToPromise.call(this, obj);
    }
    if (isObject(obj)) {
        return objectToPromise.call(this, obj);
    }
    return obj;
}

function typeError(value) {
    return new TypeError(`You may only yield a function, promise, generator, array, or object, but the following object was passed: "${String(value)}"`);
}

function co(gen) {
    const ctx = this;
    return new Promise((resolve, reject) => {
        let it;
        if (typeof gen === 'function') {
            it = gen.call(ctx);
        }
        if (!it || typeof it.next !== 'function') {
            return resolve(it);
        }
        onFulfilled();
        function onFulfilled(res) {
            let ret;
            try {
                ret = it.next(res);
            } catch (err) {
                return reject(err);
            }
            next(ret);
        }

        function onRejected(res) {
            let ret;
            try {
                ret = it.throw(res);
            } catch (err) {
                return reject(err);
            }
            next(ret);
        }
        function next(ret) {
            if (ret.done) {
                return resolve(ret.value);
            }
            const value = toPromise.call(ctx, ret.value);
            if (value && isPromise(value)) {
                return value.then(onFulfilled, onRejected);
            }
            return onRejected(typeError(ret.value));
        }
    });
}

co是一个真正的异步解决方案,因为它暴露的接口足够简单。

import co from './co';

function fetchByName(name) {
    const url = `https://api.github.com/users/${name}/repos`;
    return fetch(url).then(res => res.json());
}

function *gen() {
    const value1 = yield fetchByName('veedrin');
    console.log(value1);
    const value2 = yield fetchByName('tj');
    console.log(value2);
}

co(gen);

直接把Generator函数传入co函数即可,太优雅了。
https://github.com/veedrin/horseshoe

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:第3篇:状态机之细说Async专题

链接:https://www.javascriptc.com/2941.html

« VS Code 1.41 发布!Web 版 VS Code 增强对 macOS or iPadOS 的支持
开发项目时,如何限制接口被多次点击调用»
Flutter 中文教程资源

相关推荐

QR code