# 同步与异步
# 同步
- 基于单线程的JS同时只能处理一件事情,而同步即是在主线程上排队执行的任务,只有当前任务执行完成,才会进入下一个任务。同步执行的函数会在预期得到结果。可以清楚什么时候能够得到返回值。
- 所有同步代码只会进入调用栈,同步代码会阻塞主线程的执行,而且会优先于其他非同步代码执行。
# 异步
- 异步指当前执行的代码会进入异步线程处理之后才会再由主线程处理回调。
- 异步的结果不是马上能够得到,而是会在将来的某个时间点获取到。
- 通常异步代码所要经过的步骤比同步代码多,由于异步代码不是直接放在调用栈中执行,而是要派发(也有可能不需要)给其他线程处理,等处理完成后的回调放在某个地方存储(比如任务队列),等到同步队列执行完成之后才会取回异步回调代码进行执行。
# 异步、单线程与Event Loop
- JS主线程处理当前正在执行的代码,它会执行当前调用栈栈顶的执行上下文,从堆空间(一般是存储对象)和栈空间(一般是存储非对象值以及对象引用)取数据,进而处理当前调用栈所用到的数据。
- 所有的同步代码会按照代码顺序压入调用栈中等待主线程执行,如果代码中遇到了异步代码,则会根据异步类型抛给异步线程执行。
- 异步类型,主要分为微任务与宏任务。
- 任务队列其实本质就是一块内存空间,里面的任务是依据FIFO先进先出的规则来执行,所有异步代码执行完毕的回调都是加入到异步任务队列中等待主线程的调用。
- 异步可以提高cpu的利用率。
# 微任务
- 微任务队列与宏任务队列的区别就在于,主线程对于其中的任务调度的区别,主进程会优先执行微任务队列中的全部任务,当微任务中的全部任务执行完毕才会挨个执行宏任务。
- 微任务可以由这些方法关键字调用产生Promise、async、await、MutaionObserver、process.nextTick(node环境)。
- 如果调用微任务方法时,方法内部包含其他线程干预处理时,会抛给执行线程执行,而主线程继续执行下面的代码,等到其他线程处理完成之后,如果有回调函数则会把回调加入到指定异步类型(这里为微任务队列)的队列中排队等待主线程执行。
# 宏任务
- 一般宏任务队列存放的是WebApis的回调,WebApis中包含许多线程,GUI渲染线程(与JS主线程互斥不能同时进行)、事件触发线程、定时器线程、异步网络请求线程。其优先级低于微任务。
# JS单线程
- JS单线程设计之初就是为了简化代码,解决DOM冲突,如果JS为多线程语言,那么有可能产生多个线程同时操作DOM的情况,那么将会导致JS操作同个DOM引起冲突,如果介于多线程的锁机制来解决冲突,又会使得JS代码复杂度提高。
- 基于JS单线程的设计,引出异步执行的方式,使得JS具有类似多线程的效果,但无论是异步还是同步,JS永远只有一个线程在执行。
# Event Loop
- 事件循环机制是针对于主线程的调度方式。
- 可以理解为主线程在寻找任务执行的过程就是事件循环,其寻找方式就是调用机制。
- 浏览器执行JS代码的流程
- 通常浏览器在最开始运行JS代码的入口就是HTML中的script标签所涵盖的代码。
- 当GUI渲染线程解析到script标签,则会把标签所涵盖的JS代码加入到宏任务队列中。
- 首先JS引擎(如V8引擎)先取第一个宏任务,即script的代码块,然后主线程在调用栈中解析JS代码。
- 等所有代码解析完成之后开始运行JS代码。
- 如果遇到同步代码直接执行。
- 遇到异步代码,如果是宏任务类型即异步WebApis处理的异步代码,那么将会通知WebApis在对应的线程中处理异步任务,此时JS主线程继续执行下面的代码,在其他线程处理完毕之后如果有回调函数,则异步线程会将回调函数加入到宏任务队列尾部。
- 如果是微任务类型的异步代码,也同宏任务处理,只不过是把回调函数加入到微任务队列中,其优先级高于宏任务队列。
- 当同步代码全部执行完成,主线程将会一直检测任务队列,如果有异步微任务则执行完全部的微任务。
- 进一步执行浏览器渲染进程绘制页面,之后就是开始下一轮的事件循环,就又回到取宏任务执行。
- 这里注意,所有的微任务都是由宏任务中执行的代码产生,一开始只有宏任务队列有任务
以下是事件循环大致流程
以下是主线程判断逻辑
# 前端异步的场景
- 前端异步主要用于代码可能会发生等待,而等待过程不能阻塞主线程运行的情况。
- 通常WebApis接口都是异步调用的,由于需要其他线程的处理,就需要等待其返回结果,那么JS主线程就没必要一直等待,这样就需要使用异步来进行处理。
- 比如定时器任务setTimeout、setInterval、ajax请求、图片动态加载、DOM事件触发这些都属于浏览器执行的异步任务;比如JS中的Promise、async、await属于JS语言自身的异步操作这些都可以实现异步。
- 当需要动态加载图片的时候就需要用到异步;当需要执行的JS的同步代码需要长时间占用的主线程时可以使用异步方式拆分为多个步骤执行,这样可以避免浏览器页面长时间无响应或者卡顿。
- 当需要执行很长一段时间才能得到结果的代码时也可以使用html5中的Web worker在浏览器渲染进程下新开一个线程用来专门执行此代码,通过postMessage来返回运行结果这样也不会占用JS主线程,但是这个线程无法操作DOM和BOM。
# WebWorker多线程
- 基于js单线程的局限性,如果执行一个很耗时间的函数,那么主线程将会被长时间占用,因此导致事件循环暂停,使得浏览器无法及时渲染和响应,那么将会造成页面崩溃,用户体验下降,所以html5支持了webworker。
- webwork简单理解就是可以让特定的js代码在其他线程中执行,等执行结束后返回结果给主线程接收即可。
- 比如在js中需要实现一个识别图片的算法,而且此算法需要很长的计算时间,如果让js主线程来执行将会导致上述发生的事情,那么正好可以使用webwork技术来实现。
- 创建一个web worker文件。
- 其中写入算法代码,在最后调用postMessage(result)方法返回结果给主线程。
- js主代码中通过
w=new Worker(文件路径)
来创建一个渲染进程的webworker子线程实例。 - 通过
w.onmessage=function(e){console.log(e.data)}
给其添加一个事件监听器。 - 当webworker中传递消息给js主线程时会在此回调函数中执行。
- 通过调用w.terminate()终止webworker线程。
- webworker线程与js主线程最大的区别就在于webworker线程无法操作window与document对象。
// test.html(主线程)
const w= new Worker('postMessage.js')
w.onmessage=function(e){
console.log(e.data);
}
w.postMessage('b') // b is cat
w.terminate() // 手动关闭子线程
----------------------------------------------
// postMessage.js(worker线程)
this.addEventListener('message', (e) => {
switch (e.data) {
case 'a': this.postMessage(e.data+' is tom');
break;
case 'b': this.postMessage(e.data + ' is cat');
break;
default: this.postMessage(e.data + " i don't know");
this.close(); // 自身关闭
break;
};
});