一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
Udemy Node.js, Express, MongoDB & More: The Complete Bootcamp
同步和异步
"同步模式": 只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
"异步模式": 每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。
在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。
单线程
而PHP就会为每个用户分配一个线程
异步和回调
使用回调函数并不会使代码自动成为异步,只有在使用一些Node API(如readFile)时的回调是异步的。回调是拿到异步结果的一种方式;同时,回调也可以拿到同步的结果。
实现功能
步骤1 从本地dog.txt中获取狗品种(字段)
步骤2 利用superagent方法向某网站请求获取对应照片
步骤3 将获取到的照片地址写入本地dog-img.txt中
需要的模块:
const fs = require('fs'); //文件操作
const superagent = require('superagent'); //获取图片
1.先写一个Callback Hell版
fs.readFile('./dog.txt', 'utf-8', (err, data) => { //1
if (err) console.log('Can not read find this file');
superagent
.get(`https://dog.ceo/api/breed/${data}/images/random`)//2
.then((res) => {
console.log(res.body.message);
fs.writeFile('./dog-img.txt', res.body.message, (err) => {//3
if (err) return console.log('Can not write file');
console.log(`Successfully get the dog image`);
});
});
});
层层嵌套,可读性差
2.利用Promise对象优化
如何定义Prmoise对象?
Promise对象是一个构造函数,我们使用时要new出它的实例。
接受一个函数作为参数。该函数有两个参数resolve和reject,它们可以获取到异步操作的消息,即成功返回的value或失败返回的error,从而改变Promise实例对象的“状态”,状态改变后建议分别使用then(),catch()方法指定resolved状态和rejected状态的回调函数。
const promise = new Promise((resolve, reject) => {
//...
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
如何使用Promise对象?
就本例来说,异步操作是文件的读写。这里不用变量接收Promise实例对象,而是直接将它作为返回值:
const readFilePro = (file) => {
return new Promise((resolve, reject) => {
fs.readFile(file, 'utf-8', (err, data) => {
if (err) reject(`Could not find that file 🥲`);
resolve(data);
});
});
};
const writeFilePro = (file, data) => {
return new Promise((reslove, reject) => {
fs.writeFile(file, data, err => {
if (err) reject(`Could not write file 🥲`);
reslove('Success');
});
});
};
这也是使用Promise解决callback hell的关键步骤:让函数返回一个Promise实例对象,这样就可以在其后链式调用then方法,即将异步操作以同步操作的流程表达出来。
readFilePro('./dog.txt')
.then(data => {
console.log(`${data}🐶`);//1
return superagent.get(`https://dog.ceo/api/breed/${data}/images/random`);
})
.then(response => {
console.log(response.body.message);//2
return writeFilePro('./dog-img.txt', response.body.message);
})
.then(() => {
console.log(`Random dog image saved to file !`);//3
})🐶
.catch((err) => {
console.log(err);
});
3.asycn/await——Promise的语法糖
async关键字,该声明表示函数内部有异步操作,即表示它并不会阻塞Event Loop。并且,该函数会自动返回一个Promise对象,对,和我们之前自己写的函数一样。
当async函数执行的时候,一旦遇到await就会先暂停,直到其后Promise异步操作完成,再接着执行函数体内后面的语句。这使我们的代码看起来更像同步,可读性强,但本质上和方法2是一样的。等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者throw err。
就本例,我们定义一个async函数getDogPic,注意用关键字声明:
const getDogPic = async () => {
try {
const data = await readFilePro(`./dog.txt`);//1
//我们用变量data储存await表达式的值,也就是Promise成功后返回的参数。
console.log(`Breed: ${data}`);
const response = superagent.get(`https://dog.ceo/api/breed/${data}/images/random`);//2
await writeFilePro('./dog-img.txt', response.body.message);//3
console.log('Random dog image saved to file!');
} catch (err) {
console.log(err);
throw err;
}
return 'READY 🐶';
};
对“自动返回一个Promise对象”的理解:
//调用getDogPic函数并用x接收它的返回值
const x = getDogPic();
console.log(x)
//输出
Promise { <pending> }
如上,打印x,并不是我们返回的'READY 🐶'字符串,而是返回一个状态为resolved的 Promise 对象,且then方法接收到的参数为字符串‘READY 🐶’。如果我们想得到该字符串,如前文,可以用then方法接收Promise成功的参数,并设置回调:
getDogPic().then(x=>console.log(x))
//输出
'READY 🐶'
但这不是最好的写法。因为我们已经使用了async&await,又在这里用then方法,很混乱。更好的解决方法是使用另一个立即执行的async函数:
(async () => {
try {
const x = await getDogPic();
console.log(x);
} catch (err) {
console.log('ERROR 💥');
}
})();
Promise.all
拓展一下功能,如果我们想同时获得三张狗狗的照片,那么需要定义三个Promise实例对象。使用多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发,这就要用到all方法。
const res1Pro = superagent.get(`https://dog.ceo/api/breed/${data}/images/random`);
const res2Pro = ...
const res3Pro = ...
const all = await Promise.all([res1Pro, res2Pro, res3Pro]);
const imgs = all.map(el => el.body.message);
控制台输出:
参考资料
Node.js, Express, MongoDB & More: The Complete Bootcamp P39-P45