5

这次彻底了解JavaScript执行机制

 2 years ago
source link: https://developer.51cto.com/article/707318.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
这次彻底了解JavaScript执行机制-51CTO.COM
这次彻底了解JavaScript执行机制
2022-04-25 09:03:16
这篇文章的目的是为了让你彻底理解 JavaScript 的执行,如果你到本文最后还没有理解,你可以揍我一顿。

174ea1a66af2d3101e6144a8b2f6db647ce59d.jpg

无论你是 JavaScript 新手还是老手,无论你是在面试工作,还是只是做常规的开发工作,通常会发现给定几行代码,你需要知道要输出什么以及以什么顺序输出 . 由于 JavaScript 是一种单线程语言,我们可以得出以下结论:

let a = '1';
console.log(a);
let b = '2';
console.log(b);

然而,JavaScript 实际上是这样的:

setTimeout(function(){
    console.log('start')
});
new Promise(function(resolve){
    console.log('start for');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('start then')
});
console.log('end');
// Following the idea that JS executes in the order in which the statements appear, I confidently write down the output:
// start
// start for
// start then
// end

在 Chrome 上查看它是完全错误的😝

一、 关于 JavaScript

JavaScript 是一种单线程语言。Web-worker 是在最新的 HTML5 中提出的,但 JavaScript 是单线程的核心保持不变。所以所有 JavaScript 版本的“多线程”都是用单线程模拟的,所有的 JavaScript 多线程都是纸老虎!

二、JavaScript 事件循环

由于 JavaScript 是单线程的,它就像一个只有一个窗口的银行。客户需要一一排队办理业务。

同样,JavaScript 任务也需要一个一个地执行。如果一项任务花费的时间太长,则下一项也必须等待。

那么问题来了,如果我们想浏览新闻,但新闻中包含加载缓慢的超高清图像,我们的网页是否应该一直卡住直到图像完全显示?所以聪明的程序员将任务分为两类:

当我们打开一个网站时,页面的渲染过程是很多同步任务,比如渲染页面骨架和页面元素。

需要大量时间的任务,比如加载图片和音乐,都是异步任务。这部分有严格的文字定义,但本文的目的是以最小的学习成本彻底理解实现机制,所以我们用一张图来说明:

b98309a22c8aeaa63a7465f99ca38ef5ec5fa6.jpg

文字要表达的内容:

同步和异步任务去不同的执行“地方”,同步任务去主线程,异步任务去事件表和注册函数。

当指定的事件完成时,事件表将此函数移至事件队列。

如果执行后主线程中的任务为空,事件队列会读取相应的函数,进入主线程执行。

这个过程一遍又一遍地重复,称为事件循环。

我们怎么知道主线程栈是空的?JavaScript 引擎有一个监控进程,不断检查主线程堆栈是否为空,如果是,则检查 Event Queue 以查看是否有任何函数等待调用。

说了这么多,不如直接写一段代码:

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('success!');
    }
})
console.log('end');

这是一个简单的ajax请求代码:

  • ajax 去事件表并注册回调函数成功。
  • 执行 console.log(‘success’)。
  • ajax Event 完成,回调函数success 进入Event Queue。
  • 主线程从事件队列中读取成功并执行。

相信通过上面的文字和代码,你对JS的执行顺序有了初步的了解。接下来,我们来看看进阶话题:setTimeout。

三、 爱恨交加超时

著名的 setTimeout 无需进一步解释。setTimeout 的第一印象是异步执行可以延迟,我们经常这样实现:

setTimeout(() => {
 console.log(‘Delay 3 seconds’);
},3000)

当 setTimeout 用得越来越多时,问题也出现了。有时函数会在 3 秒的书面延迟后 5 或 6 秒内执行。怎么了?

让我们从一个例子开始:

setTimeout(() => {
    task();
},3000)
console.log('console');

按照我们之前的结论,setTimeout是异步的,应该先执行console.log。

//console
//task()

去看看吧!这是正确的!然后我们修改之前的代码:

setTimeout(() => {
    task()
},3000)
sleep(10000000)

控制台上的 task() 在 Chrome 中执行需要超过 3 秒的时间。

此时,我们需要重新思考setTimeout的定义。

先说上面的代码是如何执行的:

  • task() 进入事件表并注册,定时器启动。
  • 执行sleep,非常慢,非常慢,计时继续。
  • task()进入Event Queue,但是,sleep太慢无法执行。
  • sleep终于结束了,task()终于从Event Queue执行到主线程。

上述过程完成后,我们知道setTimeout是一个在指定时间后将任务添加到Event Queue(本例中为task())的函数。

而且,由于是单线程任务,需要一个一个执行,如果上一个任务耗时过长,我们只能等待。导致实际延迟超过 3 秒。

SetTimeout(fn,0) 是我们经常遇到的另一个代码。可以立即完成吗?

SetTimeout (fn,0) 指定任务将在主线程上最早可用的空闲时间执行。这意味着一旦堆栈中的所有同步任务完成并且堆栈为空,主线程将立即执行。例如:

//code1
console.log('one');
setTimeout(() => {
    console.log('two')
},0);
// result
// one  
// two
//code2
console.log('one');
setTimeout(() => {
    console.log('two')
},3000);
// result
// one
// ... 3s later
// two

关于 setTimeout 要补充的一点是,即使主线程是空的,0 毫秒实际上也是无法到达的。根据 HTML 标准,最小值为 4 毫秒。有兴趣的同学可以自行了解。

四、 恨与爱setInterval

说了 setTimeout,你不能错过它的孪生兄弟 setInterval。它们是相似的,只是后者是循环执行。对于执行顺序,setInterval 将按指定的时间间隔将注册的函数放入事件队列中。如果上一个任务耗时过长,也需要等待。

唯一需要注意的是,对于 setInterval(fn,ms),我们已经知道不是每 ms 秒执行一次 fn,而是每 ms 秒进入 Event Queue。一旦 setInterval 的回调 fn 花费的时间超过了延迟 ms,时间间隔就完全不可见了。请读者细细品味这句话。

五、 Promise 和 process.nextTick(callback)

我们已经看过传统的计时器,然后,我们将探讨 Promise 与 process.Nexttick(回调)的性能。

Promise 的定义和功能这里就不介绍了,process.nexttick(回调)类似于node.js 版本的“setTimeout”,在事件循环的下一次迭代中调用回调函数。

我们开始谈正事吧。除了广义的同步和异步任务,我们对任务有更详细的定义:

  • 宏任务:包括整个代码脚本、setTimeout 和 setInterval
  •  微任务:Promise、process.nexttick

不同类型的任务会进入对应的Event Queue。例如,setTimeout 和 setInterval 将进入同一个事件队列。

事件循环的顺序决定了 JS 代码的执行顺序。输入整体代码(宏任务)后,第一个循环开始。然后,执行所有微任务。然后再从宏任务开始,找一个任务队列完成,然后,执行所有的微任务。如果听起来有点绕,我们用本文开头的代码来说明:

setTimeout(function() {
    console.log('setTimeout');
})
new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})
console.log('console');

  • 此代码作为宏任务进入主线程。
  • 当遇到 setTimeout 时,将其回调函数注册并分发到宏任务 Event Queue。(注册过程同上,下面不再赘述)。
  • 然后,遇到一个 Promise,立即执行 New Promise,然后将 then 函数分派到微任务事件队列中。如果遇到console.log(),立即执行。
  • 好的,整个脚本作为第一个宏任务执行。什么是微任务?我们发现 then 是在 microtask Event Queue 中执行的。
  • 好了,第一轮的 Event loop 已经结束了,让我们开始第二轮,当然是从宏任务 Event Queue 开始。我们在宏任务Event Queue中找到setTimeout对应的回调函数,立即执行。

事件循环、宏任务和微任务的关系如下图所示:

73987409421cf381edc7069585112e7960d039.jpg

让我们看一些更复杂的代码,看看你是否真的了解 JS 的工作原理:

console.log('1');
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})
setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

第一轮事件循环流程分析如下:

整个脚本作为第一个宏任务进入主线程,遇到console.log,打印1。

  • 当遇到 setTimeout 时,它的回调函数被调度到宏任务事件队列。我们称之为 setTimeout1。
  • 当遇到 process.nexttick() 时,将其回调函数调度到微任务事件队列中。我们称它为 process1。
  • 如果遇到 Promise,直接执行新的 Promise,打印 7,然后,分发到微任务 Event Queue,让我们称之为then1。
  • 再次遇到setTimeout,它的回调函数被分发到宏任务Event Queue中,我们称之为setTimeout2。

c773800792b91802a1c208a50be3053ef8245a.jpg

  • 上表展示了第一轮Event loop的宏任务结束时各个Event Queue的情况。这时候已经输出了1和7。
  • 我们找到了两个微任务 process1 和 then1。
  • 执行process1,输出6。
  • 执行 then1 print 8。

好了,第一轮事件循环正式结束,本轮结果输出1,7,6,8。所以第二个时间循环从 setTimeout1 宏任务开始:

首先,print2。接下来是process.nexttick(),它也被分派到微任务事件队列中,称为process2。新的 Promise 立即执行输出 4,然后也被分发到微任务事件队列中,记为 then2。

5345fe872541f0d932a5197a5561014a21c5f4.jpg

  • 第二轮事件循环宏任务完成,我们发现 process2 和 then2 微任务可以执行。
  •  3的输出。
  • 5的输出。
  • 第二个事件循环结束,第二轮输出2,4,3,5。
  • 第三个事件循环开始,此时只剩下setTimeout2,执行。
  • 所以它只print 9。
  • 将 process.nexttick() 分发到微任务事件队列。记得process 3。
  • 只需执行 new Promise,print 11。
  • then 分配 microtask Event Queue,记为 then3。

c9f9dac93c640954ffe3162cc0a34b9fe29e85.jpg

  • 第三轮事件循环宏任务执行完成,执行两个微任务process3和then3。
  • 10 的输出。
  • 12的输出。
  • 第三轮事件循环结束。第三轮输出9,11,10,12。

整个代码,一共经过了3次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。

六、写在最后

1、异步 JavaScript

我们从一开始就说过 JavaScript 是单线程语言,无论什么新的框架和语法实现被称为异步,实际上都是以同步的方式模拟的,所以牢牢掌握单线程很重要。

2、事件循环

事件循环是实现异步 JavaScript 的一种方法,也是 JavaScript 的执行机制。

3、JavaScript的执行和运行

在 Node.js、浏览器、Ringo 等不同的环境中执行和运行 JavaScript 是有很大区别的。虽然运行多指 JavaScript 解析引擎,但它是统一的。

4、立即设置

还有许多其他类型的微任务和宏任务,例如 setImmediate,它们不进行中介。

5、在一天结束时

JavaScript 是一种单线程语言。事件循环是 JavaScript 的执行机制。

牢牢把握两个基本点,以认真学习JavaScript为中心,早日实现成为前端高手的伟大梦想!😆


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK