Javascript 异步编程(一)初识异步
关于同步和异步,我们先来看两个例子。
const async=()=>{
console.log('async..',1)
let t=new Date();
while (true){
if(+new Date()-t>=2000){
console.log('async...',2)
break
}
}
console.log('async...',3)
}
async();
//async.. 1
//async... 2
//async... 3
顺序执行
const sync=()=>{
console.log('sync...',1);
setTimeout(()=>{
console.log('sync...',2)
},2000);
console.log('sync...',3)
}
sync();
//sync... 1
//sync... 3
//sync... 2
可能都知道JavaScript是单线程的,即同一时刻只能做一件事,如果有多个任务,则需要排队执行,但是这样同步执行的效率低,如果一个任务长时间据有CPU,其他任务则需要等待,这无疑会浪费资源,造成资源利用率低。为此,
JavaScript将任务的执行分为两种:
- 同步:后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;
- 异步:后一个任务不等前一个任务结束就执行,程序的执行顺序与任务的排列顺序是不一致的、异步的
需要了解的基本知识
进程与线程
进程(process):我们知道,程序运行是需要系统资源的(CPU,内存,I/O等)为了能使程序能够并发执行,并对并发执行的程序加以描述和控制,从而引入进程。是进程实体的运行过程,是系统进行资源分配和调动的独立单位
线程(thread):通过引入线程,一个能独立运行的基本单位,作为操作系统调度和分派的基本单位。通过减少程序在并发进行时所付出的时空开销,从而提高程序并发执行的程度,以及提高资源利用率和系统的吞吐量
进程和线程的区别和关系:
- 进程是操作系统分配资源的最小单位,线程是程序执行的最小单位。
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)。
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
以工厂模式比喻,江南皮革厂老板苦恼产量提不上去,于是给车间主任(进程)开会。
老板:你们几个怎么搞得,怎么订单(多任务)多了,产量还跟不上了?
车间主任A:手底下这么多人,哪管的过来。订单不好分配、进度不无法跟踪
车间主任B:A车间经常跟我们车间抢物料(系统资源)
车间主任A:放***屁,你们的人上个月还抢我设备呢(系统资源)
老板:当然你们说的都是客观存在的因素,你们不会从手下挑选几个有能力的人成立班组(线程)么?权利下放(独立调度)
车间主任A,B:好好好,试试。
linux下查看进程的常用命令
- ps
- top
Javascript 单线程
在ecma-262中并无与线程相关的内容,其单线程/多线程主要依赖于其JavaScript引擎(解释器)。
浏览器的进程和线程
以Chrome为例,Chrome浏览器使用多个进程来隔离不同的网页。因此在Chrome中打开一个网页相当于起了一个进程
![](https://img.haomeiwen.com/i6366468/4813cb3197c2440f.png)
多进程的原因:
- 因为进程之间相互独立,一个浏览器选项卡无响应不会影响其他的选项卡。
- 同样因为进程之间相互独立,一个选项卡中如果恶意脚本,不会影响其他选项卡。例如,Chrome 浏览器可以对处理用户输入(如渲染器)的进程,限制其文件访问的权限。
GUI线程:渲染布局(HTML,CSS等)
JS引擎线程:1.解析、执行JS 2.与GUI线程互斥 (因为引擎是单线程的,所以Javascript是单线程的)
定时触发器线程:setTimeout/setInterval
事件触发线程:将满足触发条件的时间放入任务队列
异步HTTP请求线程:XHR所在线程
单线程的原因(引用自浏览器进程?线程?傻傻分不清楚!)
这是因为Javascript这门脚本语言诞生的使命所致:JavaScript为处理页面中用户的交互,以及操作DOM树、CSS样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果JavaScript是多线程的方式来操作这些UI DOM,则可能出现UI操作的冲突; 如果Javascript是多线程的话,在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,Javascript在最初就选择了单线程执行。
Node.js中的进程和线程
当一个 Node.js 的应用启动的同时,它会启动如下模块:
- 一个进程
- 一个线程:单线程意味着在当前进程中同一时刻只有一个指令在执行。
- 事件循环机制:尽管 JavaScript 是单线程的,但通过使用回调,promises, async/await 等语法,基于事件循环将对操作系统的操作异步化,使得 Node 拥有异步非阻塞 IO 的特性。
- JS 引擎实例
- Node.js 实例
Node 运行在单线程上,并且在事件循环中同一时刻只有一个进程的任务被执行,每次同一时刻只会执行一段代码(多段代码不会同时执行)。
只有一个js引擎在主线程上运行。其他异步IO和事件驱动相关的线程通过libuv来实现内部的线程池和线程调度
为什么Node.js也是单线程呢?
因为JavaScript起初是运行在浏览器端的...你懂的。
Node.js如何实现并行:
它通过事件轮询(event loop)来实现并行操作,因此我们要避免阻塞操作,
Node.js可以多进程/多线程么?
-
利用
child_process
模块 fork子进程,实现多进程 -
利用
cluster
模块fork子进程--Master-Worker,实现多进程
![](https://img.haomeiwen.com/i6366468/19fd82672fb103c2.png)
- 使用第三方包node-threads-a-gogo 实现多线程
前端的异步场景
- 定时器
- 网络请求
- 事件绑定
- ES6 Promise
定时器
再回到我们的代码
console.log('sync...',1);
setTimeout(()=>{
console.log('sync...',2)
},2000);
console.log('sync...',3)
流程分析:
- 主程序调用栈
- 调用webAPI-
setTimeout
- 定时器线程计数2s
- 事件触发线程将定时器时间放入任务队列
- 主线程通过Event Loop遍历任务队列执行异步任务
console.log()
console.time('test')
const test=()=>{
let t=+new Date();
while (true){
if(+new Date()-t>=5000){
break;
}
}
setTimeout(()=>{
console.log('setTimeout...')
},2000)
}
test();
console.timeEnd('test');
注意:
- 定时任务可能不会按时执行,延迟原因(等待同步任务、等待CPU加载)
- 定时器嵌套5次之后最小间隔不能低于4ms
应用场景:
- 防抖
- 节流
- 倒计时
- 动画(存在丢帧)
案例分析
for(var i=1;i<=10;i++){
setTimeout(function() {
console.log(i)
},1000*i)
}
在这个案例中,我们期望每隔1s,依次打印i的值。但实际结果却是每隔1s重复打印11。
问题的原因在于:
- 定时器需要等待同步任务执行完成,那么i的值为11。
-
var
是全局作用域
修复方案:
//使用闭包,保留当前的作用域
for(var i=1;i<=10;i++){
(i=>{
setTimeout(function() {
console.log(i)
},1000*i)
})(i)
}
//使用let 保留当前的作用域
for(let i=1;i<=10;i++){
setTimeout(function() {
console.log(i)
},1000*i)
}