跳到主要内容

异步和单线程

  • 回调可以是同步也可以是异步,异步必须放在回调里执行

什么是异步(对比同步)​

  • 同步会阻塞代码执行,而异步不会
  • 如果js不是异步的话,由于js代码本身是自上而下执行的,那么如果上一行代码需要执行很久,下面的代码就会被阻塞,对用户来说,就是”卡死”,这样的话,会造成很差的用户体验。
js
// 异步
console.log(100)
setTimeout(function() {
console.log(200)
},1000)
console.log(300)

// 同步
console.log(100)
alert(200) //同步会阻塞代码执行
console.log(300)

前端使用异步的场景

  • 可能发生等待的时候
  • 等待过程中不能像alert一样阻塞程序运行
  • 因此,虽有的“等待情况”都需要异步
  • 定时任务 setTimeout setInverval
  • 网络请求:ajax请求 动态img标签加载

前端使用异步的场景有哪些

  • 定时任务 setTimeout setInverval
  • 网络请求:ajax请求 动态img标签加载
  • 事件绑定 (因为有回调函数)

什么是单线程​

  • 一次只能执行一个任务

不能同时执行多个任务,比如一个杀毒软件,他能同时进行清理垃圾和扫描病毒等多种操作,就是多线程,可以执行多个任务

单线程的优缺点​

优点:

  1. 避免了资源的竞争:在多线程环境中,由于不同线程之间的执行顺序不确定,可能会导致一些难以发现的竞态条件问题。单线程避免了这个问题。
    • 如:操作DOM必然会涉及资源的竞争
  2. 节省资源:减少资源开销,由于JavaScript只有一个主线程,因此占用的内存和CPU资源相对较少,可以使页面更加流畅。
  3. 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准

缺点:

  1. 阻塞:由于JavaScript是单线程的,当某个任务需要大量计算时,主线程会被阻塞,页面会卡顿或者失去响应。

    异步编程:避免长时间的阻塞

    为了避免这个问题,就有了事件循环,来实现异步非阻塞

  2. 无法充分利用多核CPU:由于JavaScript只有一个主线程,因此无法充分利用多核CPU的优势,可能导致某些任务的处理速度较慢。

    为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程

    可以将一些计算密集型的任务分配到Web Worker中运行,避免占用主线程导致页面卡顿或失去响应。

js是如何实现异步的​

  •  既然js是单线程的,那么js是如何实现异步的呢,是通过事件循环(event loop),理解了event loop 就理解了js的执行机制。

js为什么需要异步​

如果js不是异步的话,由于js代码本身是自上而下执行的,那么如果上一行代码需要执行很久,下面的代码就会被阻塞,对用户来说,就是”卡死”,这样的话,会造成很差的用户体验。

为什么js在浏览器中有事件循环的机制?​

  • 首先js是单线程的,如果同时需要执行多个操作
    • 比如2个线程操作DOM,一个线程删除当前DOM节点,而另一个线程是操作当前DOM节点,那么这时候浏览器该听谁的, 对于浏览器而言就无法正确渲染出我们想要的结果,为了避免这个问题,就有了事件循环,来实现异步非阻塞
      • 2个事件操作,有了事件循环,就可以判断这2个事件操作,谁先执行,看哪个操作是宏任务还是微任

什么是javascript事件循环 (Event Loop)?​

一个事件循环中有一个或者是多个任务队列

  • 事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理

任务队列分为 macro-task(宏任务)与 micro-task(微任务)

  • (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

    (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

    (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

    (4)主线程不断重复上面的第三步。

    • 只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

宏任务和微任务​

js
宏任务队列有:script(整体代码)、setTimeout、setInterval 、【事件绑定】、setImmediately,I/O,UI render
new promise
微任务队列有:async、await、promise、promise.then、promose.catch process.nexttick,Object.observe(已废弃了), MutationObserver(监听DOM树改变,html5新特性)

函数返回promise是属于微任务! 【如async返回promise,用await等待promise返回的结果 =>属于微任务】

  • async是基于promise的,async 函数执行完毕会返回一个 promise,会放到微任务队列里面

ajax请求不属于宏任务,js线程遇到ajax请求,会将请求交给对应的http线程处理,一旦请求返回结果,就会将对应的回调放入宏任务队列,等请求完成执行。

注意:new promise() 创建promise对象会立即执行,它是同步的立即执行函数

js
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}

async function async2() {
console.log('async2');
}

console.log('script start');

setTimeout(()=>{
console.log('setTimeout');
},0)

async1();

new Promise((resolve)=>{
console.log('promise1');
resolve();
}).then(()=>{
console.log('promise2');
});

console.log('script end');

//结果
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

1、首先执行 script start

2、遇到事件,定时器放到下一个宏任务中

3、执行async1函数,输出async1 start

4、遇到 await async2(),【执行async2过程中会等待promise返回的结果,有结果之后才会执行下面的代码 (放到微任务中)】 ,输出async2

5、遇到new prmise()立即执行,输出promise1

6、script end 【一个主线程结束】

7、async1 end 【执行微任务里面的】

8、promise2 【执行下一个微任务】

9、setTimeout 【执行另一个宏任务】 (微任务执行完之后,执行宏任务里的定时器)

什么是javascript事件循环,什么是宏任务队列,什么是微任务队列​

  • 任务队列分为 macro-task(宏任务)与 micro-task(微任务),在最新标准中,它们被分别称为 task与 jobs。

  • 我们都知道javascript是单线程,只有唯一的事件循环。而javascript再执行过程中,除了函数调用栈来调节函数的执行顺序外,还有个任务队列task queue,任务队列可以是多个,其中又包括宏任务队列和微任务队列;


  • 每一个宏任务执行完毕之后都必须将当前的微任务队列清空,才会执行下一个宏任务

    • 宏任务队列一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务。
    • 微任务队列中所有的任务都会被依次取出来执行,直到微任务队列为空。
    • 在执行完所有的微任务之后,执行下一个宏任务之前,浏览器会执行 UI 渲染操作、更新界面。

第一个宏任务script进入后。从上往下执行,遇到同步代码就放在主线程形成执行栈中执行,异步的就注册异步任务,从上往下所有同步代码搞定后,也就是本次script宏任务执行完之后,就开始将微任务队列的任务一个一个取出来,也是从上到下执行当前微任务队列清空完之后,才会执行下一个宏任务

1、先执行主线程

2、然后执行任务队列 【分为微任务和宏任务】

3、每个宏任务执行完之把当前微任务全部执行完,才继续执行下一个宏任务

js
<script>
setTimeout(function(){
console.log('1')
});

new Promise(function(resolve){
console.log('2');
resolve();
}).then(function(){
console.log('3')
});

console.log('4');

结果:2->4->3>1
//1、遇到new Promise会立即执行,输出2
//2、输出4
//3、执行微任务 then 输出3
//4、执行宏任务 定时在 输出1
</script>
  • js异步有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入eventqueue,然后在执行微任务,将微任务放入eventqueue,注意的是!!!!,这两个queue不是同一个queue
  • 因为settimeout是宏任务,虽然先执行的他,但是他被放到了宏任务的eventqueue里面,然后代码继续往下检查看有没有微任务,检测到Promise的then函数把他放入了微任务序列。等到主线进程的所有代码执行结束后。先从微任务queue里拿回调函数,然后微任务queue空了后再从宏任务的queue拿函数
js
setTimeout(()=>{
new Promise((resolve)=>{
resolve()
}).then(()=>{
console.log('then')
})
setTimeout(()=>{
console.log('setTimeout 1')
},100)
},100)

结果:
then
setTimeout 1

script(整体代码(宏任务))是怎么入栈的​

  • macro-task(宏任务)可以说作为主人,micro-task(微任务)作为奴隶

    macro作为主导,它有支配micro的能力

text
在一个macro任务消灭之前,它会让它创建的micro任务都执行完,然后才进入执行下一个macro任务
  • 所以奴隶先挂掉的,最后才到主人挂掉。。

为什么有了宏任务,还需要微任务? 只有一种任务可以吗​

  • 因为宏任务是放在队列里都保持这先进先出原则执行

如果只有宏任务,如果这时候有紧急的任务需要优先去处理,这时候只有宏任务是不够的,因为宏任务是先进先出的原则执行,所以就引入了微任务的概念

  • 每一个宏任务执行完毕之后都必须将当前的微任务队列清空,才会执行下一个宏任务
  • 在浏览器里,浏览器先执行一遍宏任务,就去微任务队列里,把所有微任务执行,才执行下一个宏任务

为什么微任务比宏任务执行更早?​

  • 根本原因是 :微任务的优先级高于宏任务

比如要处理紧急任务,只有宏任务是不够的,就引入了微任务的概念,让微任务执行完之后再执行下一个宏任务

  • 执行EventLoop时候,会优先去检查微任务队列,清空之后再去检查宏任务队列

EventLoop例子​

js
console.log('start');
setTimeout(() => {
console.log('timeout');
});
Promise.resolve().then(() => {
console.log('resolve');
});
console.log('end');

我们来分析一下:

  1. 刚开始整个脚本作为一个宏任务来执行,对于同步代码直接压入执行栈(关于执行栈,若不了解请移步之前的文章《JavaScript内存机制之问——数据是如何存储的?》)进行执行,因此先打印start和end
  2. setTimeout 作为一个宏任务放入宏任务队列
  3. Promise.then作为一个为微任务放入到微任务队列
  4. 当本次宏任务执行完,检查微任务队列,发现一个Promise.then, 执行
  5. 接下来进入到下一个宏任务——setTimeout, 执行

第一个宏任务script执行之后,然后执行微任务,然后到下一个宏任务setTimeout

因此最后的顺序是:

js
start
end
resolve
timeout

1、一开始整段脚本作为第一个宏任务执行

2、执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列

3、当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空

4、执行浏览器 UI 线程的渲染工作

5、检查是否有Web worker任务,有则执行

6、执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

js
new Promise(resolve => {
console.log('promise')
setTimeout(() => {
console.log(1)
resolve()
console.log(2)
}, 1000)
}).then(res => {
console.log('then')
})
console.log('script')
// promise => script => 1 => 2 =>then
  • 1、遇到new Promise会立即执行,输出promise
  • 2、输出同步任务代码 输出script
  • 3、同步执行完之后,查看微任务,发现then,执行微任务的then
  • 4、由于微任务里面then嵌套了定时器,需要先执行宏任务的定时器,才能执行then
    • 因此输出1、2,最后才执行微任务里的then