Skip to content

单线程和异步

单线程 ---> 异步[解决单线程]

异步 ---> 事件循环[异步的实现方案]

单线程

js是单线程语言,运行在浏览器的渲染主线程当中,主线程只有一个

js引擎和Dom操作共用同一个线程

  • js为什么是单线程

防止多个线程对于Dom节点造成不可控的修改,降低复杂度,JavaScript选择只用一个主线程来执行代码,以保证代码执行的一致性;

img.png

同步和异步

  • JS是单线程语言,只能同时做一件事
  • 浏览器和Node.js已经支持JS启动进程,如:Web Worker
  • JSDOM渲染共用同一个线程,因为JS可修改DOM结构(JSDOM只能同时有一个进行)
  • 遇到等待(网络请求,定时任务),不能卡主
  • 需要异步
  • 回调callback函数形式
  • 异步不会阻塞代码执行,
  • 同步会阻塞代码执行

同步

依次执行,会阻塞代码的执行,

比如:

javascript
console.log(1);
alert('阻塞了...');
console.log(2); // 只有当alert消失,才会到这一步

js会阻塞页面的渲染

js代码执行和dom渲染共用一个线程,要么等js执行完,要么等dom渲染完,才会执行下一步

html
<div id="app">app</div>
<button onclick="load()">load</button>
<script>
    function delay(delay) {
        var start = Date.now();
        while (Date.now() - start < delay) {
        }
    }

    function load() {
        document.getElementById('app').innerHTML = '阻塞了...';
        delay(3000);
    }
</script>

异步

javascript
console.log('start');

setTimeout(function () {
    console.log('async');
},0 );

while (1) {
}
// 最终只输出 start,因为while(1)是死循环,会一直占用主线程,不会执行setTimeout

各自执行,互不影响,不会阻塞代码执行

TIP

异步是将回调函数包装成一个任务,放到任务队列中,等主线程任务执行完成再去队列依次执行

异步的代码会依次进入到队列当中,等主线程任务执行完成再去队列依次执行

异步的应用

  • 网络请求

ajax图片加载

  • 定时任务

setTimeout

  • 事件处理

onclick

执行栈

js在解析同步任务的时候,会将这些任务按照执行顺序排列到一个地方,这个地方就叫做执行栈

调用一个函数总是会为其创造一个新的栈帧。

栈:后进先出

事件队列

event loop [时间循环] 或者 message queue [消息队列]

js会将异步任务按照执行顺序,加入到与执行栈不同的另一个队列,也就是事件队列。

队列:先进先出

函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。

浏览器的事件循环

image.png

执行步骤:

  • 1、js从上到下解析方法,主线程将同步任务添加到执行栈;
  • 2、当碰到ajax、setTimeout等异步任务时,会暂时挂起,继续执行执行栈中的任务, 等异步任务返回结果,再按照执行顺序排列到事件队列中;
  • 3、主线程先将执行栈中同步任务清空,检查事件队列是否有任务,如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,继续排到事件队列;
  • 4、主线程每次清空执行栈,就去检查事件队列是否有任务,如果有就取出一个推到执行栈,这个过程是循环往复的...这个过程就叫做EventLoop事件循环。

最新标准

主线程: 微队列: 交互队列: 延时队列:

宏任务和微任务

实际上异步任务之间也不相同,执行优先级也有区别。不同的异步任务被分为两类: 宏任务(macro task)和微任务(micro task)。我们将经常遇到的异步任务进行分类如下:

宏任务:setTimeout,setInterval,setImmediate,I/O(磁盘读写或网络通信),UI交互事件 微任务:process.nextTick,Promise.then

image.png

当执行栈中的任务清空,主线程会先检查微任务队列中是否有任务,如果有,就将微任务队列中的任务依次执行,直到微任务队列为空,之后再检查宏任务队列中是否有任务,如果有,则每次取出第一个宏任务加入到执行栈中,之后再清空执行栈,检查微任务,以此循环... ...

同一次事件循环中,微任务永远在宏任务之前执行。

一次事件循环只执行处于 Macrotask 队首的任务,执行完成后,立即执行 Microtask 队列中的所有任务。

await后的内容为宏任务,await下的内容为异步任务

javascript
console.log('script start')

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

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

async1()

setTimeout(function () {
    console.log('setTimeout')
}, 0)

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

console.log('script end')

// 新版输出(新版的chrome浏览器优化了,await变得更快了,输出为)

// script start => async2 end => Promise => script end => async1 end => promise1 => promise2  => setTimeout
// 注意一个点await async2() 执行完后面的任务才会注册到微任务中


// 旧版输出如下,但是请继续看完本文下面的注意那里,新版有改动
// script start => async2 end => Promise => script end => promise1 => promise2 => **async1 end** => setTimeout

总结

  1. js解析方法时,将同步任务排队到执行栈中,异步任务排队到事件队列中。
  2. 事件队列分为:
  • 宏任务:setTimeout,setInterval,setImmediate,I/O,UI交互事件
  • 微任务:process.nextTick,Promise.then
  1. 浏览器环境中执行方法时,先将执行栈中的任务清空,再将微任务推到执行栈中并清空,之后检查是否存在宏任务,若存在则取出一个宏任务,执行完成检查是否有微任务,以此循环…

TIP

先将宏任务中的执行栈清空再去清空微任务队列,再执行下一个宏任务。

例题

例题1

javascript
console.log('start')

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

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

console.log('end')

start
// 存宏任务和微任务,不会直接执行
end
// 栈空,执行微任务队列中的任务(经常会有坑,需要注意一下)
promise1
promise2
// 再下一轮eventloop执行宏任务
setTimeout

例题2

javascript
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')

// 从上往下看,定义的两个async函数先别管(执行再说)
script start
//setTimeout放到宏任务队列
// 执行了async1(异步函数)
async1 start
// await 后面是 async2(),执行 async2()异步函数
async2
// await 后面的代码是微任务,放入微任务队列
promise1
// .then()放到微任务队列
script end
// 同步代码(同时也是宏任务)执行完成,接着执行微任务
async1 end
promise2
// 微任务队列清空,执行宏任务
setTimeout

例题3

javascript
console.log('start')

setTimeout(() => {
  console.log('children2')
  Promise.resolve().then(() => {
    console.log('children3')
  })
}, 0)

new Promise((resolve, reject) => {
  console.log('children4')
  setTimeout(() => {
    console.log('children5')
    resolve('children6')
  }, 0)
}).then((res) => {
  console.log('children7')
  setTimeout(() => {
    console.log(res)
  }, 0)
})

start
// 遇到setTimeout,放入宏任务队列,称为宏任务1
children4
// 遇到setTimeout,放入宏任务队列,称为宏任务2
// 遇到.then  !!!不会被放到微任务队列!!!,因为resolve 是放到 setTimeout中执行的。
// 执行完检查微任务队列是空的,于是执行宏任务1
children2
// 把Promise.resolve().then放入微任务队列
// 宏任务队列1空了,检查微任务队列,执行微任务
children3
// 微任务清空,执行宏任务2
children5
// .then放入微任务队列,宏任务2执行完,执行微任务
children7
// setTimeout放入宏任务中,微任务执行完后执行
children6