您的位置: 翼速应用 > 业内知识 > web前端 > 正文

JavaScript 的顺序执行 执行机制

本文是关于前端技术的一定知识点了解。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),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)

时间循环.png

首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。

如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。


异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。

事件循环2.png

因为单线程,所以代码自上而下执行,所有代码被放到执行栈中执行;


遇到异步函数将回调函数添加到一个任务队列里面;


当执行栈中的代码执行完以后,会去循环任务队列里的函数;


将任务队列里的函数放到执行栈中执行;


如此往复,称为事件循环;


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

php任务.pngQQ截图20220209155842.png

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、代码的检查装载阶段(预编译阶段),此阶段进行变量和函数的声明,但是不对变量进行赋值, 变量的默认值为undefined

2、代码的执行阶段,此阶段对变量进行赋值和函数的声明。 所以:Js的变量提升和函数提升会影响JS的执行结果,ES6中的let定义的变量不会提升。

3、js的执行顺序,先同步后异步。

4、异步中任务队列的执行顺序: 先微任务microtask队列,再宏任务macrotask队列。

5、调用Promise 中的resolvereject属于微任务队列,setTimeout等属于宏任务队列 所以:


以上就是JavaScript 的顺序执行 执行机制文章的详细内容,更多关于JavaScript 相关文章请关注翼速应用平台阅读了解!


我来说两句

0 条评论

推荐阅读

  • 响应式布局CSS媒体查询设备像素比介绍

    构建响应式网站布局最常见的是流体网格,灵活调整大小的站点布局技术,确保用户在使用的幕上获得完整的体验。响应式设计如何展示富媒体图像,可以通过以下几种方法。

    admin
  • 提升网站的性能快速加载的实用技巧

    网站速度很重要,快速加载的网站会带来更好的用户体验、更高的转化率、更多的参与度,而且在搜索引擎排名中也扮演重要角色,做SEO,网站硬件是起跑线,如果输在了起跑线,又怎么跟同行竞争。有许多方法可提升网站的性能,有一些技巧可以避免踩坑。

    admin
  • 织梦CMS TAG页找不到标签和实现彩色标签解决方法

    织梦cms是我们常见的网站程序系统的一款,在TAG标签中常常遇到的问题也很多。当我们点击 tags.php 页的某个标签的时候,有时会提示:“系统无此标签,可 能已经移除!” 但是我们检查程序后台,以及前台显示页面。这个标签确实存在,如果解决这个问题那?

    admin
  • HTML关于fieldset标签主要的作用

    在前端开发html页面中常用的标签很多,今天为大家带来的是关于HTML中fieldset标签主要的作用说明,根据技术分析HTML

    admin

精选专题