同步模式与异步模式
事件循环与消息队列
JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程,但是浏览器的渲染进程是提供多个线程的,如下:
- JS引擎线程
- 事件触发线程
- 定时触发器线程
- 异步http请求线程
- GUI渲染线程
当遇到计时器、DOM事件监听或者是网络请求的任务时,JS引擎会将它们直接交给 webapi,也就是浏览器提供的相应线程(如定时器线程为setTimeout计时、异步http请求线程处理网络请求)去处理,而JS引擎线程继续后面的其他任务,这样便实现了 异步非阻塞。
定时器触发线程也只是为 setTimeout(..., 1000) 定时而已,时间一到,还会把它对应的回调函数(callback)交给 任务队列 去维护,JS引擎线程会在适当的时候去任务队列取出任务并执行。
JS引擎线程什么时候去处理呢?消息队列又是什么?
JavaScript 通过 事件循环 event loop 的机制来解决这个问题。
其实 事件循环 机制和 任务队列 的维护是由事件触发线程控制的。
事件触发线程 同样是浏览器渲染引擎提供的,它会维护一个 任务队列。
JS引擎线程遇到异步(DOM事件监听、网络请求、setTimeout计时器等...),会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由 事件触发线程 将异步对应的 回调函数 加入到消息队列中,消息队列中的回调函数等待被执行。
同时,JS引擎线程会维护一个 执行栈,同步代码会依次加入执行栈然后执行,结束会退出执行栈。
如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。
消息队列是类似队列的数据结构,遵循先入先出(FIFO)的规则。
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一但"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复,这种机制就被称为事件循环(event loop)机制。
异步编程的几种方式
Promise异步方案 宏任务/微任务队列
Promise
- promise是一个类,在执行这个类的时候需要传递一个执行器进去 执行器,执行器会立即执行
- promise中有三种状态 分别为成功fulfilled 失败rejected 等待pending
一旦状态确定就不可更改 - resolve和reject函数是用来更改状态的
- then方法内部做的事情就是判断状态,成功就调用成功函数,反之调用失败函数,then方法是被定义到原型对象中的
- then成功回调有一个参数表示成功之后的值,同样失败也有参数
- then方法是可以被链式调用的,后面的then拿到的值是,是上一个then的返回值
const PENDING='pending'
const FULFILLED='fulfilled'
const REJECTED='rejected'
class MyPromise{
construtor(executor){
try{
executor(this.resolve,this.reject)
}catch(e){
this.reject(e)
}
}
status=PENDING
//成功之后的值
value=undefined
//失败后的原因
reason=undefined
//成功回调
successCallback=[]
//失败回调
failCallback=[]
resolve=(value)=>{
//如果状态不是等待阻止程序向下执行
if(this.status!==PENDING) return;
this.status=FULFILLED
//保存成功之后的值
this.value=value
while(this.successCallback.length){
this.successCallback.shift()()
}
}
reject=()=>{
if(this.status!==PENDING) return;
this.status=REJECTED
//保存失败后的原因
this.reason=reason
while(this.failCallback.length){
this.failCallback.shift()()
}
}
then(successCallback,failCallback){
let promise2=new MyPromise((resolve,reject)=>{
if(this.status===FULFILLED){
setTimeout(()=>{
try{
let x= successCallback(this.value)
//判断x的值是普通值还是promise对象
//如果是普通值直接调用resolve
//如果是promise对象查看promise对象的返回结果
//再根据promise对象返回的结果决定调用resolve还是reject
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
}
if(this.status===REJECTED){
setTimeout(()=>{
try{
let x= failCallback(this.reason)
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
}else{
//等待,存储成功和失败回调
this.successCallback.push(()=>{
setTimeout(()=>{
try{
let x= successCallback(this.value)
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
})
this.failCallback.push(()=>{
setTimeout(()=>{
try{
let x= failCallback(this.reason)
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
})
}
})
return promise2
}
}
function resolvePromise(promise2,x,resolve,reject){
if(x===promise2){
reject(new TypeError('chaining cycle detected for promise'))
}
if(x instanceof MyPromise){
//x.then(value=>resolve(value),reason=>reject(reason))
x.then(resolve,reject)
}else{
resolve(x)
}
}
macro-task(宏任务) 和 micro-task(微任务)。
所有任务分为 macro-task 和 micro-task:
- macro-task:主代码块、setTimeout、setInterval等(可以看到,事件队列中的每一个事件都是一个 macro-task,现在称之为宏任务队列)
- micro-task:Promise、process.nextTick等
JS引擎线程首先执行主代码块。
每次执行栈执行的代码就是一个宏任务,包括任务队列(宏任务队列)中的,因为执行栈中的宏任务执行完会去取任务队列(宏任务队列)中的任务加入执行栈中,即同样是事件循环的机制。
在执行宏任务时遇到Promise等,会创建微任务(.then()里面的回调),并加入到微任务队列队尾。
micro-task必然是在某个宏任务执行的时候创建的,而在下一个宏任务开始之前,浏览器会对页面重新渲染(task >> 渲染 >> 下一个task(从任务队列中取一个))。同时,在上一个宏任务执行完成后,渲染页面之前,会执行当前微任务队列中的所有微任务。
也就是说,在某一个macro-task执行完后,在重新渲染与开始下一个宏任务之前,就会将在它执行期间产生的所有micro-task都执行完毕(在渲染前)。
这样就可以解释 "promise 1" "promise 2" 在 "timer over" 之前打印了。"promise 1" "promise 2" 做为微任务加入到微任务队列中,而 "timer over" 做为宏任务加入到宏任务队列中,它们同时在等待被执行,但是微任务队列中的所有微任务都会在开始下一个宏任务之前都被执行完。
在node环境下,process.nextTick的优先级高于Promise,也就是说:在宏任务结束后会先执行微任务队列中的nextTickQueue,然后才会执行微任务中的Promise。
执行机制:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS引擎线程继续,开始下一个宏任务(从宏任务队列中获取)
宏任务 macro-task(Task)
一个event loop有一个或者多个task队列。task任务源非常宽泛,比如ajax的onload,click事件,基本上我们经常绑定的各种事件都是task任务源,还有数据库操作(IndexedDB ),需要注意的是setTimeout、setInterval、setImmediate也是task任务源。总结来说task任务源:
- script
- setTimeout
- setInterval
- setImmediate
- I/O
- requestAnimationFrame
- UI rendering
微任务 micro-task(Job)
microtask 队列和task 队列有些相似,都是先进先出的队列,由指定的任务源去提供任务,不同的是一个 event loop里只有一个microtask 队列。另外microtask执行时机和Macrotasks也有所差异
- process.nextTick
- promises
- Object.observe
- MutationObserver
宏任务和微任务的区别
- 宏队列可以有多个,微任务队列只有一个,所以每创建一个新的settimeout都是一个新的宏任务队列,执行完一个宏任务队列后,都会去checkpoint 微任务。
- 一个事件循环后,微任务队列执行完了,再执行宏任务队列
- 一个事件循环中,在执行完一个宏队列之后,就会去check 微任务队列