操作系统
操作系统根据cpu的核心可以并发执行程序,比如4核cpu可以同时跑4个程序。
浏览器
浏览器本身可以调用操作系统的并发执行,浏览器是多线程的。
如下图:
图中一个浏览器,可以有网络线程,IO读写线程,音视频播放线程,等等。
那为什么说js是单线程语言?
准确来说,不是js是单线程,而是浏览器打开一个页面之后,本页面的整个环境的js代码处于一个线程中。
浏览器每个页面是一个进程
一个页面是一个独立的进程,进程跟进程之间,也就是页面跟页面之间,内存是独立的。
但是在同一个进程中,自己的内存是内部共享的。
每个浏览器进程都有一个主渲染线程
这个主渲染线程就是我们常说的js单线程。
(因为浏览器解析html、计算css样式,执行js运算等等,这些操作能够共享内存,能够共享变量、对象,所以主渲染线程被设计为单线程模式,这样才能保证不同代码操作同一份共享内存的先后顺序,把顺序的逻辑让编写代码的人来控制。)
js单线程中的异步
这里的异步就是相对同步来说,同步是主线程按照代码编写的顺序一一执行,异步是某段代码发起一个异步执行程序,然后就跳过继续执行下一个同步代码。
如下图:
在主线程中发起异步执行代码,异步执行的代码和主线程代码是"并发"并行执行的,同一个时刻可以有多个任务并行执行。
js单线程的异步回调
异步执行并无问题,如果主线程和异步线程不需要接触的话,各自跑就行了,但是异步执行完毕之后,需要执行回调函数。
比如发起异步网络请求,或者异步磁盘IO读写请求,当异步请求完成时,往往肯定是需要后续处理的,那就是回调函数。比如网络请求拿到图片后,要通过回调函数把图片展示到页面上,所以,这时候,异步执行又要"回归到"js主线程中来执行回调函数。
使用事件循环机制,来处理异步回调
要把异步程序完成后的回调函数纳入主线程来执行,就需要一个机制:事件循环。
如上图:异步执行完成后,把执行结果作为参数传入回调函数,然后把回调函数返回给主线程执行。返回的回调函数,是放入一个Micro任务执行队列中(异步放入)。
1、主线程把目前可见的同步代码从头到尾执行完毕。
然后:
2、主线程把Micro任务队列任务执行完毕。
注:1、2是不断重复循环的,因为异步Micro任务队列会不断的补充回调函数,这里一个关键点在于,主线程把队列中的任务执行完没看到有补充之后,就回到主线程中执行任务,就是不断的循环的两边清空队列中的任务。
异步执行和并发执行的区别
并发执行,是多核cpu同时执行多个程序。
异步执行,是把程序抽出主线程之外来执行。但是如果异步代码和主线程代码都是用同一个线程来跑的话,时间上来说异步代码和主线程代码还是有一个先后顺序无法并行跑的,只是概念上它们不是串联的。
但是像网络请求,io请求这些异步程序,个人猜测是调用其他线程比如网络线程,IO线程来跑的,如果网络线程是跑在CPU的另一个核的线程上,那么就是真正意义的异步并发执行了,这种情况下不会阻塞js单线程的运行。
像js的Web Workers
,个人觉得就是发起一个真正并发的异步执行,利用了CPU的多线程来跑的。
特别提醒:
setTimeout需要提供一个回调函数,但是回调函数不是异步的,回调函数最终是要在setTimeout这个异步定时程序到时间后,把回调函数放入Micro任务队列,然后等待js主线程(单线程)下一轮事件循环来把队列处理。所以回调函数本身是会占用js主线程运行时间的(跑在js单线程里,阻塞js单线程的)。
所以,setTimeout定时程序是异步执行,回调函数不是。同理,网络请求是异步请求,但是请求完成后执行的回调函数不是异步执行。
来看一个例子:
js
function doSomething(d) {
console.log(d);
}
doSomething("开始")
new Promise(function callBack0(resolve, reject) {
doSomething(1);
let responsePromise = fetch("https://url");
resolve();
// return fetch("url");
}).then(function callBack1() {
doSomething(3);
}).then(function callBack2() {
doSomething(4);
})
doSomething(2)
//输出 1 2 3 4
- 首先可以明确的是,
.then(fn)
中的fn的执行是属于回调函数的执行,所以fn会等待主线程同步代码执行完成后,再从Micro任务队列中拿到fn来执行。所以callBack1和callBack2两个回调是最后执行,而,主线程同步代码中的doSomething("开始")、doSomething(2)
按顺序执行。 - 问题是
doSomething(1);
是属于同步代码还是异步代码呢,其实是属于同步代码。也就是会再主线程同步代码中按顺序执行。
到此,上面所说的要么是同步执行,要么是回调执行,没有异步执行。
上面的异步代码是fetch("https://url");
。
所以,可以如下图这样理解:
图中绿色的才是异步执行,异步执行既不在主线程队列中,也不在回调函数Micro任务队列中。
区分真正异步和假异步
如果一个异步代码,最终需要使用js单线程来执行,不管把它的顺序如何调整,当执行它的时候,总是会阻塞主线程,导致页面卡顿等问题。
所以,真正有意义的异步,比如计时器、网络请求、IO读写等,这些执行不会阻塞js单线程。
什么情况使用真正异步?
据个人猜测,异步执行会产生共享内存变量同时修改的问题,比如2个线程同时对i++,乱套了。所以如果能够保证函数不依赖共享变量(在后端语言中可以暂时锁定变量不让修改),而且cpu是多线程的,那么使用真正异步就有意义。
比如运算类的算法函数,而不是操作对象的函数。
如何使用真正异步?
如果代码最终需要用js单线程来跑,那不会是真正的异步。
但是可以通过Promise.then放入到回调队列中,这样可以排在同步代码之后执行。
或者使用web worker真正的异步(像fetch请求那样):
js
let worker = new Worker("worker.js");
worker.onmessage = function (e) {
console.dir(e.data)
}
worker.postMessage(data);
worker.terminate();
ini
//worker.js
self.onmessage = (e) => {
postMessage(self.onmessage === onmessage);
};
worker的执行不会占用js单线程时间。