php 执行js-js执行机制实例解读

2023-08-26 0 4,452 百度已收录

要理解JavaScript的运行机制,需要深入理解几个点:JavaScript的单线程机制、任务队列(同步任务和异步任务)、事件和回调函数、定时器、Event Loop(事件循环)。

JavaScript的单线程机制

JavaScript 的一个语言特性(也是该语言的核心)是单线程。 简单来说,单线程就是同一时间只能做一件事。 当有多个任务时,只能按照顺序完成一项任务,然后再执行下一项任务。

JavaScript 的单线程性与其语言的使用有关。 作为一种浏览器脚本语言,JavaScript的主要目的是完成用户交互和操作DOM。 这就决定了它只能是单线程的,否则会造成复杂的同步问题。

想象一下,JavaScript 同时有两个线程,一个线程需要向某个 DOM 节点添加内容,另一个线程的操作是删除这个节点,那么浏览器应该引用谁呢?

所以为了防止复杂性,JavaScript从诞生起就是单线程的。

为了提高CPU利用率,HTML5提出了Web Worker标准,该标准允许JavaScript脚本创建多个线程,但子线程完全由主线程控制,不得操作DOM。 所以这个标准并没有改变JavaScript的单线程本质。

任务队列

一项一项的完成任务,意味着需要完成的任务需要排队,那么为什么需要排队呢?

排队的原因通常有两个:

于是,JavaScript的设计者也意识到,此时可以先运行已经准备好的任务来提高运行效率,即先将等待的任务挂起并放在一边,然后再执行他们在得到他们需要的东西后。 就好像接电话时对方离开一段时间,此时又有一个来电,于是你挂断当前的通话,等通话结束后再接回上一个通话。 因此就出现了同步和异步的概念,任务也分为两种,一种是同步任务(Synchronous),一种是异步任务(Asynchronous)。

具体异步执行如下:

那么怎么知道主线程执行栈是空的呢? js引擎有一个监控进程,会不断检查主线程执行栈是否为空。 一旦为空,就会去Event Queue中检查是否有等待调用的函数。

使用下面的一个

一张图来说明主线程和任务队列。

如果用文字描述地图要表达的内容:

Storm事件和回调函数

“任务队列”就是一个风暴队列(也可以理解为消息队列)。 当IO设备完成一个任务时,它会向“任务队列”添加一个storm,表示相关的异步任务可以进入“执行栈”。 ”。然后主线程读取“任务队列”,看看出了什么问题。

“任务队列”的混乱php 执行js,除了IO设备的混乱外,还包括用户引起的一些混乱(比如键盘点击、页面滚动等)。 只要指定回调函数,当这些风暴发生时,它们就会进入“任务队列”等待主线程读取。

打回来

所谓“回调函数”(callback)就是会被主线程挂起的代码。 异步任务必须指定回调函数。 当主线程开始执行异步任务时,就会执行相应的回调函数。

“任务队列”是先进先出的数据结构,排在后面的storm首先被主线程读取。 主线程的读取过程基本上都是手动的。 一旦执行堆栈被清除,“任务队列”上的第一个风暴将手动进入主线程。 但是,如果包含了“定时器”,主线程必须首先检查执行时间,并且有些事件只有在指定时间之后才能返回到主线程。

事件循环

主线程从“任务队列”中读取storm。 这个过程是循环的,所以整个运行机制也被称为“Event Loop”(事件循环)。

为了更好地理解Event Loop,我们参考Philip Roberts演讲中的一张图片。

上图中,主线程运行时,会生成堆(heap)和栈(stack)。 堆栈中的代码调用各种外部API,并向“任务队列”添加各种干扰(点击、加载、完成)。 。 当栈中的代码执行完毕后,主线程会读取“任务队列”,并依次执行这些扰动对应的回调函数。

执行栈(同步任务)中的代码总是在读取“任务队列”(异步任务)之前执行。

let data = [];
$.ajax({    url:www.javascript.com,    data:data,    success:() => {        console.log('发送成功!');
    }
})console.log('代码执行结束');

登录复制

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

定时器

“任务队列”除了放置异步任务的wave之外,还可以放置定时wave,即指定多长时间后执行个别代码。 这称为计时器函数,是定期执行的代码。

SetTimeout() 和 setInterval() 可用于注册在指定时间后调用一次或重复调用的函数。 它们的内部运行机制是完全相同的。 区别在于后者指定的代码执行一次,而后者会在指定的微秒内以数字的间隔重复调用:

setInterval(updateClock, 60000); //60秒调用一次updateClock()

登录复制

因为它们都是客户端 JavaScript 中重要的全局函数,所以它们被定义为 Window 对象。

但作为一个通用函数,它实际上并不对窗口做任何事情。

Window 对象的 setTImeout() 方法用于实现在指定微秒数后运行的函数。 所以它接受两个参数,第一个是回调函数,第二个是延迟执行的微秒数。 setTimeout() 和 setInterval() 返回一个值,该值可以传递给clearTimeout() 以取消该函数的执行。

console.log(1);
setTimeout(function(){console.log(2);}, 1000);console.log(3);

登录复制

上面代码的执行结果是1,3,2,因为setTimeout()将第二行的执行延迟到了1000毫秒之后。

如果setTimeout()的第二个参数设置为0php 执行js,则表示当前代码执行完后(清空执行栈),立即执行指定的回调函数(间隔0毫秒)。

setTimeout(function(){console.log(1);}, 0);console.log(2)

登录复制

上面代码的执行结果始终是2, 1,因为只有执行完第二行之后,系统才会执行“任务队列”中的回调函数。

简而言之,setTimeout(fn,0)的意义就是指定一个任务在主线程最早可用空闲时间执行,即尽早执行。 它在“任务队列”的尾部添加了一个wave,因此只有在同步任务和“任务队列”的现有wave都被处理之后才会执行它。

HTML5标准规定setTimeout()第二个参数的最小值(最短间隔)不能高于4毫秒。 如果高于这个值,就会手动减少。

需要注意的是,setTimeout()只是将storm插入到“任务队列”中,主线程执行完当前代码(执行栈)后才会执行它指定的回调函数。 如果当前代码耗时较长,则可能会耗时较长,因此无法保证反弹函数一定会在setTimeout()指定的时间执行。

由于历史原因,setTimeout() 和 setInterval() 的第一个参数可以作为字符串传递。 如果是这样,则将在指定的超时或间隔后评估字符串(相当于 eval() )。

Node.js 的事件循环

Node.js也是单线程的Event Loop,但其运行机制与浏览器环境不同。

Node.js的运行机制如下。

除了setTimeout和setInterval这两个方法之外,Node.js还提供了另外两个与“任务队列”相关的方法:process.nextTick和setImmediate。 它们可以帮助我们加深对“任务队列”的理解。

process.nextTick方法可以在下一个Event Loop(主线程读取“任务队列”)之前在当前“执行堆栈”末尾触发回调函数。 也就是说,它指定的任务总是发生在所有异步任务之前。 setImmediate方法是在当前“任务队列”的末尾添加一个storm,即它指定的任务总是在下一个Event Loop中执行,这与setTimeout(fn, 0)非常相似。请参见下面的例子

process.nextTick(function A() {console.log(1);process.nextTick(function B(){console.log(2);});});
setTimeout(function timeout() {console.log('TIMEOUT FIRED');
}, 0)// 1// 2// TIMEOUT FIRED

登录复制

上面的代码中,由于process.nextTick指定的回调函数总是在当前“执行栈”末尾触发,所以函数A在setTimeout指定的回调函数超时之前执行,函数B也在超时之前执行。 这意味着如果有多个process.nextTick语句(无论是否嵌套),它们都会在当前“执行栈”上执行。

现在,再次查看 setImmediate。

setImmediate(function A() {console.log(1);
setImmediate(function B(){console.log(2);});});
setTimeout(function timeout() {console.log('TIMEOUT FIRED');
}, 0);

登录复制

上面代码中setImmediate和setTimeout(fn,0)分别添加了回调函数A和timeout,这两个函数都是在下一个Event Loop中触发的。 那么,哪个回调函数会先执行呢? 答案并不确定。 操作的结果可能是 1–TIMEOUT FIRED–2 或 TIMEOUT FIRED–1–2。

令人费解的是,Node.js 文档指出 setImmediate 指定的回调函数总是落后于 setTimeout。 事实上,这只发生在递归调用中。

setImmediate(function (){setImmediate(function A() {console.log(1);
setImmediate(function B(){console.log(2);});});
setTimeout(function timeout() {console.log('TIMEOUT FIRED');
}, 0);
}); 
// 1 // TIMEOUT FIRED // 2

登录复制

上面的代码中,setImmediate和setTimeout被封装在一个setImmediate中,它的运行结果始终是1-TIMEOUT FIRED-2,并且函数A必须在超时时触发。 至于TIMEOUT FIRED旁边的2(即函数B在超时之前触发),是因为setImmediate总是将storm注册到下一轮Event Loop,所以函数A和timeout在同一轮Loop中执行,而函数B在下一轮循环执行中执行。

由此我们得到process.nextTick和setImmediate之间的一个重要区别:多个process.nextTick语句总是在当前“执行栈”中执行一次,而多个setImmediate可能需要多次循环才能执行。 事实上,这就是 Node.js 10.0 版本添加 setImmediate 方法的原因。 否则,像下面这样的对process.nextTick的递归调用将是无休止的,主线程根本不会读取“事件队列”!

process.nextTick(function foo() {process.nextTick(foo);
});

登录复制

事实上,现在如果你递归地编写 process.nextTick,Node.js 会抛出警告,要求你更改为 setImmediate。

另外,由于process.nextTick指定的回调函数是在本次“事件周期”中触发的,而setImmediate是在上一个“事件周期”中指定的,所以显然前者总是早于前者发生,而执行效率也高(因为不需要检查“任务队列”)。

承诺

除了广义的同步任务和异步任务之外,任务还有更精细的定义:

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

根据宏任务和微任务的分类方法,JS的执行机制是

请看下面的反例:

setTimeout(function(){
     console.log('定时器开始啦')
 });
 new Promise(function(resolve){
     console.log('马上执行for循环啦');     for(var i = 0; i < 10000; i++){
         i == 99 && resolve();
     }
 }).then(function(){
     console.log('执行then函数啦')
 }); console.log('代码执行结束');

登录复制

所以最终的执行顺序是[立即执行for循环-代码执行结束-执行then函数-定时器启动]

我们来分析一段比较复杂的代码,看看你是否真正掌握了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')
    })
})

登录复制

第一轮风暴周期过程分析如下:

宏任务事件队列 微任务事件队列

设置超时1

流程1

设置超时2

那么1

* 上表是第一轮风暴周期宏任务结束时各个Event Queue的情况。 此时1和7已经输出了。

好了,第一轮风暴循环即将结束,本轮结果输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:

宏任务事件队列 微任务事件队列

设置超时2

进程2

那么2

* 第二轮风暴周期宏任务结束,我们发现还有process2和then2两个微任务可以执行。

* 输出 3。

* 输出 5。

* 第二轮风暴周期结束,第二轮产出为2、4、3、5。

* 第三轮风暴周期开始,此时只剩下setTimeout2,执行它。

* 直接输出9。

* 将process.nextTick()分发到微任务事件队列。 将其表示为process3。

* 直接执行new Promise,输出11。

* 分发then到微任务事件队列,记为then3。

宏任务事件队列 微任务事件队列

流程3

然后3

* 第三轮风暴周期宏任务执行结束,执行两个微任务process3和then3。

* 输出 10。

* 输出 12。

* 第三轮风暴周期结束,第三轮产出为9、11、10、12。

整个代码中,一共进行了3个风暴周期,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。

(请注意,节点环境中的窃听依赖于libuv和后端环境不完全一样,输出顺序可能会有偏差)

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 php php 执行js-js执行机制实例解读 https://www.wkzy.net/game/164153.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务