javascript是顺序执行。JavaScript是单线程语言,执行顺序是自上而下的,也就是说代码在执行过程中,另一段代码想要执行就必须等当前代码执行完成后才可以进行。
本教程 *** 作环境:windows7系统、javascript1.8.5版、Dell G3电脑。
JavaScript 的顺序执行 执行机制1.单线程的JavaScript
大家都知道,JavaScript是单线程语言,执行顺序是自上而下。JavaScript没有多线程的概念,所有的程序都是单线程依次执行的。就是代码在执行过程中,另一段代码想要执行就必须等当前代码执行完成后才可以进行。
注意,JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合
那为什么js是单线程而不能是多线程呢?多线程效率不是更高吗?
JavaScript 之所以采用单线程,而不是多线程,与它的用途有关系,作为网页脚本语言,JavaScript的主要用途是与用户互动,以及 *** 作DOM。
如果 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?是不是还要有锁机制?
JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。
所以,为了避免复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。
常见的浏览器无响应(假死),往往就是因为某一段 JavaScript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
JavaScript 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。
如果排队是因为计算量大,CPU 忙不过来,倒也算了,但是很多时候 CPU 是闲着的,因为 IO *** 作(输入输出)很慢(比如 Ajax *** 作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript 语言的设计者意识到,这时 CPU 完全可以不管 IO *** 作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO *** 作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript 内部采用的“事件循环”机制(Event Loop)。
单线程模型虽然对 JavaScript 构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果用得好,JavaScript 程序是不会出现堵塞的,这就是为什么 Node 可以用很少的资源,应付大流量访问的原因。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得 *** 作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
现在我们看一段代码
function fn(){ console.log('start'); setTimeout(()=>{ console.log('setTimeout'); },0); console.log('end'); } fn() // 输出 start end setTimeout
既然JavaScript的执行顺序是自上而下的,那上面那段代码的执行顺序为什么会被打乱了呢?
因为JavaScript的执行模式有两种:同步 和 异步
2.JavaScript的同步和异步
程序里面所有的任务,可以分成两类:同步任务(synchronous) 和 异步任务(asynchronous)。
什么是同步和异步呢?同步和异步又是如何实现的呢?
同步任务 :是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
异步任务: 是那些被引擎放在一边,不进入主线程、而进入 任务队列 的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax *** 作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。(通俗讲就是 只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。)
排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。
举例来说,Ajax *** 作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax *** 作返回结果,再往下执行;如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax *** 作有了结果,主线程再执行对应的回调函数。
js中包含诸多创建异步的函数如:
seTimeout,setInterval,dom事件,ajax,Promise,process.nextTick等函数
3.任务队列和事件循环
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)
首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。
如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。
异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的 *** 作。
因为单线程,所以代码自上而下执行,所有代码被放到
执行栈
中执行;遇到异步函数将回调函数添加到一个
任务队列
里面;当
执行栈
中的代码执行完以后,会去循环任务队列
里的函数;将
任务队列
里的函数放到执行栈
中执行;如此往复,称为
事件循环
;
JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环。
这样分析,上面那一段代码就得到了合理的解释。
再来看一下这段代码:
function fn() { setTimeout(()=>{ console.log('a'); },0); new Promise((resolve)=>{ console.log('b'); resolve(); }).then(()=>{ console.log('c') }); } fn() // b c a
4.Promise和async 立即执行
Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的。
而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?
await等到之后做了什么?
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到微任务(microtask)中,然后就会跳出整个async函数来执行后面的代码。
不管await后面的代码是同步还是异步,await总是需要时间,从右向左执行,先执行右侧的代码,执行完后,发现有await关键字,于是让出线程,阻塞代码。
由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。
例如:
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); }
等同于
async function async1() { console.log('async1 start'); Promise.resolve(async2()).then(() => { console.log('async1 end'); }) }
5.宏任务和微任务
两个任务分别处于任务队列中的宏队列 和 微队列中;宏任务队列
与微任务队列
组成了任务队列;任务队列
将任务放入执行栈
中执行
宏任务
宏队列,macrotask,也叫tasks。
异步任务的回调会依次进入macro task queue,等待后续被调用。
宏任务一般包括:
- 整体代码script
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
微任务
微队列,microtask,也叫jobs。
异步任务的回调会依次进入micro task queue,等待后续被调用
微任务一般包括:
process.nextTick (Node独有)
Promise
Object.observe
MutationObserver
1.执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等)。
2.全局Script代码执行完毕后,执行栈
Stack会清空。
3.先从微任务队列
中取出位于队首的回调任务,放入执行栈
Stack中执行,执行完后微队列
长度减1。
4.继续循环取出位于微队列
的任务,放入执行栈
Stack中执行,以此类推,直到直到把微任务
执行完毕。注意,如果在执行微任务
的过程中,又产生了微任务
,那么会加入到微队列
的末尾,也会在这个周期被调用执行。
5.微队列
中的所有微任务
都执行完毕,此时微队列
为空队列,执行栈
Stack也为空。
6.取出宏队列
中的任务,放入执行栈
Stack中执行。
7.执行完毕后,执行栈
Stack为空。
8.重复第3-7个步骤。
以上是完成的****事件循环
6.面试题测试
现在我们再来分析一下最开始的那个面试题
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0) async1(); new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); console.log('script end'); /* script start async1 start async2 promise1 script end async1 end promise2 setTimeout */
我们分析一下整个过程:
1.首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。
2.然后我们看到首先定义了两个async函数,接着往下看,然后遇到了 console 语句,直接输出 script start。输出之后,script 任务继续往下执行,遇到 setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中。
3.script 任务继续往下执行,执行了async1()函数,前面讲过async函数中在await之前的代码是立即执行的,所以会立即输出async1 start。
遇到了await时,会将await后面的表达式执行一遍,所以就紧接着输出async2,然后将await后面的代码也就是console.log(‘async1 end’)加入到microtask中的Promise队列中,接着跳出async1函数来执行后面的代码。
4.script任务继续往下执行,遇到Promise实例。由于Promise中的函数是立即执行的,而后续的 .then 则会被分发到 microtask 的 Promise 队列中去。所以会先输出 promise1,然后执行 resolve,将 promise2 分配到对应队列。
5.script任务继续往下执行,最后只有一句输出了 script end,至此,全局任务就执行完毕了。
根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。
因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中, Promise 队列有的两个任务async1 end和promise2,因此按先后顺序输出 async1 end,promise2。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。
6.第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出即可,至此整个流程结束。
再来一个稍微复杂点的代码
function fn(){ console.log(1); setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3); }); },0); new Promise((resolve, reject) => { console.log(4); resolve(5); }).then(data => { console.log(data); }); setTimeout(() => { console.log(6); },0); console.log(7); } fn(); //
流程重现
1.执行函数同步语句
- step1
console.log(1);
执行栈: [ console ]
宏任务: []
微任务: []
打印结果:
1
- step2
setTimeout(() => { // 这个回调函数叫做callback1,setTimeout属于宏任务,所以放到宏队列中 console.log(2); Promise.resolve().then(() => { console.log(3) }); });
执行栈: [ setTimeout ]
宏任务: [ callback1 ]
微任务: []
打印结果:
1
- step3
new Promise((resolve, reject) => { // 注意,这里是同步执行的 console.log(4); resolve(5) }).then((data) => { // 这个回调函数叫做callback2,promise属于微任务,所以放到微队列中 console.log(data); });
执行栈: [ promise ]
宏任务: [ callback1 ]
微任务: [ callback2 ]
打印结果:
1
4
- step4
setTimeout(() => { // 这个回调函数叫做callback3,setTimeout属于宏任务,所以放到宏队列中 console.log(6); })
执行栈: [ setTimeout ]
宏任务: [ callback1 , callback3 ]
微任务: [ callback2 ]
打印结果:
1
4
- step5
console.log(7)
执行栈: [ console ]
宏任务: [ callback1 , callback3 ]
微任务: [ callback2 ]
打印结果:
1
4
7
2.同步语句执行完毕,从微队列
中依次取出任务执行,直到微队列
为空
- step6
console.log(data) // 这里data是Promise的成功参数为5
执行栈: [ callback2 ]
宏任务: [ callback1 , callback3 ]
微任务: []
打印结果:
1
4
7
5
3.这里微队列
中只有一个任务,执行完后开始从宏队列
中取任务执行
- step7
console.log(2);
执行栈: [ callback1 ]
宏任务: [ callback3 ]
微任务: []
打印结果:
1
4
7
5
2
但是执行callback1
的时候遇到另一个Promise,Promise异步执行完毕以后在微队列
中又注册了一个callback4
函数
- step8
Promise.resolve().then(() => { // 这个回调函数叫做callback4,promise属于微任务,所以放到微队列中 console.log(3); });
执行栈: [ Promise ]
宏任务: [ callback3 ]
微任务: [ callback4 ]
打印结果:
1
4
7
5
2
4.取出一个宏任务macrotask执行完毕,然后再去微任务队列microtask queue中依次取出执行
- step9
console.log(3)
执行栈: [ callback4 ]
宏任务: [ callback3 ]
微任务: []
打印结果:
1
4
7
5
2
3
5.微队列
全部执行完,再去宏队列
**中取第一个任务执行
- step10
console.log(6)
执行栈: [ callback3 ]
宏任务: []
微任务: []
打印结果:
1
4
7
5
2
3
6
6.以上全部执行完毕,执行栈
,宏队列
,****微队列
**均为空
执行栈: []
宏任务: []
微任务: []
打印结果:
1
4
7
5
2
3
6
总结
1、代码的检查装载阶段(预编译阶段),此阶段进行变量和函数的声明,但是不对变量进行赋值, 变量的默认值为undefined。
2、代码的执行阶段,此阶段对变量进行赋值和函数的声明。 所以:Js的变量提升和函数提升会影响JS的执行结果,ES6中的let定义的变量不会提升。
3、js的执行顺序,先同步后异步。
4、异步中任务队列的执行顺序: 先微任务microtask队列,再宏任务macrotask队列。
5、调用Promise 中的resolve,reject属于微任务队列,setTimeout等属于宏任务队列 所以:
【同步>异步;微任务>宏任务】
【相关推荐:javascript学习教程】
以上就是javascript是顺序执行吗的详细内容,
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)