从底层看前端(五)——JavaScript代码执行(一)

 2024-03-17 02:06:28  阅读 0

今天我们来谈谈执行力。

首先我们想一下,如果我们是浏览器或者node开发者,我们应该如何使用引擎呢?

当获取一段代码时,浏览器或节点环境要做的第一件事就是将其传递给引擎并要求其执行。

然而,执行并不是一劳永逸的事情。 当宿主环境遇到一些事件时,它会不断地传递一段代码给引擎执行。 另外,我们还可能向引擎提供API,比如这个API,它会允许代码在特定的时间执行。

因此,我们首先应该形成一个感性的认识:引擎会驻留在内存中,等待我们(主机)将代码或函数传递给它执行。

在 ES3 或更早版本中,无法异步执行代码。 这意味着宿主环境将一段代码传递给引擎,引擎直接按顺序执行该代码。 该任务也是主机发起的任务。 。

然而ES5之后,引入了它。 这样引擎本身就可以发起任务,而不需要浏览器的安排。

由于这里主要讲语言,因此我们也采用JSC引擎的术语,将主机发起的任务变成宏任务。 将引擎启动的任务称为微任务。

宏任务和微任务

引擎等待宿主环境分配宏任务。 在操作系统中,等待行为通常是一个事件循环,因此在节点术语中,这部分也称为事件循环。

然而,术语本身并不是我们讨论的重点。 我们在这里重点讨论事件循环的原理。 在底层C/C++代码中,这个事件循环是一个在独立线程中运行的循环。 我们用伪代码来表示,大致是这样的:

while (true){
    r = wait();
    execute(r)
}

我们可以看到整个循环所做的基本上就是重复“等待-执行”。 当然,实际的代码并没有那么简单。 还需要判断循环是否结束、宏任务队列等逻辑。

这里的每一个执行过程实际上都是一个宏任务。 我们可以粗略地理解宏任务队列相当于事件循环。

在宏任务中,也会产生异步代码,必须保证这些异步代码在宏任务中完成。 因此,每个宏任务还包含一个微任务队列。

后台调用前台js代码_js调用后端接口_控制台调用js

通过宏任务和微任务机制,我们可以实现引擎级和主机级任务,比如始终将微任务添加到队列末尾。 等待主机API,将其添加到宏任务中。

接下来我们就来详细介绍一下。

它是语言提供的标准化异步管理方法。 它的总体思路是,需要执行io、等待或其他异步操作的函数不返回真实结果,而是返回一个。 函数的调用者可以选择等待此成功实现(通过then方法的回调)。

基本用法示例如下:

function sleep(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}
sleep(1000).then( ()=> console.log("finished"));

这段代码定义了一个sleep,其作用是等待传入参数指定的时间长度。

then回调是一个异步执行过程。 我们来研究一下函数中的执行顺序。 我们来看一个代码示例:

var r = new Promise(function(resolve, reject){
	console.info('a')
	resolve()
})
r.then(()=>console.info('c')
console.info('b')

我们执行这段代码后,注意输出的顺序是a、b、c。 在进入('b')之前,毫无疑问已经获取到了,但是始终是异步操作,所以c不能出现在b之前。

接下来我们尝试混合它。

在此代码中,我设置了两个互不干扰的异步操作:执行 .log('d') 和执行 .log('c')。

var r = new Promise(function(resolve, reject){
    console.log("a");
    resolve()
});
setTimeout(()=>console.log("d"), 0)
r.then(() => console.log("c"));
console.log("b")

我们发现,无论代码的顺序如何,d都必须出现在c之后,因为生成的是引擎内部的微任务,而不是生成宏任务的浏览器API。

为了理解微任务总是先于宏任务,我们在这里设计了一个实验:执行一个需要一秒钟的实验。

setTimeout(()=>console.log("d"), 0)
var r = new Promise(function(resolve, reject){
    resolve()
});
r.then(() => { 
    var begin = Date.now();
    while(Date.now() - begin < 1000);
    console.log("c1") 
    new Promise(function(resolve, reject){
        resolve()
    }).then(() => console.log("c2"))
});

这里我们强制执行时间为1秒,这样可以保证任务c2在d之后添加到任务队列中。

我们可以看到,即使执行了耗时一秒的c1,enque的c2仍然先于d执行。 这很好地解释了微任务优先级的原理。

通过一系列的实验,我们可以总结出如何分析异步执行的顺序:

首先我们分析一下有多少个宏任务;

在每个宏任务中,分析有多少个微任务;

根据调用次数确定宏任务中微任务的执行顺序;

根据宏任务的启动规则和调用顺序,确定宏任务的执行顺序;

确定整个序列;

让我们看一个稍微复杂一点的例子:

function sleep(duration) {
    return new Promise(function(resolve, reject) {
        console.log("b");
        setTimeout(resolve,duration);
    })
}
console.log("a");
sleep(5000).then(()=>console.log("c"));

这是一种很常见的封装方式,利用封装成一个可以异步使用的函数。

我们首先看一下将整个代码分为两个宏任务。 5 秒或 0 秒并不重要。

第一个宏任务包括同步执行的.log('a')和.log('b')。

最后调用第二个宏任务执行,然后异步执行then中的代码,因此调用了.log('c'),最终输出序列为:abc。

是中的定义,但是实际写代码的时候我们可以发现,它似乎并不比写回调的方式简单,但是从ES6开始,我们有了async和await。 这种语法的改进和配合可以有效地改进代码。 结构。

新功能:异步/等待

async/await是新增加的功能,提供了for、if等代码结构来编写异步方法。 它的运行时基础是。 面对这个比较新的功能,我们先来看看基本的用法。

异步函数必须返回,我们可以将所有返回的函数视为异步函数。

async函数是一种特殊的语法,特点是在关键字前加上async关键字,从而定义了一个async函数,在该函数中我们可以使用await来等待。

function sleep(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}
async function foo(){
    console.log("a")
    await sleep(2000)
    console.log("b")
}

该代码利用了我们之前定义的 sleep 函数。 在异步函数 foo 中,我们调用 sleep。

异步函数的强大之处在于它们可以嵌套。 当我们定义一批原子操作时,我们可以使用 async 函数来组合新的 async 函数。

function sleep(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}
async function foo(name){
    await sleep(2000)
    console.log(name)
}
async function foo2(){
    await foo("a");
    await foo("b");
}

这里foo2使用await调用了异步函数foo两次。 可见,如果我们将sleep等异步操作放入某个框架或库中,用户无需了解概念就可以进行异步编程。

另外,/经常与异步一起提及。 需要说明的是,/并不是异步代码,但是当缺少async/await时,一些框架(比如co)会利用这样的特性来模拟async/await。

它并不是为了实现异步而设计的,因此对于async/await来说,/模拟异步的方法应该被放弃。

标签: async 异步队列

如本站内容信息有侵犯到您的权益请联系我们删除,谢谢!!


Copyright © 2020 All Rights Reserved 京ICP5741267-1号 统计代码