JS基础-三座大山之三一异步(1)
今天,来讲一讲异步,讲到异步脑海里不能之单单想到Promise,async,await的,拔出萝卜带出泥,异步涉及到:js的执行机制,事件循环(event loop),宏任务,微任务等等,这篇主要把原理讲明白,要掌握异步还要进行练习,Promise和练习题的讲解将放在JS基础-三座大山之三一异步(2)中
带着以下几个问题开始学习吧
- 进程,线程
- event loop
- event loop的机制,可画图
- 宏任务,微任务,两者的区别
1. 线程和进程的区别
进程
一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程:
进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
2. 异步
同步:就是按顺序一行一行执行,如果遇到某一步执行时间非常长也要等他执行完后才往后执行(有点傻)
异步:遇到一个任务,这个任务不知道什么时候能执行完,但是总不能干等着吧,那就这个任务先执行着,我也不等了,我先往后执行,等你任务执行完后返回的结果找个信使传给我就行了
总的来说就是:
- js 是
单线程
的,就也就是说同一个时间只能做一件事 - 作为浏览器脚本语言,与他的用途有关
- js的主要用途是和用户互动,以及操作DOM,这决定了它只能是单线程
有些任务是耗时的,会阻塞代码执行
如:遇到等待(网络请求,定时任务)不能卡主,所以需要异步,异步不会阻塞代码,同步会阻塞代码
上代码:
console.log('1')
setTimeout(function(){
console.log('2')
},5000) // 总不能真的等到5ms后才执行吧?
console.log('3')
// ********************
console.log('1')
console.log('3')
btn.addEventListener('click',function(){ // 总不能等到真的等用户点击后才往后执行吧,玩意万一用户一直不点击我后面的代码就永远不执行吗?
console.log('2')
})
console.log('3')
所以,为了解决同步会阻塞代码的问题,引入了异步,谈异步就离不开谈事件循环event loop
- JS是单线程运行的,异步要基于回调来实现,
事件循环event loop
就是异步回调的实现原理 - JavaScript 是单线程语言,
事件循环
是其实现非阻塞异步的核心机制
3. 事件循环/事件轮询 event loop
了解事件循环的好处:
-
可以更好的理解代码底层运行机制,夯实基本功
-
对学习 promise,vue里的的 $nextTick 等等有很多帮助
3.1 同步代码
和异步代码
我们可以把代码分为同步代码
和异步代码
console.log('1') // 同步代码
setTimeout(function(){ // 异步代码
console.log('2')
},5000)
console.log('3') // 同步代码
异步代码
还有
setTimeout()
setInterval() 定时器
Ajax/Fetch
事件绑定
3.2 js执行机制:
同步代码
:立即放入JS引擎(js主线程)
执行,并在原地等待
结果
异步代码
:先放入宿主环境(浏览器/Node)
,不必原地等待结果,并不阻塞
主线程继续往下执行,异步执行结果在将来执行
event loop执行过程:
1 console.log('Hi')
2 setTimeout(function cb1(){
console.log('cb1')
},5000)
3 console.log('Bye')
- 先将 1 放入 调用栈(执行栈)中,执行完后,清空栈,打印 Hi
- 执行 2 的时候,setTimeout是一个函数,setTimeout函数是Web API里面执行的,将setTimeout的参数 function cb1放入到 定时器 timer中,等事件到了就把cd1放入到
task Queue
中。 然后清空调用栈 - 将 3 放入 调用栈中,执行完后,清空栈, 打印 Bye
- 同步代码执行完毕后,浏览器回立即启动 Event Loop机制,不断轮询查找
task Queue
,直到task Queue
有内容时,就执行
总结一下这个过程:
-
同步代码一行一行放在call stack(执行栈/调用栈)执行
-
遇到异步,会先记录下,等待时机(定时、网络请求等)
-
时机到了 就将异步移动到
task Queue
任务队列中 -
如call stack 为空(即同步代码执行完成) event loop 开始工作
-
轮询查找
task Queue
, 如有则移动到call stack 执行 -
然后继续轮询查找,像永动机一样
3.3 总结
-
JS是单线程运行的,异步要基于回调来实现,event loop就是异步回调的实现原理
-
JavaScript 是单线程语言,事件循环是其实现非阻塞异步的核心机制
-
JS是单线程,防止代码阻塞,我们把代码(任务):同步和异步
-
同步代码给js引擎执行,异步代码交给宿主环境
-
同步代码放入执行栈中,异步代码等待时机成熟送入
任务队列
排队 -
执行栈执行完毕,回去任务队列看看是否有异步任务,有就送到执行栈执行,反复循环查看执行,这个过程是事件循环(event loop)
上event loop的机制图
4. 事件循环之宏任务和微任务
- JS把异步任务分为
宏任务
和微任务
在es5之后,js引入了Promise,这样,不需要浏览器,js引擎自身也能够发起异步任务了
4.1 宏任务 (MacroTask)
-
代表离散的异步操作,由宿主环境(浏览器/Node.js)调度,代码中遇到则将它放入
Task Queue
队列中 -
常见来源:
-
script
-
setTimeout / setInterval
-
I/O 操作(文件读写、网络请求)
-
DOM 事件回调(如 click)
-
setImmediate(Node.js)
4.2 微任务 (MicroTask)
-
代表高优先级的异步操作,在同步代码执行后立即执行,微任务是js引擎发起的。码中遇到则将它放入
Microtask Queue
队列中 -
常见来源
- Promise(Promise本身是同步,then/catch/finally的回调函数是异步)
- process.nextTick(node)
- Promise.then() catch()
- Async/Await
- Object.observe等等
4.3 执行图
这里介绍一个学习事件循环的网站:JS Visualizer 9000 (jsv9000.app)
分析一下代码:
console.log(100) // 同步代码 放入执行栈/调用栈中
setTimeout(()=>{
console.log(200) // 宏任务 放入Task Queue队列
})
Promise.resolve().then(()=>{ // Promise本身是同步,then/catch/finally的回调函数是异步,是微任务,放入Microtask Queue队列
console.log(300)
})
console.log(400) // 同步代码 放入执行栈/调用栈中
// 100 400 300 200
console.log("Start"); // 1. 同步
setTimeout(() => console.log("Timeout"), 0); // 2. 宏任务
Promise.resolve()
.then(() => console.log("Promise 1")) // 3. 微任务
.then(() => console.log("Promise 2")); // 4. 微任务
console.log("End"); // 5. 同步
/**
* 同步代码 Start → End 执行所有同步代码
* 清空微任务 Promise 1 执行第一个 then,Promise 2 微任务中触发的微任务会立即执行
* 执行宏任务 Timeout 取出宏任务队列中的回调执行
*/
// Start → End → Promise 1 → Promise 2 → Timeout
4.4 宏任务,微任务 和 DOM渲染
-
JS是单线程的,DOM渲染和js执行共用一个线程
-
JS执行的时候,得留一些时机供DOM渲染
-
宏任务:setTimeout,setInterval,Ajax,DOM事件(会直接放入任务队列中)
-
微任务:Promise async/await
-
宏任务:DOM渲染后触发,如setTimeout
-
微任务:DOM渲染前触发。如Promise
-
微任务执行时机要比宏任务要早?
因为微任务是在在DOM渲染前触发,宏任务是在DOM渲染后触发。
4.5 DOM事件和 event loop 的关系
因为js是单线程的,DOM事件和异步(setTimeout,ajax等)一样使用了回调的机制,基于event loop,他们的触发时机是由浏览器控制的,但是DOM事件不是异步(DOM事件的执行时机一般取决于用户的操作,比如点击按钮,但是DOM都是回调函数,所以遇到DOM事件,会先记录下,用户一操作就将会调函数让入到Callback Queue中)。
4.5 一个练习
console.log('1')
setTimeout(function(){
console.log('2')
},4000)
const p = new Promise((resolve,reject)=>{
console.log('3')
resolve('1000') // 标记成功
console.log('4')
})
p.then(data=>{
console.log(data)
})
console.log('5')
// 输出结果 1,3,4,5,1000,2
5. 总结
- JS是单线程运行的,
异步
要基于回调来实现,事件循环event loop
就是异步回调的实现原理 - JavaScript 是单线程语言,
事件循环event loop
是其实现非阻塞异步的核心机制 - js代码分为同步代码和异步代码,异步任务又分为宏任务和微任务
- js执行代码时,从上到下,遇到同步代码放入到
call stack 执行栈
中并且等待结果的输出,如果遇到异步代码,则看看是不是宏任务,是宏任务的话,等待时机成熟之后将宏任务直接放入Task Queue 任务队列
中,是微任务的的话,就将微任务放入到Microtask Queue 微任务队列中
,等同步代码执行完毕,js启动事件循环event loop机制
开始循环访问微任务队列,有的话,将队列中的任务按照先进先出的规则放入到call stack 执行栈
中执行,微任务执行完后,再将Task Queue 任务队列
中任务按照先进先出的规则放入到call stack 执行栈
中执行,然后反复循环
Promise和练习题的讲解在JS基础-三座大山之三一异步(2)中