同步代码与异步代码
在了解这个之前,咱们先来理解一下同步代码和异步代码的区别。
这段代码会打印多少?
let a = 1;
console.log(a);
setTimeout(() => {
a++;
},1000)
console.log(a);
输出:1,1. 这里因为放了一个定时器,而定时器是一个耗时的代码,同步代码的执行是不需要耗时的,而异步代码的执行是需要消耗时间的,当jsV8遇到耗时的代码时会先将挂起,先执行同步代码在执行异步代码。
同步代码:不耗时执行的代码
异步代码: 需要耗时执行的代码
进程与线程
进程:进程是操作系统中程序的一次动态执行过程,是系统进行资源分配和调度的基本单位。每个进程都拥有独立的地址空间,包括代码、数据、堆栈等,以及操作系统分配的资源如文件描述符、信号量等。
特点:
- 独立性:进程之间相互独立,一个进程的异常或终止不会直接影响到其他进程。
- 资源拥有:拥有独立的内存空间和系统资源,能够防止数据冲突。
- 上下文切换:切换进程时,需要保存和恢复进程的上下文(如寄存器状态、栈指针等),开销较大。
- 通信:进程间通信(IPC,Inter-Process Communication)较为复杂,需要使用管道、消息队列、共享内存等机制。
线程:线程是进程内的一个执行单元,是CPU调度的基本单位。多个线程可以共享同一个进程的地址空间和资源,独立执行不同的任务。
特点:
- 轻量级:相比进程,线程的创建、销毁和切换开销小,因为它们共享进程的内存空间。
- 共享资源:同一进程内的线程可以直接访问进程的全局变量和资源,无需特别的通信机制。
- 并发执行:线程可以在同一进程中并发执行,提高了CPU的利用率和程序的响应速度。
- 同步问题:由于共享资源,线程间需要同步机制(如互斥锁、信号量)来避免数据竞争和一致性问题。
这里有一个要注意的点:js的加载是会阻塞页面的渲染的,当引用的js库没有完全加载完时,后面的代码就不会执行。渲染线程和js引擎线程是不能同时工作的。
二者之间的关系:
- 包含关系:一个进程可以包含一个或多个线程。
- 资源分配:进程是资源分配的基本单位,而线程是CPU调度的基本单位。
- 灵活性与效率:线程提供了比进程更高的并发程度和通信效率,但同时也引入了线程安全的问题。
- 应用场景:需要大量独立资源和完全隔离的场景适合使用进程,而需要高效并发处理和资源共享的场景则倾向于使用线程。
微任务与宏任务
let count = 0;
function a(){
setTimeout(()=>{
count++;
},1000)
}
function b(){
console.log(count);
}
a();
b();
咱们先来看这段代码:请问b调用后的输出是多少?
输出:0. 这里运行到13行时,虽然是a先调用,但是a里面有一个定时器,需要耗时执行,是一个异步代码,所以当V8运行到第三行的时候会先将里面的异步代码挂起,因为v8只有一个线程嘛。发现count的值是0,因此打印0,过了1s后执行异步代码count++.这里我们说a()函数是一个异步代码是因为它里面有个定时器是耗时代码,可是有的时候假设它里面不是一个定时器,是一个其他东西,像http请求,但是http的请求耗时又是不一定的,它的耗时可能与网速有关,也可能与设备的性能有关,网速好设备性能好耗时就少,网速慢设备性能差耗时就长。那你说这个代码到底耗不耗时呢?因此随着科技的这样发展,js单纯的把代码分成同步和异步不足以去描述更深层次的场景。为了解决这些应用场景,js官方就在异步代码里面又区分了一个微任务与宏任务。
那么微任务和宏任务有哪些呢?首先这两个都是属于异步代码的。
微任务:promise.then(), process.nextTick(), MutationOvserver()
宏任务:setTimeout(0s钟也是宏任务), setInterval, setImmediate, I/O, UI-rendering
例1
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve()
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(4);
})
setTimeout(function () {
console.log(5);
})
console.log(6);
1.第一行代码,输出1
2.第二行代码,调用promise函数,注意这里是调用promise,不是上面微任务说的promise.then(),因此是一个同步代码,输出2.
3.第六行代码,这里才是异步代码里面的微任务,加入微任务队列先挂起(then1)
4.第九行代码也是异步代码里的微任务,因此加入微任务队列,挂起(then2)排在then1后面
5.第十二行代码是异步任务里的宏任务,加入宏任务队列挂起(set)
6.第十五行代码为同步任务,输出6.
到这里,第一次事件循环机制里的同步任务就全部执行完毕了,开始寻找是否有异步代码需要执行。
7.如果有的话就先执行微任务,因此then1出队列,输出3,接着then2出队列,输出4.
8.微任务执行完毕后,如果有需要的话就会渲染页面,也就是html页面的加载,这里没有。因此执行异步中的宏任务,也是第二次事件循环机制的开始,set出队列,输出5.
9.因此最终结果输出126345.
例2
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve()
})
.then(() => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0)
})
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0)
}, 0)
console.log(7);
1.首先第一步第一行执行同步代码输出1毋庸置疑。
2.第二行同样是调用promise函数,是一个同步代码,输出2
3.第四行调用resolve(), . then()就能执行了,是异步代码里的微任务,因此整体整个then()先加入微任务队列(输出3挂起)
4.到第十二行,是一个定时器,属于异步代码里的宏任务,先加入红队伍队列set1
5.到达18行,输出7
至此,第一次事件循环的第一步同步代码就执行结束了,接下来执行微任务
6.执行微任务,then1出队列,但是then1中也有同步和异步,不管,先执行同步代码,输出3,发现有个定时器,于是将set2加入宏任务队列,排在set1也就是第十二行的set后面。微任务就执行1结束了。
7.接下来开始执行宏任务,set1出队列,宏任务开启一次新的事件循环,同步任务先执行,输出5,然后第二次事件循环发现了一个定时器宏任务,set3把它加入宏任务队列,排在set2也就是第八行的set后面。第二次事件循环的同步结束,然后去找微任务队列,发现微任务队列是空的,没有微任务要执行,接着去宏任务队列找宏任务,开启第三次事件循环,set2出队列,因此输出4,这也意味着第二次事件循环宏任务结束,第二次事件循环结束,输出4即是第二次时间循环的结束也是第三次事件循环的开始,紧接重复刚刚的步骤去微任务队列找,发现没有,然后去宏任务队列找,set3出队列,输出6。
因此最终输出的结果为1273546.
例3
1.第一行执行同步代码,输出script start
2.第三行和第七行都是函数的声明并没有调用,先不管,到第十行,async1的调用,但是由于async2前有个await 需要等到async2执行结束才会运行后面的代码,因此先调用async2,async2里面是同步代码,直接输出async2 end,接着轮到第五行代码执行,但是由于await会将这行打印放到了微任务队列,因此async1 end加入微任务队列。
这里有个重要的点,就是await会将后续代码阻塞进微任务队列,这也是为什么await后面的函数会先执行的原因,你可以理解为它就是一个强盗,很霸道,不仅我要先执行,我还要把我后面的代码全部放进微任务队列里。
这里再跟大家说一下这个async,为什么要加这个东西呢?其实加入这个就是相当于在函数里里面返回了一个promise函数,还记得promise函数的作用,
- 通过Promise来处理异步操作,实现将多个异步操作按照顺序依次执行;
- 通过每个Promise的
.then
或.catch
方法可以返回一个新的Promise来实现链式调用;
也就是不受定时器等耗时代码的影响,所以这里的async你可以理解为一个封装,官方打造出来的一个让你可以直接用,不用在写promise了,更加方便。好啦这里插入了一点小插曲,回到咱们刚刚的步骤,
3.第十一行是一个定时器,异步任务中的宏任务,放进宏队伍队列set
4.第十四行,是一个同步任务,输出promise.
5.第十八行,是一个微任务,放进微任务队列then1
6.第二十一行,也是一个微任务,放进微任务队列里then2,排在async1 end 和then1的后面。
7.第二十四行,同步任务,输出script end
8.到这里所有同步代码都执行完毕了,开始执行微任务,async1 end ,then1,then2,依次出队列,输出async1 end,then1,then2.
9.微任务全部执行完毕,执行宏任务,set出队列,打印setTimeout.。