JavaScript在浏览器环境中的异步

    科技2022-07-20  188

    JavaScript在浏览器环境中的异步

    单线程与多线程浏览器的多线程浏览器的异步逻辑js引擎与GUI引擎是互斥的setTimeout(0): 手动异步 参考:

    https://blog.csdn.net/qq_26222859/article/details/77622222 https://www.cnblogs.com/aaron—blog/p/10903118.html

    单线程与多线程

    JavaScript 默认情况下是单线程运行的(除了用H5 Web Workers),但运行js脚本的环境可以是多线程的

    众所周知JavaScript是单线程运行的脚本语言,即: 逐行执行语句,上面的语句没有执行完会阻塞下面语句的执行,但在不同的环境下,却可以实现异步的操作(如浏览器环境下,node环境下)。

    浏览器的多线程

    在JavaScript引擎中负责解析和执行JavaScript代码的线程只有一个。但是除了这个主进程以外,还有其他很多辅助线程。那么诸如onclick回调,setTimeout,Ajax这些都是怎么实现的呢?即浏览器搞了几个其他线程去辅助JavaScript线程的运行。

    浏览器有很多线程,例如:

    GUI渲染线程 - 用于更新页面JavaScript引擎线程 - 用于解析JavaScript代码定时器触发线程 - 浏览器定时计数器并不是 js引擎计数浏览器事件线程 - 用于解析BOM渲染等工作http线程 - 主要负责数据请求EventLoop轮询处理线程 - 事件被触发时该线程会把事件添加到待处理队列的队尾等等等

    浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程。

    浏览器的异步逻辑

    下面这张图可以说明浏览器的异步逻辑

    首先,js引擎解析js代码(称为同步过程,即把整个页面的js代码从头到尾执行下来),当处理到与其他线程相关的代码,就会分发给其他线程(比如onclick, setTimeout,setInterval,ajax请求, 对DOM进行写操作等等…此时的分发过程还是同步过程)其他线程处理完之后(其他线程处理请求的过程就是异步过程)会把需要js引擎执行的任务放入callback queue里(也叫任务队列、消息队列、事件队列),等js线程的同步任务执行完了再从callback queue里按顺序执行其他线程返回来的任务。其他线程执行完毕后放进callback queue里的事件,却需要等待js引擎执行完毕当前stack里的任务,空闲下来,才会被执行。

    总结起来,JS线程就像是领导,而其他线程相当于员工。 领导拿到一个项目,先把整个计划过一遍,把任务下达给其他线程(同步过程),这个出谋划策的过程,JS线程(领导)不会傻等这个员工执行完了再给下一个员工分配任务,而是跳过这个任务,继续分配下一个任务,不然哪个员工没干好岂不是坏了整个团队的进度; 同样地其他线程(员工)拿到任务就开始专注处理自己的事情,接下来领导使唤别人与我无关,毕竟要早点交差么… 员工的事情干完了,发邮件给领导,这个邮件就在领导的 callback queue里躺着,等领导忙完了手上的事情,打开邮箱,嚯,来反馈了,再按照FIFO(first in first out)的顺序一件件处理反馈。

    js引擎与GUI引擎是互斥的

    虽然不同线程之间是互相独立的,但当GUI引擎需要更新界面的时候,却会受到JS线程的阻塞。

    了解过 H5 Web Workers的朋友应该知道,在 H5 Web Workers出现之后,JS也可以“开挂” 实现多线程运行,当然这不在本文的讨论范畴内。这里只提一点, H5 Web Workers的一大用处就是让js引擎中的耗时大的计算不影响界面的响应。

    例如,如果js引擎需要处理一段耗时很长的代码:递归计算斐波那契数列的第2000项…呵呵, 由于线程阻塞,此时页面是无响应的,比如:

    在<input>标签里键入无响应,js实现的轮播图不再播放等

    所以此时可以把递归计算斐波那契数列的函数交给Workers分线程,当然这不是本篇的主题。据这个例子是想说明:

    js引擎与GUI引擎是互斥的

    也就是说GUI引擎在渲染时会阻塞js引擎计算,反过来也一样。 原因很简单,不同线程的具体语句之间执行顺序是不一定的,如果在GUI渲染的时候,js改变了dom,那页面到底听谁的?这就会造成渲染不同步。

    这里插句题外话,无论 H5 Web Workers如何神通广大,都无法改变一个事实: 要实现对DOM的操作,还是只能在js主线程上进行,而不能分到workers分线程上。原因也是:不同线程的具体语句之间执行顺序是不一定的,要避免不同线程对DOM修改造成冲突。

    接着上面领导和员工的比方,GUI页面就好比呈现给甲方的最终结果,DOM渲染引擎按照JS引擎(领导)的要求完成了渲染方案,最终要JS引擎拍板,反馈到GUI页面上。

    借用一下别人的例子:

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <table border=1> <tr><td><button id='do'>Do long calc - bad status!</button></td> <td><div id='status'>Not Calculating yet.</div></td> </tr> <tr><td><button id='do_ok'>Do long calc - good status!</button></td> <td><div id='status_ok'>Not Calculating yet.</div></td> </tr> </table> <script> function long_running(status_div) { var result = 0; for (var i = 0; i < 1000; i++) { for (var j = 0; j < 700; j++) { for (var k = 0; k < 300; k++) { result = result + i + j + k; } } } document.querySelector(status_div).innerHTML = 'calclation done' ; } document.querySelector('#do').onclick = function () { document.querySelector('#status').innerHTML = 'calculating....'; long_running('#status'); }; document.querySelector('#do_ok').onclick = function () { document.querySelector('#status_ok').innerHTML = 'calculating....'; window.setTimeout(function (){ long_running('#status_ok') }, 0); }; </script> </body> </html>

    我们希望能看到计算的每一个过程,我们在程序开始,计算,结束时,都执行了一个dom操作,插入了代表当前状态的字符串,Not Calculating yet.和calculating…和calclation done.计算中是一个耗时的3重for循环. 在没有使用settimeout的时候,执行结果是由Not Calculating yet 直接跳到了calclation done.这显然不是我们希望的.而造成这样结果的原因正是js的事件循环单线程机制.dom操作是异步的,for循环计算是同步的.异步操作都会被延迟到同步计算之后执行.也就是代码的执行顺序变了.calculating…和calclation done的dom操作都被放到事件队列后面而且紧跟在一起,造成了丢帧.无法实时的反应.这个例子也告诉了我们,在需要实时反馈的操作,如渲染等,和其他相关同步的代码,要么一起同步,要么一起异步才能保证代码的执行顺序.在js中,就只能让同步代码也异步.即给for计算加上settimeout.

    setTimeout(0): 手动异步

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <script> alert(1); setTimeout("alert(2)", 0); alert(3); </script> </body> </html>

    上面这个代码的输出顺序是1->3->2,原因就是1和3是JS引擎中的同步过程,优先执行,而2是异步过程,要等JS自己的同步过程执行完了,再被执行。

    Processed: 0.010, SQL: 8