js是单线程语言,只能同时做一件事儿。
原因:js作为浏览器脚本语言,主要是实现与用户交互,以及操作DOM。这决定了它只能是单线程,否则会带来复杂的同步问题。比如,假定js同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?没有办法处理,若是单线程就比较简单了,用户先执行那个操作,线程就进行哪一个,不会出现冲突。
但是作为单线程语言,缺点也很明显,如果前面的某部分代码执行时间过长,那么后面的代码就必须一直等待下去,会拖延整个程序。对此js是怎么解决的呢?
异步和同步
同步即是从上到下按照顺序一步一步的执行程序,是会阻塞程序运行的代码,前面的未执行完,后面的无法执行。
console.log(1)
console.log(2)
console.log(3)
按从上到下顺序输出1 2 3
异步则是不会阻塞程序运行的代码,异步的代码没有执行完毕,不会阻塞后面的代码执行。
setTimeout(() => {
console.log(1)
}, 1000)
setTimeout(() => {
console.log(2)
}, 100)
setTimeout(() => {
console.log(3)
}, 10)
按照时间顺序输出3 2 1
那么问题来了,Javascript作为一个单线程语言,怎么实现的异步?
javascript运行环境——浏览器
浏览器是JavaScript语言的摇篮,也是它的栖息地之 一。脱离了环境,JavaScript代码是不能够运行的,所以在解答以上问题前,先来了解下浏览器。
多进程的浏览器:
浏览器,是一种多进程的架构设计,主要包含以下4种进程
浏览器内核:
即浏览器底层最核心和最基础的那一部分,它主要负责对网页当中的html、css、JavaScript进行解释然后在浏览器当中进行渲染最终呈现给用户,也就是说内核的工作就是渲染,所以常常把浏览器内核称为渲染引擎。由5种线程组成
与JavaScript单线程语言不同,浏览器内核属于多线程的。
回到前面,Javascript作为一个单线程语言,怎么实现的异步?
JavaScript的异步机制主要是由运行环境提供的,如浏览器举例,分析如下代码:
setTimeout(() => {
console.log(1)
}, 1000)
console.log(2)
setTimeout(() => {
console.log(3)
}, 10)
// 2 3 1
JS引擎线程按顺序执行代码,当遇到定时器任务,便将其指派给定时器触发线程,然后,继续执行接下来的代码。
总结:JS本身作为单线程语言,自身无法实现异步,需要依赖于它的运行环境,浏览器内核便是其一,浏览器内核含有的多个线程能够帮助JS达到异步的目的。
解释说明:
进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程。
线程:进程中的一个执行任务,负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
同步任务与异步任务
同步任务:指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务。
异步任务:指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。异步任务又细分为宏任务和微任务。
宏任务:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
微任务: Promise, Object.observe, MutationObserver
执行顺序:同步任务 > 微任务 > 宏任务(面试题常考,不论是笔试还是面试!!!)
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
Promise.resolve().then(() => {
console.log(3);
});
console.log(4);
// 1 4 3 2
任务队列
js中有两类任务队列:宏任务队列和微任务队列。宏任务队列可以有多个,微任务队列只有一个,宏任务进宏任务队列,微任务进微任务队列。 当任务进入到任务队列后并不会立即执行,而是会等主线程处理完了自己的事情后,再来执行任务队列中的任务。
浏览器的事件循环
当了解完所有的小知识点后,来串一下这些知识点。
最开始,只有宏任务队列里有一个 script(整体代码),然后推入执行栈。
script(整体代码)在执行栈中开始划分同步任务和异步任务。
同步任务开始执行,执行结束后出栈。中途如遇到异步任务,会开始划分微任务与宏任务,并将其推进微任务队列和宏任务队列。
当执行栈中的同步任务全部执行完,执行栈清空了,开始执行微任务,逐一将微任务从微任务队列中加入执行栈中执行。
当执行栈中的微任务被执行完了,执行栈清空了,再逐一将宏任务从宏任务队列中加入执行栈中执行,直至执行栈再一次清空,结束。
这部分是面试中高频出现的内容,大部分会以代码题形式出现,给一段代码,然后说出执行结果的输出先后顺序。下面来看几道题,学习解题规则。
new Promise(function (resolve) {
console.log(1);
resolve();
}).then(function () {
console.log(2)
})
console.log(3)
分析:Promise本身是同步的,但是then() ,catch()方法是异步的
答案:1 3 2
同步 | 微任务 | 宏任务 |
console.log(1) | console.log(2) | |
console.log(3) |
setTimeout(() => console.log(1), 0)
new Promise(res => {
console.log(2)
setTimeout(() => console.log(3), 0)
res()
}).then(() => console.log(4))
console.log(5)
答案:2 5 4 1 3
同步 | 微任务 | 宏任务 |
console.log(2) | console.log(4) | console.log(1) |
console.log(5) | console.log(3) |
async function fn1() {
console.log(1)
await fn2()
show()
console.log(2)
}
async function fn2() {
console.log(3)
}
function show() {
console.log(4)
}
console.log(5)
fn1()
console.log(6)
分析:async函数,当函数被调用时,程序会正常立即执行,但是当碰到await关键词时,await下面的语句会作为微任务加入到微任务队列中,await后面跟着的部分也是会立即执行的
5 1 3 6 4 2
同步 | 微任务 | 宏任务 |
console.log(5) | console.log(4) | |
console.log(1) | console.log(2) | |
console.log(3) | ||
console.log(6) |
let p;
async function f1() {
console.log(1)
p = await 3;
console.log(p)
}
f1();
console.log(2);
console.log(p);
答案:1 2 undefined 3
分析:await 3会立即执行,后面的进入微任务,包括把3赋值给p
同步 | 微任务 | 宏任务 |
console.log(1) | console.log(3) | |
console.log(2) | ||
console.log(undefined) |
setTimeout(() => {
Promise.resolve().then(() => {
console.log(3)
})
console.log(2)
})
new Promise((resolve) => {
console.log(4)
setTimeout(() => {
console.log(5)
resolve();
}, 2);
}).then(
console.log(1)
);
分析:当发现宏任务包含微任务,给他拆分,即所谓事件“循环”
答案:4 1 2 3 5
同步 | 微任务 | 宏任务 |
console.log(4) | console.log(1) | Promise.resolve().then(() => { console.log(3) }) console.log(2) |
同步 | 微任务 | 宏任务 |
console.log(2) | console.log(3) | console.log(5) |
//美团一面题目(属于上一道题进阶版)
setTimeout(() => {
Promise.resolve().then(() => {
console.log(3)
})
console.log(2)
})
new Promise((resolve) => {
console.log(4)
setTimeout(() => {
console.log(5)
resolve();
}, 2);
}).then(res => {
console.log(res)
});
分析 注意行12写在了setTimeout中,那么自动的行15得在resolve()之后执行,由于resolve()未传参,res = undefined
//4 2 3 5 undefined
同步 | 微任务 | 宏任务 |
console.log(4) | Promise.resolve().then(() => { console.log(3) }) console.log(2) | |
同步 | 微任务 | 宏任务 |
console.log(2) | console.log(3) | console.log(5) console.log(undefined) |
setTimeout(() =>
new Promise(res => {
console.log(6)
setTimeout(() => console.log(7), 0)
res()
}).then(() => console.log(8))
, 0)
new Promise(res => {
console.log(2)
setTimeout(() => console.log(3), 0)
res()
}).then(() => console.log(4))
分析:回顾事件循环流程,当执行栈中的同步任务全部执行完,宏、微任务早已经全部进入了任务队列中。
当执行到表格中紫色部分,表示它已经出队列了,但是由于发现它内部还有微任务,会再次让它‘循环一遍’,那么相应的console.log(7)重新进入宏任务队列,排在了console.log(3)之后
答案:2 4 6 8 3 7
同步 | 微任务 | 宏任务 |
console.log(2) | console.log(4) | new Promise(res => { console.log(6) setTimeout(() => console.log(7), 0) res() }).then(() => console.log(8)) |
同步 | 微任务 | 宏任务 |
console.log(6) | console.log(8) | console.log(3) console.log(7) |
分析图解(一个粗糙的图解):
最后自测一下学习成果吧,答案在最后
async function fn1() {
console.log(1)
await fn2()
console.log(2)
}
async function fn2() {
console.log(3)
}
console.log(4)
setTimeout(function () {
console.log(5)
}, 0)
fn1()
new Promise(function (resolve) {
console.log(6);
resolve();
}).then(function () {
console.log(7)
})
console.log(8)
答案:
4 1 3 6 8 2 7 5