【js重学系列】异步编程
js 异步
你应该知道,javascript语言是一门单线程的语言,主要设计是单线程异步模型,不像java语言,类继承Thread再来个thread.start就可以开辟一个线程,所以,javascript就像一条流水线,仅仅是一条流水线而已,要么加工,要么包装,不能同时进行多个任务和流程。所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。
线程和进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源
一个进程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
线程的划分尺度小于进程,使得多线程程序的并发性高。
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
浏览器是多进程的
放在浏览器中,每打开一个tab页面,其实就是新开了一个进程,在这个进程中,还有ui渲染线程,js引擎线程,http请求线程等。 所以,浏览器是一个多进程的。
JS 为什么是单线程的
最初设计 JS 是用来在浏览器验证表单操控 DOM 元素的是一门脚本语言,如果 js 是多线程的,那么两个线程同时对一个 DOM 元素进行了相互冲突的操作,那么浏览器的解析器是无法执行的。
单线程就是同一个时间只能做一件事。多线程就是同一个时间可以做很多事情。
JavaScript 是单线程的。举个很简单的例子你就明白了,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,那浏览器要怎么显示,是不是乱套了。所以 JavaScript 只能是单线程的。
也许会有人说再 HTML5 中可以用
new Worker(xxx.js)
在 JavaScript 中创建多个线程。但是子线程完全受主线程控制,且不得操作 DOM。所以 JavaScript 还是单线程的。在最新的 HTML5 中提出了 Web-Worker,但 javascript 是单线程这一核心仍未改变。所以一切 javascript 版的"多线程"都是用单线程模拟出来的。
什么是同步或异步
在研究 js 异步之前,先弄清楚异步是什么。异步是和同步相对的概念,
同步,指的是一个调用发起后要等待结果返回,返回时候必须拿到返回结果。
而异步的调用,发起之后直接返回,返回的时候还没有结果,也不用等待结果,而调用结果是产生结果后通过被调用者通知调用者来传递的。
举个例子,A 想找 C,但是不知道 C 的电话号码,但是他有 B 的电话号码,于是 A 给 B 打电话询问 C 的电话号码,B 需要查找才能知道 C 的电话号码,之后会出现两种场景看下面两个场景:
A 不挂电话,等到 B 找到号码之后直接告诉 A
A 挂电话,B 找到后再给 A 打电话告诉 A能感受到这两种情况是不同的吧,前一种就是同步,后一种就是异步。
为什么js是异步的
先来看 js 的诞生,JavaScript 诞生于 1995 年,由 Brendan Eich 设计,最早是在 Netscape 公司的浏览器上实现,用来实现在浏览器中处理简单的表单验证等用户交互。至于后来提交到 ECMA,形成规范,种种历史不是这篇文章的重点,提到这些就是想说一点,js 的最初设计就是为了浏览器的 GUI 交互。对于图形化界面处理,引入多线程势必会带来各种各样的同步问题,因此浏览器中的 js 被设计成单线程,还是很容易理解的。但是单线程有一个问题:一旦这个唯一的线程被阻塞就没办法工作了--这肯定是不行的。由于异步编程可以实现“非阻塞”的调用效果,引入异步编程自然就是顺理成章的事情了。
现在,js 的运行环境不限于浏览器,还有 node.js,node.js 设计的最初想法就是设计一个完全由事件驱动,非阻塞式 IO 实现的服务器运行环境,因为网络 IO 请求是一个非常大的性能瓶颈,前期使用其他编程语言都失败了,就是因为人们固有的同步编程思想,人们更倾向于使用同步设计的 API。而 js 由于最初设计就是全异步的,人们不会有很多不适应,加上 V8 高性能引擎的出现,才造就了 node.js 技术的产生。node.js 擅长处理 IO 密集型业务,就得益于事件驱动,非阻塞 IO 的设计,而这一切都与异步编程密不可分。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。这就是 JavaScript 中的同步任务。
如果 js 中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。 对于用户而言,阻塞就以为着“卡死”,这样就导致了很差的用户体验。比如在进行 ajax 请求的时候如果没有返回数据后面的代码就没办法执行
但是同步任务有个很大的缺点,如果前一个任务执行了很长时间还没结束,那下一个任务就不能执行,举个简单的例子,页面某个区域渲染过程中需要用 Ajax 去请求数据,如这个请求很长时间都请求不到数据,那下个任务就不能执行,也就说页面其他区域不能渲染。于是就有了 JavaScript 异步任务来解决这个缺点。
异步任务可以单独执行,不要等前一个任务结束后再执行。但是异步任务执行结束后就会在那边等待,直到线程里面没有任务了。才会喊异步任务的回调函数过来执行。
引入同步和异步
Javascript 语言的执行环境是"单线程"(single thread,就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推)。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 Javascript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,Javascript 语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
“同步模式" 就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
“异步模式" 非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是 Ajax 操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有 http 请求,服务器性能会急剧下降,很快就会失去响应。
- 单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
- 如果排队是因为计算量大,CPU 忙不过来,倒也算了,但是很多时候 CPU 是闲着的,因为 IO 设备(输入输出设备)很慢(比如 Ajax 操作从网络读取数据),不得不等着结果出来,再往下执行。
- JavaScript 语言的设计者意识到,这时主线程完全可以不管 IO 设备,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 设备返回了结果,再回过头,把挂起的任务继续执行下去。
- 于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
- 怎么知道主线程执行栈为空啊?js 引擎存在 monitoring process 进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去 Event Queue 那里检查是否有等待被调用的函数
js 异步原理
栈 堆 队列 事件循环
这个队列就是异步队列,它是处理异步事件的核心,整个 js 调用时候,同步任务和其他编程语言一样,在栈中调用,一旦遇上异步任务,不立刻执行,直接把它放到异步队列里面,这样就形成了两种不同的任务。由于主线程中没有阻塞,很快就完成,栈中任务边空之后,就会有一个事件循环,把队列里面的任务一个一个取出来执行。只要主线程空闲,异步队列有任务,事件循环就会从队列中取出任务执行。
说的比较简单,js 执行引擎设计比这复杂的多得多,但是在 js 的异步实现原理中,事件循环和异步队列是核心的内容。JS 异步的执行机制其实就是事件循环(eventloop),理解了 eventloop 机制,就理解了 js 异步的执行机制。
异步编程实现
回调函数(callback)
A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.
翻译:回调是一个函数被作为一个参数传递到另一个函数里,在那个函数执行完后再执行。( 也即:B 函数被作为参数传递到 A 函数里,在 A 函数执行完后再执行 B )
callback 方式利用了函数式编程的特点,把要执行的函数作为参数传入,由被调用者控制执行时机,确保能够拿到正确的结果。这种方式初看可能会有点难懂,但是熟悉函数式编程其实很简单,很好地解决了最基本的异步问题,早期异步编程只能通过这种方式。
然而这种方式会有一个致命的问题,在实际开发中,模型总不会这样简单,下面的场景是常有的事:
fun1(data => {
// ...
fun2(data, result => {
// ...
fun3(result, () => {
// ...
});
});
});
整个随着系统越来越复杂,整个回调函数的层次会逐渐加深,里面再加上复杂的逻辑,代码编写维护都将变得十分困难,可读性几乎没有。这被称为毁掉地狱,一度困扰着开发者,甚至是曾经异步编程最为人诟病的地方
promise
使用回调函数来编程很简单,但是回调地狱实在是太可怕了,嵌套层级足够深之后绝对是维护的噩梦,而 promise 的出现就是解决这一问题的。promise 是按照规范实现的一个对象,ES6 提供了原生的实现,早期的三方实现也有很多。在此不会去讨论 promise 规范和实现原理,重点来看 promise 是如何解决异步编程的问题的。
Promise 对象代表一个未完成、但预计将来会完成的操作,有三种状态:
pending:初始值,不是 fulfilled,也不是 rejected
resolved(也叫 fulfilled):代表操作成功
rejected:代表操作失败
整个 promise 的状态只支持两种转换:从 pending 转变为 resolved,或从 pending 转变为 rejected,一旦转化发生就会保持这种状态,不可以再发生变化,状态发生变化后会触发 then 方法。这里比较抽象,我们直接来改造上面的例子:
new Promise((reso, rej) => {
setTimeout(() => {
reso('hello')
}, 2000)
}).then(res => {
console.log(res);
return new Promise(reso => {
setTimeout(() => {
reso('world')
}, 2000)
})
}).then(res => {
console.log(res);
})
Promise 是一个构造函数,它创建一个 promise 对象,接收一个回调函数作为参数,而回调函数又接收两个函数做参数,分别代表 promise 的两种状态转化。resolve 回调会使 promise 由 pending 转变为 resolved,而 reject 回调会使 promise 由 pending 转变为 rejected。
当 promise 变为 resolved 时候,then 方法就会被触发,在里面可以获取到 resolve 的内容,then 方法。而一旦 promise 变为 rejected,就会产生一个 error。无论是 resolve 还是 reject,都会返回一个新的 Promise 实例,返回值将作为参数传入这个新 Promise 的 resolve 函数,这样就可以实现链式调用,对于错误的处理,系统提供了 catch 方法,错误会一直向后传递,总是能被下一个 catch 捕获。用 promise 可以有效地避免回调嵌套的问题,代码会变成下面的样子:
fun1().then(data => {
// ...
return fun2(data);
}).then(result => {
// ...
return fun3(result);
}).then(() => {
// ...
});
generator
async,await
总结
js 的异步:我们从最开头就说 javascript 是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要
javascript 是一门单线程语言,在最新的 HTML5 中提出了 Web-Worker,但 javascript 是单线程这一核心仍未改变。所以一切 javascript 版的"多线程"都是用单线程模拟出来的,一切 javascript 多线程都是纸老虎!
事件循环 Event Loop:事件循环是 js 实现异步的一种方法,也是 js 的执行机制。
javascript 的执行和运行:执行和运行有很大的区别,javascript 在不同的环境下,比如 node,浏览器,Ringo 等等,执行方式是不同的。而运行大多指 javascript 解析引擎,是统一的。
setImmediate:微任务和宏任务还有很多种类,比如
setImmediate
等等,执行都是有共同点的,有兴趣的同学可以自行了解。最后的最后:牢牢把握两个基本点,以认真学习 javascript 为中心,早日实现成为前端高手的伟大梦想!
- javascript 是一门单线程语言
- Event Loop 是 javascript 的执行机制