当前位置: 首页 > Web前端 > HTML5

【面试题目】JS异步原理(事件、队列)

时间:2023-04-05 20:17:08 HTML5

JS异步原理(事件、队列)调用栈JS在执行时,会形成一个调用栈。当一个函数被调用时,返回地址、参数和局部变量将被压入堆栈。在中,如果在当前运行的函数中调用了另一个函数,则该函数的相关内容也会被压入栈顶。函数执行后,会被弹出调用栈。变量也会弹出,因为复杂类型值存放在堆中,所以只有指针被弹出,它们的值还在堆中,GC决定回收。尾调用:表示一个函数的最后一步调用另一个函数。从调用栈可以看出,调用栈中有函数a。如果函数a调用函数b,函数b也会被压入栈中。这时候,栈中就会有两个函数。但是如果b函数是a函数的最后一步,并且不需要保留外层函数调用记录,即a函数的调用位置变量不用,那么只会保留b函数在调用栈中,称为“尾调用优化”(Tailcalloptimization),即只保留内部函数的调用记录。如果所有的函数都是尾调用,完全可以每次执行只有一条调用记录,这样会大大节省内存。这就是“尾调用优化”的意思。函数a(){让m=1;让n=2;返回b(m+n);}A();//与函数a()相同{returnb(3);}A();//等于b(3);事件循环(eventloop)和任务队列(taskqueue)JS的异步机制由事件循环和任务队列组成。JS本身是一种单线程语言,所以所谓的异步要靠浏览器或者操作系统来完成。JavaScript主线程有一个执行栈和一个任务队列。主线程会依次执行代码。当遇到函数时,会先将函数压入栈中,然后将函数弹出栈,直到所有代码执行完毕。当遇到异步操作(例如:setTimeout、AJAX)时,异步操作会被浏览器(OS)执行,浏览器会在这些任务完成后将预定义的回调函数推入主线程的任务队列中completed中,当主线程的执行栈清空后,会读取任务队列中的回调函数。当读取任务队列后,主线程会继续执行,从而进入死循环,这就是事件循环。但是,我们只有一个主线程和一个调用堆栈,因此万一在读取所述文件时有另一个请求正在服务,它的回调将需要等待堆栈变空。回调等待轮到它们执行的中间地带称为任务队列(或事件队列或消息队列)。每当主线程完成其上一个任务时,就会在无限循环中调用回调,因此得名“事件循环”。Microtask和Macrotask一个浏览器环境(相关的同源浏览上下文的单位。)只能有一个事件循环(Eventloop),一个事件循环可以有多个任务队列(Taskqueue),每个任务都有一个任务源(任务来源)。例如,客户端可以实现一个包含鼠标键盘事件的任务队列,以及其他任务队列,并赋予鼠标键盘事件的任务队列更高的优先级,比如75%的概率执行。这样既可以保证流畅的交互,也可以进行其他的任务。但是,同一个任务队列中的任务必须按照先进先出的顺序执行。多个任务队列是为了方便控制??优先级。任务队列是先进先出队列。宏任务和微任务是异步任务的两类。挂起任务时,JS引擎会将所有任务按照类别划分到这两个队列中。首先,从macrotask队列(这个队列也叫任务队列)中取出第一个任务,执行完后从microtask队列中取出,队列中的所有任务依次执行;之后,取出macrotask任务,如此循环,直到两个队列中的任务全部取出。整个代码(脚本)是一个宏任务。js先执行一个macrotask,遇到(setTimeout、setInterval、setImmediate等)时创建一个macrotask,分别挂起两个队列。当执行栈为空时,处理macrotask,完成后再处理microtask,直到microtask执行完毕,然后主线程调用栈。注意:每个事件循环(事件循环的一个循环),只处理一个(宏)任务。宏任务完成后,所有微任务将在同一个循环中处理。在处理这些微任务时,可以将更多的微任务入队,一个一个执行,直到处理完整个微任务队列。两大类具体分类如下:macro-task:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI渲染micro-task:process.nextTick、Promises(这里指的是原生Promise实现的thebrowser),Object.observe,MutationObserver参考文章:Promise的队列和setTimeout的队列有什么关系?Node事件循环深入讲解JavaScript事件循环机制(下)