js中异步处理的正确姿势:你想知道的都在这了

假如你已经知道了什么是异步,并且已经写过很多的异步代码。这篇文章主要介绍几种对异步代码的处理,即异步编码姿势:

  1. 回调函数;
  2. Promise;
  3. 迭代器、生成器;
  4. async/await。

重点在第3、4部分。

回调函数

这个没什么好说的,直接看一段代码:

const fs = require('fs');

fs.readFile('config.json', (err, data) => {
    if (err) {
        console.error(err);
    } else {
        console.log(data);
    }
});

后面部分都以该读取文件操作为例来讲解。

Promise

Promise就是为异步而生的,主要是为了解决所谓的回调地狱问题。Promise的三个状态:pendingfulfilledrejected

通常的写法:

const fs = require('fs');

const promise = new promise((resolve, reject) => {
    fs.readFile('config.json', (err, data) => {
        if (err) {
            reject(err);
        } else {
            resolve(data);
        }
    });
});

promise.then(data => {
    console.log(data);
}).catch(err => {
    console.error(err);
});

需要注意一点的是:new Promise()传入的函数会立即执行,thencatch中传入的函数才是异步执行的。

then方法何时执行?取决于两点:

  1. promise何时变成完成状态(fulfilled);
  2. 在异步队列中的位置。

迭代器、生成器

概念的理解

先理解两个概念:生成器是一个返回迭代器的函数;那么迭代器就是生成器执行后返回的结果(对象)。所以,生成器是函数,迭代器是对象(很容易弄混的两个概念)。

首先,生成器是一个函数,这是一个特殊的函数,函数定义如下:

// 这就是一个生成器(函数)
function *createIterator() {
    const a = yield 1;
    const b = yield a + 2;
    yield b + 3;
}

// 这就是一个迭代器(对象)
const iterator = createIterator();

// 注释部分是next方法执行的返回值
iterator.next();    // {value: 1, done: false}      执行完这句并没有给a赋值
iterator.next();    // {value: 3, done: false}      执行这句的时候才会给a赋值1
iterator.next(5);   // {value: 8, done: false}      执行这句的时候才会给b赋值5
iterator.next();    // {value: undefined, done: true}

异步的实现

看下面这段代码:

const fs = require('fs');

// 定义一个读取文件的函数,下面所有用到的地方均来自于此
function readFile(filename) {
    return new Promise((resolve, reject) => {
        fs.readFile(filename, (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

这是node.js中一个简单的读取文件的异步操作,因为用了Promise,所以正常的使用应该是这样的:

readFile('config.json').then(data => {
    console.log(data);
}).catch(err => {
    console.error(err);
})

其实这就是上面介绍的Promise对异步的处理。假如我们有这样一个想法,希望代码是这样的:

try {
    // 同步读取,避免回调
    const data = readFile('config.json');
    console.log(data);
} catch (err) {
    console.error(err);
}

我们知道,正常情况下,这段代码肯定不会如期执行,因为我们的data其实是一个promise对象。但是假如有这样一个容器,它能如期的执行我们上面的这段代码,我们只需要把代码丢进这个特殊的容器里。注意到没有,上面这段代码其实是一段同步的代码,通过同步的代码实现异步的操作,这似乎是一个很完美的想法,只是首先我们需要有这样的一个容器。

运行容器

运行异步代码的容器:

// 运行生成器函数的一个容器
// 参数必须是一个生成器
function run(gen) {
    // 创建迭代器
    const task = gen();
    // 开始执行
    let result = task.next();
    
    (function step() {
        if (!result.done) {
            // 用Promise处理
            // 解释:无论result.value本身是不是promise对象,都会作为一个promise对象来异步处理
            const promise = Promise.resolve(result.value);
            promise.then(value => {
                // 把本次执行的结果返回
                // 也就是语句 const value = yield func(); 的返回值
                result = task.next(value);
                // 继续
                step();
            }).catch(err => {
                result = task.throw(err);
                // 继续
                step();
            })
        }
    }());
}

现在,我们有了这样的一个容器run,把读取文件的那段“同步”代码丢进这个容器里:

run(function *() {
    try {
        // 注意这里多了一个 yield
        const data = yield readFile('config.json');
        console.log(data);
    } catch (err) {
        console.error(err);
    }
});

现在,我们的代码便能如期的执行了!

简单的解释一下,我们将读取文件的这段“同步”代码包装成了一个生成器函数,然后传给run函数去处理。在run函数内部首先执行这个生成器函数并返回了一个迭代器对象,当第一次执行let result = task.next()的时候,执行的就是readFile('config.json')这句,而这个函数会异步去读取文件并立马返回一个promise对象。所以result的值就是{value: promise, done: false}。由于result.value本身是一个promise对象,所以执行const promise = Promise.resolve(result.value)这句的时候返回的仍然是传入的那个promise对象(也就是result.value)。当读取文件操作完成之后,才会执行thencatch中的代码,在thenresult = task.next(value)这句代码就会让之前卡住的yield readFile('config.json')往后执行,也就是data接收到value的值,然后打印出来。

如果你对迭代器/生成器这块不熟的话,理解起来可能比较痛苦,建议先去补补这方面的知识。

其实,github上已经有人提供了run这样的容器,叫做co。所以,我们只要把注意力放在容器中的生成器里面的代码上面就可以了。

注意点

run容器中yield之后所有的代码都已经是异步执行的了,所以不管yield后面跟的是不是一个promise对象,后面的代码都是异步的。看一个简单的例子:

const add = (a, b) => a + b;

run(function *(){
    console.log('run 开始执行');
    const sum = yield add(1, 2);
    console.log('sum:', sum);
});

console.log('结束了!');

这段代码中yield后面跟的是一个add函数,函数的返回值是一个数值3,并非一个promise对象或其他异步操作。但这段代码执行的结果是:

// run 开始执行
// 结束了!
// sum: 3

哪怕yield后面跟的不是一个函数,直接是一个数值3,执行的结果也是跟上面一样。

为什么?

注意在run中,我们是通过Promise.resolve(result.value)来处理的,result.value就是yield后面跟的东西。对Promise比较熟悉的话应该知道,Promise.resolve()传入的参数如果是一个promise对象,那么直接返回这个对象,如果传入的不是一个promise对象,那么会返回一个新创建的promise对象,并且是完成状态。也就是说Promise.resolve无论如何都会返回一个promise对象,而只有执行了then方法中的result = task.next(value)这句代码之后,yield之后的代码才会继续执行,(sum也才会接收到传过来的值)。因为result = task.next(value)是异步执行的,所以yield之后的代码自然就是异步的了。

async/await

如果你看懂了上面的介绍,那么理解async/await就很轻松了;如果你觉得上面的写法很操蛋,那么下面的写法就是一个字爽。

异步实现

先直接上代码:

async function run() {
    try {
        // 这里的 readFile 是上面定义的函数
        const data = await readFile('config.json');
        console.log(data);
    } catch(err) {
        console.error(err);
    }
}

run();

就是这么简单!一眼看上去,跟上面第3部分的代码有些相像,只是yield变成了await*变成了async,外面多了一个容器run

再对比代码的执行顺序:

const add = (a, b) => a + b;

async function run(){
    console.log('run 开始执行');
    const sum = await add(1, 2);
    console.log('sum:', sum);
}

run();
console.log('结束了!');

执行结果:

// run 开始执行
// 结束了!
// sum: 3

有木有很惊讶?就连执行的顺序都跟yield实现的方式一样。而且再也不用管什么容器了,看上去更加直观。这就是所谓的用同步的代码方式去写异步的操作,借用一下老外的说法:让那些烦人的回调见鬼去吧。

虽然这里不用管什么运行容器之类的东西了,但是理解它实现的原理还是很重要的。我不知道async/await是否可以理解成yield实现异步的语法糖,只不过async/await纳入ES7的标准了,而yield的写法是我们自己实现的(比如运行容器run就是我们自己封装的,你也可以根据需求扩展出更强大的功能来)。

最后

感谢阅读和分享!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容