资讯

精准传达 • 有效沟通

从品牌网站建设到网络营销策划,从策略到执行的一站式服务

Javascrip事件循环,就看这个,超详细解读-创新互联

在说 Js 事件循环前,我们需要先搞清楚一些相关概念,比如:进程,线程(多线程,单线程),主线程,执行栈,同步任务,异步任务(宏任务、微任务),任务队列(宏队列、微队列)。

创新互联专注于龙子湖企业网站建设,自适应网站建设,商城系统网站开发。龙子湖网站建设公司,为龙子湖等地区提供建站服务。全流程按需定制制作,专业设计,全程项目跟踪,创新互联专业和态度为您提供的服务

进程:每个应用程序至少会有一个进程。例如浏览器应用程序,当浏览器启动后,它会在浏览器内部启动多个进程,如GPU进程,网络进程,渲染进程等。每个进程里可以有一个或多个线程。进程之间也是彼此独立,各自占据一个自己的内存空间。

线程:负责处理进程分配的任务,它没有自己的内存空间,但可以共享使用进程的内存空间。如果把电脑操作系统比喻成一个写字楼,一个应用程序就如同写字楼里的一家公司,一个进程就好比公司里的一个部门,而一个线程就是部门里的一个员工,所有的线程(员工)共享一个进程(部门)的内存(资源)。

多线程:就是多个线程同时在处理一个任务,类似公司部门里多个员工分工合作共同去完成同一个项目。

单线程:就是一个任务,只有一个线程在处理,意味着任务多时,就需要排队等候。

而 JavaScript从诞生开始,大的特点就是单线程,即,同一个时间只能做一件事。JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

作为单线程语言,JavaScript 的执行顺序为从上到下,且同一时刻只能执行一个任务,这就意味着排队。当遇到一个耗时比较长的任务时,主线程不得不停下来,等到前一个任务执行完之后,再开始执行下一个任务,这样很容易造成阻塞。如果排队是因为计算量大,CPU忙不过来也就算了。但由于类似ajax网络请求、setTimeout时间延迟、DOM事件的用户交互等,这些任务并不消耗 CPU,而是一种空等,白白浪费很多时间,很不合理。

为了保证JavaScript 代码能够高效无阻塞地运行,因此出现了事件循环(Event Loop)。

事件循环,包含了 主线程,执行栈,同步任务,异步任务(宏任务,微任务),任务队列(宏队列,微队列)等。

首先,JavaScript 在执行的过程中,会创建一个JavaScript主线程和一个执行栈,并把所有任务分为同步任务和异步任务。同步任务会依次被放入执行栈中,执行栈里的任务(同步任务)以先进先出的方式,被主线程调度执行。异步任务,则会被交给相应的异步模块去处理,一旦异步处理完成后有了结果,就往任务队列的末尾添加注册一个回调事件(回调函数),等待主线程空闲时调用。这样,主线程就只管从执行栈中调度执行同步任务,直到清空执行栈,这时就会去查询任务队列。如果任务队列里有任务,就将排在最前面的任务调入主线程中执行。如果执行时,又有异步任务,就又把异步任务交给异步模块处理,返回结果后,再次往任务队列里注册回调事件。以此类推,形成一个事件环。这样主线程就无需等待,可以不停地执行任务,大大提升了主线程的效率。

主线程:执行JavaScript同步任务的场所。它同一时刻,照顺序每次只能从执行栈中调入一个同步任务并立即执行。

执行栈:存放所有的同步任务,里面的任务以先进先出的方式被主线程调度执行。

同步任务:JavaScript运行后,按照顺序,自动进入执行栈,并依次被主线程调度且立即执行的任务。

console.log(1);
console.log(2);
// console.log() 是同步任务,按照顺序,依次进入执行栈,被主线程调度执行后,依次输出 1,2

异步任务:执行后,存在无法立即处理的任务,主要有setTimeout, setInterval,XHR, Fetch,addEventListener,Promise等。

setTimeout(() =>{
    console.log(1)
}, 1000);
// setTimeout是异步任务,执行后,需要等待1000毫秒后,才会输出 1

异步任务又分宏任务和微任务。

宏任务:setTimeout, setInterval,XHR, Fetch,addEventListener, 宏任务属于一轮事件循环的开头,在事件循环过程中遇到的宏任务,会被放到下一轮循环去执行。script的整体代码在执行时,就是一个宏任务。

微任务:Promise,是一轮事件循环的最后一个环节,所有的微任务执行完,一轮事件循环就结束了。注*(new Promise() 函数本身是同步任务,只有它的then/catch/finally等方法是微任务)。

setTimeout(() =>console.log(1), 0);
Promise.resolve().then(() =>console.log(2));
console.log(3);
const p = new Promise((resolve, reject) =>{
    setTimeout(() =>{
        resolve();
    }, 1000);
    console.log(4);
});
p.then(() =>console.log(5));
// 上述整体代码为一个宏任务开始运行,运行后,JavaScript主线程开始从上到下执行代码
// 第1行代码属于异步任务中的宏任务,其任务类型是延迟任务,放入宏队列中的延迟队列,等候下一轮循环执行
// 第2行代码是一个异步任务中的微任务,放入任务队列中的微队列,等候执行栈空闲后被主线程调用执行
// 第3行代码是同步任务,进入执行栈后,被主线程直接调度且立即执行,输出 3
// 第4行代码,new Promise() 本身属同步任务
// 所以开始执行内部的 setTimeout,发现是个宏任务,放入宏队列,等候下一轮循环执行
// 接着执行同步任务 console.log(4), 直接输出 4
// p.then() 由于 new Promise() 里面的 resolve() 未执行到,目前仍 padding 未完成状态,所以不会被执行
// 此时,执行栈已经清空,主线程开始查询任务队列,发现有一个微任务,调入主线程中执行,于是输出 2
// 至此,本轮事件循环结束,开始第2轮循环,由于第1个 setTimeout 最早在任务队列里注册回调事件,所以先执行
// 于是,在第2轮循环中,console.log(1) 作为一个同步任务,被主线程直接调用执行,输出 1
// 接着开始第3轮循环,执行第2个 setTimeout 里的 resolve(),此时 p 的状态变成了已完成,开始调用then()方法
// p.then() 是一个微任务,于是被放入微队列,但当前循环的执行栈里没有任务,该微任务直接被调入主线程执行,输出 5
// 最终,上述代码的执行结果为:3-4-2-1-5

任务队列:存放着所有异步任返回结果后所注册的回调事件,等待执行栈空闲时主线程的调用。过去把任务队列分为宏队列和微队列,这种说法目前已经无法满足复杂的浏览器环境,取而代之的是一种灵活多变的处理方式。

根据 W3C 官方的解释,不同的任务可以有不同的类型,分属不同队列,不同的队列有不同的优先级,而同类型的任务必须放在同一个队列。在一个事件循环中,必须有一个微队列,相对于其他队列,微队列具有最高的优先级,必须优先调度执行。其他队列则由浏览器自行决定取哪一个队列的任务执行。

setTimeout, setInterval 的任务需要等待计时完成后再执行,属于延迟队列;

XHR, Fetch 的任务需要等待网络通信完成后再执行,属于通信队列;

addEventListener 的任务需要等待用户操作后再执行,属于交互队列;

Promise的then/catch/finally 的任务为微任务,属于微队列。

一般的队列优先情况是: 微队列 >交互队列 >延迟队列。

来看个例子,下面的代码会输出什么?

console.log(1);

setTimeout(() =>{
    fn(2);
    console.log(3);
    setTimeout(() =>console.log(4), 1000);
}, 0);

Promise.resolve().then(() =>{
    console.log(5);
});
fn(6);

function fn(x) {
    console.log(x);
}

// 第一步,执行 console.log(1),这是一个同步任务,直接输出 1;
// 第二步,执行 setTimeout,这是一个这是一个宏任务,留到下一次循环再执行,主线程继续往下执行;
// 第三步,执行 Promise,这也是一个异步任务,交给异步模块处理,继续往后执行;
// 第四步,执行 fn(6),这是一个同步任务,直接执行,输出6;
// 此时,执行栈已经清空没任务了,主线程处于空闲状态,于是就去查询任务队列;
// 发现任务队列的微队列里有一个微任务 Promise.resolve().then(), 于是调入主线程执行,输出5;
// 至此,第一轮事件循环结束。
// 第五步,开始第二轮事件循环,此时,任务队列的延迟队列里有一个 setTimeout 任务,执行它;
// 于是,第六步,执行 fn(2),函数里面的 console.log(x)是一个同步任务,直接输出 2;
// 第六步,执行 console.log(3),同步任务,直接输出 3;
// 第七步,又是一个 setTimeout 宏任务,下一循环执行,第二轮事件循环结束。
// 开始第三轮循环,此时,只有一个 console.log(4) 的同步任务,直接输出 4;
// 最终,上述代码的执行结果为:1-6-5-2-3-4

再看一个例子。

const p = new Promise((resolve, reject) =>{
    setTimeout(() =>{
        console.log(1);
    }, 1000);

    console.log(2);

    setTimeout(() =>{
        Promise.resolve().then(() =>{
            console.log(3);
        });
        console.log(4);
        resolve(5);
    }, 0);
});

p.then(v =>console.log(v)).catch(r =>console.log(r));

console.log(6);

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

// 第一步,执行 new Promise(),注意,new Promise() 本身是一个同步任务于是开始执行 Promise 函数内部的第一个 setTimeout 任务,这个一个宏任务,1000毫秒后会在任务队里注册一个的回调事件a
// 第二步,执行 console.log(2) ,这是一个同步任务,直接输出 2
// 第三步,执行第二个 setTimeout 宏任务,该任务会立即在任务队列里注册一个回调事件b
// 第四步,由于 Promise 目前还仍然处于padding 状态,所以,p.then() 不会执行
// 第五步,执行 console.log(6),直接输出 6
// 第六步,执行一个0秒的 setTimeout 宏任务,立即在任务队列里注册了一个回调事件 c 
// 至此,执行栈已经清空,微队列里没东西,任务队列里有3个宏任务,其顺序为 b, c, a,第一个事件循环结束
// 于是,第二个事件循环开始,主线程开始执行宏任务b
// 第七步,执行任务 b 的 Promise,在微队列里注册了一个微任务
// 第八步,执行 console.log(4),这是一个同步任务,直接输出 4
// 第九步,执行 resolve(5),这是一个同步任务,此时直接将 Promise 的状态变成fulfilled(已成功)
// 此时 p.then(...) 会自动被调用,在微队列里注册了第二个微任务
// 第十步,此时第一个宏任务 b 的执行栈已清空,主线程开始查询任务队列,发现微队列里有两个微任务,开始执行微任务
// 第十一步,执行第一个微任务,输出 3
// 第十二步,执行第二个微任务,输出 5
// 第十三步,执行第二个宏任务 c,输出 7
// 第十四步,执行第三个宏任务 a,输出 1
// 最终,上述代码的执行结果为:2-6-4-3-5-7-1

个人理解,如有说的不对的地方,欢迎留言指正。

参考资料 https://www.ruanyifeng.com/blog/2014/10/event-loop.html

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


网站名称:Javascrip事件循环,就看这个,超详细解读-创新互联
浏览地址:http://www.cdkjz.cn/article/jegec.html
多年建站经验

多一份参考,总有益处

联系快上网,免费获得专属《策划方案》及报价

咨询相关问题或预约面谈,可以通过以下方式与我们联系

大客户专线   成都:13518219792   座机:028-86922220