frontend9 / fe9-library

九部知识库
1.94k stars 138 forks source link

趣谈异步编程 #14

Open liyouu opened 6 years ago

liyouu commented 6 years ago

趣谈异步编程之“妈妈喊你回家吃饭”,来聊一聊 javascript 中常见的几种异步编程方式。

方式一:回调函数

小明饿了要吃饭,妈妈的饭要半个小时后才能做好,让小明先去读会儿书,饭好后再喊他。于是热腾腾的回调函数产生了:

function eat() {
    console.log('好的,我开动咯');
}

function cooking(callback) {
    console.log('妈妈认真做饭');
    setTimeout(function () {
      console.log('小明快过来,开饭啦');  
      callback();
    }, 30 * 60 * 1000);
}

function read() {
    console.log('小明假装正在读书');
}

cooking(eat);
read();

/* 执行顺序:
妈妈认真做饭
小明假装正在读书
小明快过来,开饭啦
好的,我开动咯
*/

回调函数简单直观,对于读书中的小明来说,妈妈 cooking 是一个异步任务,完成之后妈妈直接调用 eat,以此来通知小明吃饭,于是小明有饭吃了。在 promise 出现前的很长一段时间中,回调函数是异步编程的首选。

但坑猿的是,多层回调函数也很可能会将你带入 callback hell 【回调地狱】,以至于曾经一度流传【据统计,javascript 代码注释中的脏话是所有语言中最多的】。

方式二:事件

生活是什么?生活就是不断重复,于是很快小明又饿了。

但有追求的程序员拒绝一直重复,这次换了个玩法:

function eat() {
    console.log('妈妈敲门啦,该去吃饭啦');
}

function cooking() {
    console.log('妈妈认真做饭');
    setTimeout(function () {
      console.log('小明,出来吃饭啦');  
      cooking.$emit('done');
    }, 30 * 60 * 1000);
}

function read() {
    console.log('小明又假装正在读书');
    cooking.$on('done', eat);
}

cooking();
read();

/* 执行顺序:
妈妈认真做饭
小明又假装正在读书
小明,出来吃饭啦
妈妈敲门啦,该去吃饭啦
*/

可以看到,这次 eatcooking 分手了,cooking 再也看不到 eat 的身影。当妈妈 cooking 完后大喝一声 done !正在 read 的小明马上屁颠屁颠地跑过来了,为什么呢?因为小明读书的时候就一直竖着耳朵在听妈妈什么时候发出这声大喝。

在事件模型中,每个对象都是独立的个体,各自管理自己的状态,通过相互之间发送和接受消息来实现对象间通信。事件模型可以将代码格式从令人绝望的嵌套状转到优美的序列状,于是 jser 们可以从 callback hell 里爬出来了吗?既傻又天真。试想一下:如果真要你将一份多重嵌套的回调函数重构成事件模型模式,你会怎么做,你需要写多少行 $on$emit ,需要为多少个无聊的事件起名字,相信你只会更绝望。

事件模型是一个优秀的异步方案,但显然,他更擅长解构,不是为了解决 callback hell 而存在。

方式三:发布/订阅

生活不止重复,也有意外。

今天小明和妈妈吵架了,互相都不说话,于是冤大头爸爸上线了,负责担任 传话筒 一职:

function eat() {
    console.log('爸爸叫我去吃饭啦');
}

function cooking(){
    console.log('妈妈认真做饭');
    setTimeout(function () {
        console.log('孩子他爸,叫小明出来吃饭');
        Dad.publish("done");
    }, 30 * 60 * 1000);

}

function read() {
    console.log('小明依旧假装正在读书');
    Dad.subscribe('done', eat);
}

cooking();
read();

/* 执行顺序:
妈妈认真做饭
小明依旧假装正在读书
孩子他爸,叫小明出来吃饭
爸爸叫我去吃饭啦
*/

看完后智商在线的你可能会发现:这和事件模型有啥区别。。。

是的,它们没有本质区别,发布/订阅模式只是事件模型中比较高级的一种实现形式,加入爸爸这个 传话筒 后,所有的消息都由爸爸来传递、管理和跟踪。当勤奋的你代码量变得越来越大的时候,这个爸爸作用老大了。

方式四:Promise

Promise 是啥?

Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及其返回的值。

某种角度上,它既学习了回调函数的简单直观,又借鉴了事件模型的状态内聚。一个 Promise 只有三种状态,我们需要关心 fulfilledrejected ,分别用 thencatch 处理它们,于是回调地狱不见了,也不需要定义大堆事件,就这么简单,就这么神奇。

好吧,小明再次饿了,但这次我们让小明吃完后洗个碗,洗完碗后再拖个地......看看 Promise 怎么爬出 callback hell

function read() {
    console.log('小明认真读书');
}

function eat() {
    return new Promise((resolve, reject) => {
        console.log('好嘞,吃饭咯');
        setTimeout(() => {
            resolve('饭吃饱啦');
        }, 10 * 60 * 1000)
    })
}

function wash() {
    return new Promise((resolve, reject) => {
        console.log('唉,又要洗碗');
        setTimeout(() => {
            resolve('碗洗完啦');
        }, 10 * 60 * 1000)
    })
}

function mop() {
    return new Promise((resolve, reject) => {
        console.log('唉,还要拖地');
        setTimeout(() => {
            resolve('地拖完啦');
        }, 10 * 60 * 1000)
    })
}

const cooking = new Promise((resolve, reject) => {
    console.log('妈妈认真做饭');
    setTimeout(() => {
        resolve('小明快过来,开饭啦');
    }, 30 * 60 * 1000);
})

cooking.then(msg => {
    console.log(msg);
    return eat();
}).then(msg => {
    console.log(msg);
    return wash();
}).then(msg => {
    console.log(msg);
    return mop();
}).then(msg => {
    console.log(msg);
    console.log('终于结束啦,出去玩咯')
})
read();

/* 执行顺序:
妈妈认真做饭
小明认真读书
小明快过来,开饭啦
好嘞,吃饭咯
饭吃饱啦
唉,又要洗碗
碗洗完啦
唉,还要拖地
地拖完啦
终于结束啦,出去玩咯
*/ 

好了,很溜吧。

“妈妈喊你回家吃饭”到此结束,但异步编程是一个很大的议题,内容远不至此,等待你持续挖掘。

brickspert commented 6 years ago

好玩。你这个解释好有趣。不过你确定第三个,不要写个Dad如何实现的吗?

shaozj commented 6 years ago

generator 和 async+await 怎么没有?

xiongyuqiong commented 6 years ago

学习了

tk1061178 commented 6 years ago

例子举得很简洁直观。

wejex commented 6 years ago

学习一下楼主

zizijun commented 6 years ago

学习了。那如果新来一个需求,小明需要泡茶,需要同时烧开水,洗完茶壶才能泡茶,那用Promise怎么实现呢

chiwent commented 6 years ago

@zizijun 我猜你的意思是指先开始烧水,然后就在烧水的间隙时间里洗茶壶然后等到水烧开了就泡茶?我的实现是这样的:

function Task1() {
    console.log('开始烧水');
    setTimeout(function () {
      console.log('烧好了,可以泡茶了')
    }, 5000);
}

function Task2() {
    return new Promise((resolve, reject) => {
    console.log('洗茶壶');
    setTimeout(() => {
            resolve('洗好了,等着水烧开');
        }, 1000)
})
}

Task1();
Task2().then(msg => {
    console.log(msg)
});

如果我错误理解了你的意思请指出

leonsux commented 6 years ago

@zizijun , @chiwent

// 烧开水
function task1() {
    console.log('1. 烧开水ing...');
    return new Promise(resolve => {
        setTimeout(() => {
            console.log('4. 水烧开了');
            resolve();
        }, 2000);
    });
}

// 洗茶壶
function task2() {
    console.log('2. 洗茶壶ing...');
    return new Promise(resolve => {
        setTimeout(() => {
            console.log('3. 洗好了');
            resolve();
        }, 1000);
    })
}

// 泡茶
function task3() {
    console.log('5. 开始泡茶');
}

Promise.all([task1(), task2()]).then(() => task3());
xiaohuoni commented 6 years ago

哈哈哈,比我那时候说的送外卖有趣。 同问:generator 和 async+await 怎么没有?

curryhh commented 6 years ago

async+await 可以补补 更加优雅的实现方式

hsxrcn commented 4 years ago

方法一:回调函数 console.log('小明快过来,开饭啦') 的结尾是中文的“;”