promise 和 async 函数解决“回调地狱”


Node.js提供的非阻塞IO模型允许我们利用回调函数的方式处理IO操作,但是当需要连续的IO操作时,你的回调函数会出现多重嵌套,最后陷入“回调地狱”(callback hell)。

使用回调函数的痛

举个栗子:
现在有三个文件:a.txt,b.txt,c.txt,内容如下。我们要做的是读取 a 文件拿到 b 文件的文件名,再读取 b 文件拿到 c 文件的文件名,最后读取 c 文件,打印 c 文件中的内容。

**项目目录**
	- node_test
		a.txt       // content: b.txt
		b.txt       // content: c.tx
		c.txt       // content: finish
		readFileData.js  

readFileData.js 中的内容:

const fs = require('fs');

// 读取a文件内容
fs.readFile('a.txt', {flag: 'r', encoding: 'utf-8'}, (err, aData) => {
    if (err) {
        console.log(err);
    } else {
        // 读取b文件内容
        fs.readFile(aData, 'utf-8', (err, bData) => {
            if (err) {
                console.log(err);
            } else {
            	// 读取c文件内容
                fs.readFile(bData, 'utf-8', (err, cData) => {
                    if (err) {
                        console.log(err);                            
                    } else {
                    	// 打印c文件内容
                        console.log(cData);
                    }                  
                })
            }
        })
    }
})

out:
	finish

可以看的出来,才三层嵌套就已经招架不住了,晕。

解决这个问题可以使用 promise 或者 async

使用 Promise 解决 callback hell

什么是 promise,怎么使用promise

promise是一个对象,它通常代表一个在未来可能完成的异步操作。

  • 利用 Promise 构造函数构建 promise 对象
    const p = new Promise((resolve, reject) => {
        // promise函数体
        
        if (...) {
            resolve(value);
        } else {
            reject(new Error('错误说明'));
        } 
    });
    
  1. Promise 构造函数需要传入一个参数,这个参数是一个函数,假设它叫 promiseFun。

  2. promiseFun 需要两个参数,习惯把第一个参数叫做 resolve,第二个参数叫做 reject。

    // 参数resolve和reject也是一个函数,原型定义如下:
    function resolve(value) {...}
    function reject(reason) {...}  // reject函数的参数可以是一个字符串,但是建议使用 new Error(),这样显得比较规范
    
  3. promiseFun 不需要返回值,它的执行结果只有两种,成功 or 失败,把成功的结果传入 resolve 函数,失败的结果传入 reject 函数。

    3.1. Promise其实有三种状态:pending、 resolve、 reject

    3.2. 如果promise函数体中既没有调用resolve函数,又没有调用reject函数,prmise对象就会是pending状态

    const p = new Promise((resolve, reject) => {
        // promise函数体
    	console.log('pending状态');
    });
    
    p.then(...).then(...).catch()  // pending状态下,p之后的代码都不会执行,也就是跳出了promise的链式调用
    
  4. resolve 和 reject 这两个函数谁先调用,promise对象的状态就会锁定谁,另外一个就算被调用也没有任何效果。

    resolve(value);
    reject(err);
    
    这样调用之后,promise对象的状态就会锁定在 resolve(成功),后面的 reject(err) 没有任何意义。
    
  5. 在 promiseFun 函数体中,我们可以看到 resolve 和 reject 是直接调用的,并没有定义,它们是在未来定义的,在这里提前使用。

  • 定义resolve和reject函数体
  1. Promise 的内置函数 then 和 catch 就是真正用来定义 resolve 和 reject 函数体的。

    p
    .then(function(value) {
    	// 这里是 resolve 函数的具体定义
    })
    .then(function(value) {
    	// 这里是 resolve 函数的具体定义
    })
    .catch(function(reason) {
        // 这里是 reject 函数的具体定义
    })
    
  2. promise.then() 函数的返回值是一个Promise对象,这是为了构造then()函数链(即后面可以继续调用then函数)。

  3. promise.catch() 函数的返回值也是一个Promise对象(尽管我们通常不会使用这个对象)。

    3.1. catch 的位置放在最后,链式中任何一个环节出问题,都会被catch到,而且这个环节之后的代码不会再执行。

    3.2. catch 的位置在某个then()函数后面,catch函数前的任何一个环节出问题,都会被catch到,而且这个环节之后的then()函数会继续执行。

    ,p
    .then(function(value) {
    	return p2  // p2 是Promise对象
    })
    .catch(function(reason) {
        // 如果 p2 的状态锁定的是reject(失败),就会被catch到
        return 2
    })
    .then(function(value) {
    	// 处理完catch函数之后会继续执行这个then()函数
    	// 参数value的值取决于catch函数的参数函数的返回值,参考下面的“resolve 函数的两种返回值”
    	// 如果catch函数没有返回值,则 value 的值为 undefined
    	
    })	
    
  • resolve 函数的两种返回值
  1. resolve 返回值是:数字、字符串、对象等,会作为下个 then(resolve) 函数中参数函数 resolve 的参数。

    p.then(function(value) {
    	return 2
    })
    .then(function(value) {
    	console.log(value)
    })
    .catch(function(reason) {
        // 这里是 reject 函数的具体定义
    })
    
    out:
    	2 
    
  2. resolve 返回值是promise对象,那么 p.then() 方法的返回值就是这个promise对象,再用这个promise对象继续调用 then() 方法。

    p.then(function(value) {
    	const p2 = new Promise((resolve, reject) => {
    	    // promise函数体
    	    
    	    if (...) {
    	        resolve(value);
    	    } else {
    	        reject(err);
    	    } 
    	})
    	
    	return p2  // p2 是Promise对象
    })
    .then(function(value) {    // 相当于 p2.then()
    	//  这里是 p2 对象的 resolve 函数的具体定义
    })
    .catch(function(reason) {
        // 这里是 reject 函数的具体定义
    })
    
  • 如何终止或跳出promise链式调用
  1. 手动抛出异常,前提是catch函数放在最后。

    1.1. 这样做无法判断是程序错误跳出还是手动跳出。

    p.then(function(value) {
    	return 1;
    })
    .then(function(value) {
    	console.log(value);
    	throw new Error('手动抛出异常');
    })
    .then(function(value) {
    	console.log(value);
    	return 2;
    })
    .catch(function(err) {
       console.log(err);
    });
    
    out:
    	1
    	手动抛出异常 
    
  2. then()函数中返回 Promise.reject(err),前提是catch函数放在最后。
    2.1. 有时候这样并不能确定是程序错误被catch到,还是手动跳出被catch到。

    2.2. 为了解决上述问题,我们可以在catch函数中判断错误类型是否是 Error 对象来进行区别(这就是为什么前面建议reject函数的参数使用 new Error())。

    p.then(function(value) {
    	return 1;
    })
    .then(function(value) {
    	console.log(value);
    	return Promise.reject('利用reject主动跳出');
    })
    .then(function(value) {
    	console.log(value);
    })
    .catch(function(err) {
    	if (err instanceof Error) {
        	console.log('程序错误退出');
        } else {      
            console.log('主动跳出');
        }   	
    });
    
    out:
    	1
    	主动跳出 
    
  3. 通过在then()函数中返回 promise 的 pending 状态来终止链式调用,catch函数的位置不在最后的情况。

    3.1. 因为promise一直处于pending状态,有可能会出现内存泄漏的现象,不建议大量使用这种方式。

    p.then(function(value) {
    	return 1;
    })
    .then(function(value) {
    	console.log(value);
    	// 返回一个新的promise对象,么有resolve和reject,一直处于pending状态,所以链式调用在这里就被中断了
    	return new Promise(() => {console.log('利用pending主动跳出')});
    })	
    .catch(function(err) {
    	if (err instanceof Error) {
        	console.log('程序错误退出');
        } else {      
            console.log('主动跳出');
        }   	
    })
    .then(function(value) {
    	console.log(value);
    });
    
    out:
    	1
    	利用pending主动跳出
    

使用 promise 解决 callback hell

console.log('step-1');

function readFileData(fileName) {

    console.log('promise-1');
	
	// 构建promise对象并返回
    return new Promise((resolve, reject) => {

        console.log('promise-2');
		
		// 读取文件
        fs.readFile(fileName, 'utf-8', (err, data) => {

            console.log('readFile', fileName);

            if (err) {
            
                reject(err);  // 读取失败则把错误信息传入reject函数,等待catch函数处理
                
            } else {
            
                resolve(data);  // 读取成功则把数据传入resolve函数,等待then函数处理
                
                console.log(fileName, '读取成功!');
            }
        })
    });
}

console.log('step-2');

readFileData('a.txt')	// 返回promise对象,异步读取a文件内容

.then(aData => {  // 定义resolve函数,参数aData是a文件内容

    console.log('a文件内容:', aData);
    
    return readFileData(aData);  // 返回promise对象,异步读取b文件内容
    
})

.then(bData => {  // 定义resolve函数,参数bData 是b文件内容
		
    console.log('b文件内容:', bData);
    
    return readFileData(bData);  // 返回promise对象,异步读取c文件内容
    
})

.then(cData => {  // 定义resolve函数,参数cData 是c文件内容
	
    console.log('c文件内容:', cData);
    
})

.catch(err => {  // 定义reject函数,参数err是读取文件是的错误信息(如果读取失败的话)

    console.log(err);
    
});

console.log('end');


out:
	step-1
	step-2
	promise-1
	promise-2
	end
	readFile a.txt
	a.txt 读取成功!
	a文件内容: b.txt
	promise-1
	promise-2
	readFile b.txt
	b.txt 读取成功!
	b文件内容: c.txt
	promise-1
	promise-2
	readFile c.txt
	c.txt 读取成功!
	c文件内容: finish

注意:

  1. 从out的前 5 句可以看出,Promise的执行仍然是异步方式的,并没有改变成同步执行模式,只不过让代码写起来读起来像是同步执行一样。

  2. 从out的第 3,4 句可以看出,Promise函数体是在Promise对象创建的时候就被执行了。

  3. 从out的第 6,7, 8 句可以看出:
    3.1:resolve或者reject函数已经在Promise函数体内被调用了,而此时resolve和reject的值并没有被定义了,怎么办?其实这就是Promise机制实现的功能,可是先调用一个未定义的函数,等将来函数被定义的时候(then())再真正执行函数体。

    3.2:then/catch函数体并不是在then/catch被调用的时候执行的,而是在后面的某一个异步时间点被执行,对他们的执行是在稍后以异步事件的方式回调的,具体的回调时间是不确定的。这也是Promise机制实现的功能。

使用 async 解决 callback hell

什么是 async,怎么使用async

使用 promise 虽然能够解决 callback hell,但并不是最好的方法,使用的时候总是一堆 then()。
es7 给我们提供了更好的方法,使用 async 和 await 。

  • 使用 async 声明异步函数

    async function fn() {}
    // 或者
    async () => {}
    
  1. async 函数的返回值是 promise 对象

    1.1. 从下面的例子可以看到,如果async关键字函数返回的不是promise,会自动用Promise.resolve()包装

    async function asyncFun() {
    	return 'promise';
    	// 等同于
    	return Promise.resolve('promise');
    	// 等同于
    	return new Promise((resolve, reject) => { resolve('promise') })
    }
    
  2. 怎么拿到 async 函数中返回的字符串呢?

    2.1. 因为async函数的返回值是promise对象( Promise.resolve(value) ),所以要通过then()函数来获取 value 值

    2.2. 注意调用then()函数的是async函数的返回值 asyncFun(),必须加括号

    asyncFun().then(value => {
    	console.log(value);
    })
    
    
    out:
    	promise
    
  • 在 async 函数中使用 await
  1. await 后面跟 promise 对象,必须等到promise对象有返回值的时候,代码才会继续执行下去

    1.1. 可以看出来,使用 await 之后,不需要调用 then() 函数,可以直接得到 Promise.resolve(value) 的 value 值

    function setDelay(second) {
        return new Promise((resolve, reject) => {      
            // 传入的不是数值型数据就会调用 reject
            if (typeof second != 'number') {
                reject('输入的不是number类型的值!')
            }
    
            setTimeout(() => resolve(`延迟了${second}秒输出`), second * 1000);
        });
    }
    
    async function asyncFun() {
        try{
            // 会在这里等待 1 秒,拿到返回值后再继续执行下面的代码
            const result01 = await setDelay(1);
            // 会在这里等待 2 秒,拿到返回值后再继续执行下面的代码
            const result02 = await setDelay(2);
            
            console.log(result01);
            console.log(result02);
        } catch (e) {
            console.log(e);
        }
    }
    
    asyncFun();
    
    
    out:
    	延迟了1秒输出
    	延迟了2秒输出
    

    注意:

    1. await 必须在 async 函数中使用,在 async 函数外使用会报错。

      SyntaxError: await is only valid in async function

  2. await 后面跟其他值,不会阻塞后面的代码,相当于同步

    2.1. 从下面的例子可以看出来,并没有因为使用 await 就延时两秒才去执行 console.log(‘end’); 这句代码,也没有阻塞 then() 函数的调用

    async function asyncFun() {
        // await 后面是 setTimeout,是否会等待两秒再执行后面的代码
        const result = await setTimeout(() => {
            console.log('延时了两秒')
        }, 2000);
    
        console.log('end');
    
        // 返回 promise 对象
        return result;
    }
    
    asyncFun().then(value => {
        console.log(value);
    });
    
    
    out:
    	end
    	Timeout {
    	  _idleTimeout: 2000,
    	  _idlePrev: [TimersList],
    	  _idleNext: [TimersList],
    	  _idleStart: 23,
    	  _onTimeout: [Function],
    	  _timerArgs: undefined,
    	  _repeat: null,
    	  _destroyed: false,
    	  [Symbol(refed)]: true,
    	  [Symbol(asyncId)]: 2,
    	  [Symbol(triggerId)]: 1
    	}
    	延时了两秒
    
  3. 处理 async 函数中的异常

    3.1. 使用 try … catch 捕获异常,这种方法在 async 函数中使用

    function setDelay(second) {
        return new Promise((resolve, reject) => {      
            // 传入的不是数值型数据就会调用 reject
            if (typeof second != 'number') {
                reject('输入的不是number类型的值!')
            }
    
            setTimeout(() => resolve(`延迟了${second}秒输出`), second * 1000);
        });
    }
    
    async function asyncFun() {
        try{
            // 这里会产生异常
            const result01 = await setDelay('1');
            // 这里的代码不会继续执行
            const result02 = await setDelay(2);
            
            console.log(result01);
            console.log(result02);
        } catch (e) {
            console.log(e);
        }
    }
    
    asyncFun();
    
    
    out:
    	输入的不是number类型的值!	
    

    3.2. 使用 Promise.catch() 捕获异常,这种方法在 async 函数外使用

    function setDelay(second) {
        return new Promise((resolve, reject) => {      
            // 传入的不是数值型数据就会调用 reject
            if (typeof second != 'number') {
                reject('输入的不是number类型的值!')
            }
    
            setTimeout(() => resolve(`延迟了${second}秒输出`), second * 1000);
        });
    }
    
    async function asyncFun() {
        try{
            // 这里会产生异常
            const result01 = await setDelay('1');
            // 这里的代码不会继续执行
            const result02 = await setDelay(2);
            
            console.log(result01);
            console.log(result02);
        } catch (e) {
            console.log(e);
        }
    }
    
    asyncFun().catch(err => {
        console.log(err);
    });
    
    
    out:
    	输入的不是number类型的值!	
    

    3.4. 上面两种方法都会中断程序的执行。在 async 函数内使用 Promise.catch() 捕获异常,程序不会终止(类似于Promise中把catch函数提前)

    	function setDelay(second) {
        return new Promise((resolve, reject) => {      
            // 传入的不是数值型数据就会调用 reject
            if (typeof second != 'number') {
                reject('输入的不是number类型的值!')
            }
    
            setTimeout(() => resolve(`延迟了${second}秒输出`), second * 1000);
        });
    }
    
    function catchErr(promise) {
        // 处理promise对象,返回数据或错误
        return promise.then(value => {
            return [null, value];
        })
        .catch(err => {
            return [err];
        })
    }
    
    async function asyncFun() {
        // 这里会产生异常
        [err01, result01] = await catchErr(setDelay('1'));
        
        // 判断是否有异常
        if (err01) {
            // 想继续执行就不抛出错误
            console.log('出现错误,但是想让下面的代码继续执行!');
        } else {
            console.log(result01);
        }
    
        // 这里的代码会继续执行
        [err02, result02] = await catchErr(setDelay('2'));
        
        // 判断是否有异常
        if (err02) {
            // 不想继续执行就抛出错误
            throw new Error('出现错误,而且不想继续执行下面的代码!');
        } else {
            console.log(result02);
        }
    
        // 这里的不会再执行
        [err03, result03] = await catchErr(setDelay(3));
        
        // 判断是否有异常
        if (err03) {
            // 不想继续执行就抛出错误
            throw new Error('出现错误,而且不想继续执行下面的代码!');
        } else {
            console.log(result03);
        }
    }
    
    asyncFun();
    
    
    out:
    	出现错误,但是想让下面的代码继续执行!
    	(node:90200) UnhandledPromiseRejectionWarning: Error: 出现错误,而且不想继续执行下面的代码!
    	...
    

使用 async 解决 callback hell

console.log('step-1');

function readFileData(fileName) {
	
	// 构建promise对象并返回
    return new Promise((resolve, reject) => {
		
		// 读取文件
        fs.readFile(fileName, 'utf-8', (err, data) => {

            console.log('readFile', fileName);

            if (err) {
            
                reject(err);  // 读取失败则把错误信息传入reject函数,等待catch函数处理
                
            } else {
            
                resolve(data);  // 读取成功则把数据传入resolve函数,等待then函数处理
                
                console.log(fileName, '读取成功!');
            }
        })
    });
}

console.log('step-2');

async function asyncReadFile() {

    try{

        const aData = await readFileData('a.txt');  // 读取a文件内容
        console.log('读取a文件内容结束');

        const bDta = await readFileData(aData);  // 读取b文件内容
        console.log('读取b文件内容结束');

        const cDtae = await readFileData(bDta);  // 读取c文件内容
        console.log('读取c文件内容结束');

        console.log(cDtae);

    } catch(err) {

        console.log(err);

    }   
}

asyncReadFile();

console.log('end');


out:
	step-1
	step-2
	end
	readFile a.txt
	a.txt 读取成功!
	读取a文件内容结束
	readFile b.txt
	b.txt 读取成功!
	读取b文件内容结束
	readFile c.txt
	c.txt 读取成功!
	读取c文件内容结束
	finish

注意:

从out的前 5 句可以看出:

  1. 在 asyncReadFile(); 之前的代码都是在定义函数,所以首先打印 step-1,step-2。

  2. 在执行到 asyncReadFile(); 这一句时,会立即执行 asyncReadFile 函数里面的代码。

  3. 在 asyncReadFile 函数里,首先执行 const aData = await readFileData(‘a.txt’); 这一句。

  4. 这时会调用 readFileData(‘a.txt’) 函数,创建promise对象(前面说过创建promise对象会立即执行promise函数体)。

  5. 在 readFileData 函数中,当执行到 fs.readFile(fileName, ‘utf-8’, (err, data) … 时,因为 readFile 函数是异步的,这时主程序不会阻塞在这里,而是回到 console.log(‘end’); 这一句继续执行,打印 end。

  6. 执行完 console.log(‘end’); 之后,程序又回到 asyncReadFile 中,这时 readFile 函数才会执行。

总结:

  1. async 函数的执行仍然是异步方式的,并没有改变成同步执行模式,只不过让代码写起来读起来像是同步执行一样。

  2. 在 async 函数中遇到异步任务时,程序会回到 async 函数外继续执行后面的同步代码。同步代码执行完,再回到async内部,继续执行await后面的代码

  3. async 函数中的代码是同步的,必须等 await 拿到返回值,才能继续执行下面的代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值