前言
首先我们都知道javascript 是一门单线程、异步、非阻塞、解析类型脚本语言。
- 单线程 ??
- 异步 ??
- 非阻塞 ??
- 然后还有 事件循环、消息队列,还有微任务、宏任务这些??
同步任务和异步任务
同步: 前一个任务结束后在执行后一个任务,程序执行的顺序和任务的排列顺序是一致的。同步的做法例如:一边吃饭吃完饭才能睡觉.
异步:在做一件事的时候,因为这个任务花费时间比较长,在做这件事的时候,还可以去做另一件事,比如一边吃饭一边听音乐
js中的同步和异步:
实现异步模式的方法有很多,比较常用的有:
延迟类:setTimeout、setInterval、requestAnimationFrame、setImmediate。
监听事件实现的类型:监听new Image加载状态、监听script加载状态、监听iframe加载状态、Message。
带有异步功能类型 Promise、ajax等等。
js中同步和异步的区别:
"异步任务"是指不进入主线程,而进入任务队列的任务。(任务队列后面会讲到这个)。
javascript 单线程和浏览器多线程
单线程的核心概念是指:同一个时间只能做一件事。
单线程的缺点:如果当前执行代码所需时间很长,则会影响后面程序的执行,页面卡顿,造成用户体验感特别差(所以要引入异步)。
javascript为什么是单线程。
这是由 Javascript 这门脚本语言的用途决定的。作为浏览器脚本语言,JavaScript 主要用于处理页面中用户交互,以及操作 DOM 树、CSS 样式树(当然也包括服务器逻辑的交互处理)。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征。
为了利用多核 CPU 的计算能力,在 HTML5 中引入的工作线程使得浏览器端的 JavaScript 引擎可以并发地执行 JavaScript 代码,从而实现了对浏览器端多线程编程的良好支持。Web Worker 允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM 。所以,这个新标准并没有改变 JavaScript 单线程的本质。
由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。为了防止渲染出现不可预期的结果,浏览器设置 UI 渲染线程与 JavaScript 引擎线程为互斥的关系,当 JavaScript 引擎线程执行时 UI 渲染线程会被挂起,UI 更新会被保存在一个队列中等到 JavaScript 引擎线程空闲时立即被执行。
浏览器多线程
具体产看:浏览器(组成和进程)_Absurd_people的博客-CSDN博客
其实,JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程,但是浏览器的渲染进程是提供多个线程的
当遇到计时器、DOM事件监听或者是网络请求的任务时,JS引擎会将它们直接交给 webapi,也就是浏览器提供的相应线程(如定时器线程为setTimeout计时、异步http请求线程处理网络请求)去处理,而JS引擎线程继续后面的其他任务,这样便实现了 异步非阻塞。
定时器触发线程也只是为 setTimeout(..., 1000)
定时而已,时间一到,还会把它对应的回调函数(callback)交给 任务队列 去维护,JS引擎线程会在适当的时候去任务队列取出任务并执行。
执行栈与任务队列
JS引擎线程会维护一个 执行栈,同步代码会依次加入执行栈然后执行,结束会退出执行栈。如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。
栈 是一种 LIFO(Last In, First Out)的数据结构,特点即 后进先出。
我们来看一段代码:
const bar = () => console.log('bar');
const baz = () => console.log('baz');
const foo = () => {
console.log('foo');
bar();
baz();
}
foo();
事件触发线程:由浏览器渲染引擎提供,他会维护一个任务队列。
JS引擎线程遇到异步(DOM事件监听、网络请求、setTimeout计时器等...),会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由 事件触发线程 将异步对应的 回调函数 加入到任务队列中,任务队列中的回调函数等待被执行。
队列 是一种FIFO(First In, First Out) 的数据结构,它的特点就是 先进先出。
任务队列的维护
首相要明白什么是 Event Table :可以理解成一个事件与对应的回调函数
的对应表。它就是用来存储 JavaScript 中的异步事件 (request, setTimeout, IO等) 及其对应的回调函数的列表。
也要明白什么是Event Queue :简单理解就是 回调函数 队列
,所以它也叫 Callback Queue,当 Event Table 中的事件被触发,事件对应的 回调函数 就会被 push 进这个 Event Queue,然后等待被执行。
js主线程有一个事件队列,这个事件队列由事件触发线程维护,就是说 js主线程有一个事件队列,这个事件队列由事件触发线程维护,然后就是其他的线程(如定时器触发线程或http线程)也有自己的任务队列,当这些线程的回调函数到相应的是执行事件的时候首先放入自己事件队列中区,然后是由事件触发线程将其放入到js主线程的事件队列里面。js主线程会在栈被清空时去执行自己的事件队列。(这就是事件触发线程对js主线程任务队列的维护。)
宏任务和微任务
任务队列的类型
任务队列存在两种类型
宏队列macrotask queue,也叫tasks
setTimeout
setInterval
setImmediate (Node独有)
requestAnimationFrame (浏览器独有)
I/O
UI rendering (浏览器独有)
特点:
唯一,整个事件循环当中,仅存在一个。执行为同步。
微队列,microtask queue,也叫jobs。
process.nextTick (Node独有)
Promise
Object.observe
MutationObserver
特点:
不唯一,存在一定的优先级(用户I/O部分优先级更高);异步执行,同一事件循环中,只执行一个。
事件循环 event loop
全局Script代码执行完毕后,调用栈Stack会清空。然后从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1,继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空,取出宏队列macrotask queue中位于队首的任务,放入Stack中执行。每执行一个宏任务,完毕以后都会再次检查是否有微任务产生,如果有微任务产生,则执行微任务。(这就是事件循环机制)
micro-task必然是在某个宏任务执行的时候创建的。每次执行栈执行的代码就是一个宏任务,包括任务队列(宏任务队列)中的,因为执行栈中的宏任务执行完会去取任务队列(宏任务队列)中的任务加入执行栈中,即同样是事件循环的机制。
注意:如果当前执行站(call stack )还在没有执行完毕是不会执行下一个宏任务和微任务的。
(电脑性能不高,会卡顿)。
console.log('-- 开始--');
console.log(new Date());
setTimeout(() => {
console.log("定时器执行了" + new Date());
},3000);
for(var i = 0 ; i < 500000; i ++){
console.log(5000);
}
console.log('--结束了--');