JS单线程、任务队列、同步与异步
单线程模型:历史延续,JS是单线程模型,它在同一时间只能执行一个任务,其他的任务在后排队等待
执行栈(Stack):后进先出,每个函数调用形成一个栈帧,串起来就是栈
任务队列(Queue):先进先出,执行栈空了之后按进入顺序依次取出执行
堆(Heap):无序,是存放数据的地方
Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
同步模式就是后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;
异步模式则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行队列上的后一个任务,而是执行回调函数;后一个任务则是不等前一个任务的回调函数的执行而执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。
- 范例1
let timer1 = setTimeout(() => {
console.log(1)
}, 0)
console.log(2)
以上代码先输出1,立即输出2 延时0ms和延时1ms效果其实是一样的,定时器有最小时间粒度。
定时器最小时间间隔:在苹果机上的最小时间间隔是10ms,在Windows系统上的最小时间间隔大约是15ms。Firefox中定义的最小时间间隔是10ms,而HTML5规范中定义的最小时间间隔是4ms
- 范例2
const useTime = t => {
let start = Date.now()
while(Date.now() - start < t) {}
}
let timer1 = setTimeout(() => {
console.log(3)
}, 500)
let timer2 = setTimeout(() => {
console.log(4)
}, 1000)
console.log(1)
useTime(2000)
console.log(2)
以上代码立即输出1,等待2秒后同时依次输出2、3、4,因为setTimeout是异步任务,而timer1比time2先注册,且timer1比timer2定时时间短。又因为useTime函数执行时间是2s,所以执行完之后,将立即执行任务队列里的timer1、timer2,所以等待2秒后将依次输出2、3、4。
线程、事件循环和任务队列
Javascript是单线程的,但是却能执行异步任务,这主要是因为 JS 中存在事件循环(Event Loop)和任务队列(Task Queue)。
事件循环:JS 会创建一个类似于 while (true) 的循环,每执行一次循环体的过程称之为Tick。每次Tick的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中,也就是每次Tick会查看任务队列中是否有需要执行的任务。
任务队列:异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如onclick, setTimeout,ajax 处理的方式都不同,这些异步操作是由浏览器内核的webcore来执行的,webcore包含下图中的3种 webAPI,分别是DOM Binding、network、timer模块。
- DOM Binding 模块处理一些DOM绑定事件,如onclick事件触发时,回调函数会立即被webcore添加到任务队列中。
- network 模块处理Ajax请求,在网络请求返回时,才会将对应的回调函数添加到任务队列中。
- timer 模块会对setTimeout等计时器进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
主线程:JS 只有一个线程,称之为主线程。而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的。所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。只有当主线程中执行栈为空的时候(即同步代码执行完后),才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。
JavaScript:理解同步、异步和事件循环 - manxisuo
什么是JS中的执行上下文、执行栈、事件循环 - 亿速云