众所周知,Javascript 语言的执行环境是"单线程"(single thread)。javascript
所谓"单线程",就是指一次只能完成一件任务。若是有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。java
而浏览器是多线程的,JS 线程就是其中一个:node
浏览器线程知识中重要的一点是:编程
GUI渲染进程和 JavaScript 引擎进程是互斥的,由于若是这两个线程能够同时运行的话, JavaScript 的 DOM 操做将会扰乱渲染线程执行渲染先后的数据一致性。并且若是 DOM 一变化,界面就马上从新渲染,效率必然很低json
因此 JS 主线程执行任务时,浏览器渲染线程处于挂起状态。数组
同理,若是 JS 采用多线程同步的模型,那么如何保证同一时间修改了 DOM, 究竟是哪一个线程先生效呢?从操做系统调度多线程的上下文开销,到实际编程里的锁、线程同步等问题,都让开发变得比较困难。浏览器
因此 JS 最终采用了单线程的事件模型。多线程
我以前的文章《JS专题之事件循环》也有讲过这块内容,欢迎翻阅。闭包
单线程模式这种排队执行的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),每每就是由于某一段Javascript代码长时间运行(好比死循环),致使整个页面卡在这个地方,其余任务没法执行。app
为了解决这个问题,Javascript语言将任务的执行模式分红两种:同步(Synchronous)和异步(Asynchronous)。
那同步和异步的区别是什么?
咱们想象一个很常见的场景:咱们去面馆吃牛肉面,柜台人不少,前面在排队下单。
这个时候,同步就是,收银员收了你的钱,告诉你要在柜台站着等面煮好,煮好后,就端面开吃,后面的人也只能等前面的人面煮好了才能付款下单而后等着面煮好端走~
而异步就是,收银员收了你的钱,而后给了你一张小票,小票上有一个你的编号,收银员告诉你,能够去座位上,你的面一煮好,会大声叫你,你就来端面开吃。
咱们能够看出,咱们是过程的调用者,面馆是被调用者,牛肉面煮好,是咱们想要的结果,同步是调用者须要主动地等待这个结果。异步是被动的等待结果,当被调用者有结果了,就会经过消息机制或者回调机制告诉调用者结果。
同步和异步关注的是消息通讯机制,同步就是在发出一个调用时,在没有获得结果以前,该调用就不返回。可是一旦调用返回,就获得返回值了。
而异步则是相反,调用在发出以后,这个调用就直接返回了,因此没有返回结果, 而是在调用发出后,被调用者经过状态、通知来通知调用者,或经过回调函数处理这个调用。
以上:
将例子抽象成伪代码:
orderNoodle("牛肉面", function(noodle) {
// 端面
getNoodle();
// 吃面
eatNoodle();
});
复制代码
关于事件循环如何执行异步代码能够翻阅前面的文章《JS专题之事件循环》,这里大概提一下。
若是遇到异步事件,JS 引擎会把事件函数压入执行调用栈,但浏览器识别到它是异步事件后,会将其弹出执行栈,当异步函数有返回结果后,JS 引擎将异步事件的回调函数放入事件队列中,若是执行调用栈为空,就将回调函数压入执行调用栈执行。
在 JavaScript 中,函数 function 做为一等公民,使用上很是自由,不管调用它,或者做为参数,或者做为返回值均可以。
由于单线程异步的特色,后来在 JS 中,慢慢将函数的业务重点转移到了回调函数中。
function step1(cb) {
console.log("step1");
cb()
}
function step2(){
console.log("step2");
}
step1(step2); // step1 step2
复制代码
代码会按前后顺序执行 step1, step2。
如今假设咱们有这样的需求:请求文件1后,获取文件1 中的数据后请求文件2,获取文件 2 中的数据后,又请求文件三。
var fs = require("fs");
fs.readFile("./file1.json", function(err, data1) {
fs.readFile("./file2.json", function (err, data2) {
fs.readFile("./file3.json", function(err, data3) {
})
})
})
复制代码
由第四节能够看出,回调函数的写法存在不少问题。
当多个异步事务多级依赖时,回调函数会造成多级的嵌套,被花括号一层层包括,代码变成 金字塔型结构,也被称为回调地狱和洋葱模型。
在回调地狱的状况下,代码逻辑的梳理,流程的控制,代码封装维护,错误处理都变得愈来愈困难。
try...catch 是被设计成捕获当前执行环境的异常,意思是只能捕获同步代码里面的异常,异步调用里面的异常没法捕获。
function readFile(fileName) {
setTimeout(function () {
throw new Error("类型错误");
}, 1000);
}
try {
readFile('./file1.json');
} catch (e) {
// 若是异步事件出错,打印不出来错误信息
console.log('err', e);
}
复制代码
在 nodejs 对回调函数采用 error first 的思想,回调函数的第一个参数保留给一个错误error对象,若是有错误发生,错误将经过第一个参数err返回。
缘由是一个有回调函数的函数,执行分两段,第一段执行完以后,任务所在的上下文环境就已经结束了。在这之后抛出的错误,原来的上下文已经没法捕捉,只能当作参数,传入第二阶段。
fs.readFile('/etc/passwd', 'utf8', function (err, data) {
if(err) {
console.log(err)
return;
}
});
复制代码
回调函数是 JS 异步编程中的基石,但同时也存在不少问题,不太适合人类天然语言的线性思惟习惯。
接下来几篇文章,我将梳理 JS 中异步编程中的历史演进中 Promise, generator, async&await 相关的内容,欢迎关注。
欢迎关注个人我的公众号“谢南波”,专一分享原创文章。