这里有一份简洁的前端知识体系等待你查收,看看吧,会有惊喜哦~如果觉得不错,恳求star哈~
JS单线程
JS 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事,这跟它的用途有关。
作为浏览器脚本语言,JS 的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JS 同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JS 语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务,另一种是异步任务。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;最直观的感受就是,如果在函数返回的时候,调用者就能够得到预期结果。
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。最直观的感受就是,如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,比如回调函数。
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
Event Loop(事件循环)
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
微观任务和宏观任务
ES6标准中,任务队列又分为宏观任务队列和微观任务队列。
简单的说,宿主发起的任务称为宏观任务,JS 引擎发起的任务称为微观任务。
宏观任务
在宏观任务中,JS 的Promise还会产生异步代码,JS 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列:
微观任务
设计微任务的初衷是为了让 JS 引擎可以执行异步任务,不需要通过浏览器发起异步。
说到微观任务,就不得不提到Promise。
Promise是 JS 语言提供的一种标准化的异步管理方式,它的总体思想是,需要进行io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(通过Promise的then方法的回调)。
简单点说,Promise 翻译过来就是承诺的意思,这个承诺会在未来有一个确切的答复,并且该承诺有三种状态,分别是:
- 等待中(pending)
- 完成了 (resolved)
- 拒绝了(rejected)
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变。
当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的。
new Promise((resolve, reject) => {
console.log('new Promise')
resolve('success')
})
console.log('finifsh')
// new Promise -> finifsh
Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装。
Promise.resolve(1)
.then(res => {
console.log(res) // => 1
return 2 // 包装成 Promise.resolve(2)
})
.then(res => {
console.log(res) // => 2
})
JS 引入Promise的意义非常重大。因为有了Promise,不需要浏览器的安排,JS 引擎本身也可以发起任务了。
微观任务和宏观任务的执行顺序
这是一个面试中常见的问题,其实归结起来就三句话:
- 首先我们分析有多少个宏任务;
- 在每个宏任务中,分析有多少个微任务,所有微任务执行完毕才执行下一个宏任务;
- 反复进行以上步骤
添加了微观任务队列之后事件循环有什么变化呢?在调用栈执行空之后,主线程读取任务队列时,会先读取所有微观任务队列,然后读取一个宏观任务队列,再读取所有的微观任务队列。如图:
好了,说了很多概念上的东西,不如一段代码来得清晰:
console.log ('sync1');
setTimeout (function () {
console.log ('setTimeout1');
}, 0);
var promise = new Promise (function (resolve, reject) {
setTimeout (function () {
console.log ('setTimeoutPromise');
}, 0);
console.log ('promise');
resolve ();
});
promise.then (() => {
console.log ('pro_then');
setTimeout (() => {
console.log ('pro_timeout');
}, 0);
});
setTimeout (function () {
console.log ('last_setTimeout');
}, 0);
console.log ('sync2');
首先我们看第一遍同步执行,这是第一个宏任务。
第一个宏任务中(<script> 标签包裹的,就是宏任务),调用了三次setTimeout(Promise中的代码也是同步执行的),调用了一次resolve,打印了三次。
所以它产生了三个宏任务,一个微任务,两次打印。
那么,首先显示的就是 sync1、promise 和 sync2。这时,setTimeout1,setTimeoutPromise,last_setTimeout在宏任务队列中,pro_then在微任务队列中。
接下来,因为微任务队列没空,第一个宏任务没有结束,继续执行微任务队列,所以pro_then,被显示出来,然后又调用了一次setTimeout,所以pro_timeout进入宏任务队列,成为第5个宏任务。
然后,没有微任务了,执行第二个宏任务,所以接下来顺次执行宏任务,显示setTimeout1,setTimeoutPromise,last_setTimeout,pro_timeout。
注意:resolve在哪里调用,产生的微任务就属于哪个宏任务。
最终显示顺序是这样的:
- 宏任务1
- 微任务1
- sync 1
- promise
- sync 2
- 微任务2
- pro_then
- 微任务1
- 宏任务2
- setTimeout1
- 宏任务3
- setTimeoutPromise
- 宏任务4
- last_setTimeout
- 宏任务5
- pro_timeout
结语
在这篇文章中,我们学习了 JS 执行部分的知识,首先我们学习了 JS 的宏观任务和微观任务相关的知识。我们把宿主发起的任务称为宏观任务,把 JS 引擎发起的任务称为微观任务。许多的微观任务的队列组成了宏观任务。
除此之外,我们还展开介绍了用Promise来添加微观任务的方式,并且介绍了async/await这个语法的改进。