本文介绍了函数调用栈,MacroTask和MicroTask的执行顺序,nextTick在Vue.js中的实现等;本文引用的参考资料统一声明为JavaScript学习和实践资源索引。一、事件循环机制详解及实际应用JavaScript是典型的单线程单并发语言,也就是说在同一个时间片内只能执行单个任务或部分代码片。也就是说,我们可以认为某个同域浏览器中的JavaScript主线程有一个函数调用栈和一个任务队列(参考whatwg规范);主线程会依次执行代码,当遇到函数时,先将函数压入栈中,待函数运行完毕再将函数弹出栈,直到所有代码执行完毕。当函数调用栈为空时,运行时会根据事件循环(EventLoop)机制从任务队列中提取需要执行的回调并执行。每个线程都有自己的事件循环,所以每个WebWorker都有自己的,所以可以独立执行。但是,属于同一来源的所有表单共享一个事件循环,因此它们可以同步通信。事件循环(EventLoop)并不是JavaScript独有的,它广泛应用于各个领域的异步编程;所谓EventLoop就是一系列回调函数的集合,当一个异步函数执行时,会将Callbacks排队,等异步代码执行完毕后JavaScript引擎开始处理其关联的回调。在web开发中,我们经常需要处理网络请求等比较慢的操作。如果所有这些操作都以同步阻塞的方式运行,无疑会大大降低用户界面的体验。另一方面,我们点击某些按钮后的响应事件可能会导致界面重新渲染。如果响应事件的执行阻塞了界面的渲染,也会影响整体性能。在实际开发中,我们会使用异步回调来处理这些操作。调用者和响应之间的这种解耦确保JavaScript在等待异步操作完成之前仍然可以执行其他代码。EventLoop负责执行队列中的回调,并将其压入函数调用栈。其基本代码逻辑如下:while(queue.waitForMessage()){queue.processNextMessage();}完整的浏览器JavaScript事件循环机制图如下:在浏览器中,事件随时可能被触发,只有那些带有回调的事件才会将它们的相关任务推送到任务队列中。当回调函数被调用时,会在函数调用栈中创建一个初始帧,任何在整个函数调用栈被清空之前产生的任务都会被推入任务队列中延迟执行;顺序同步函数调用将创建一个新的栈帧。综上所述,浏览器中的事件循环机制是这样描述的:浏览器内核会在其他线程中进行异步操作,当操作完成后,将操作结果和预定义的回调函数放入浏览器的任务队列中JavaScript主线程。JavaScript主线程会在执行栈清空后读取任务队列,读取任务队列中的函数后,将函数压栈运行,直到执行栈清空,然后再次读取任务队列,继续执行环形。当主线程阻塞时,仍然可以将任务推入任务队列。这就是为什么当页面的JavaScript进程被阻塞时,我们触发的点击等事件会在进程恢复后依次执行。2.函数调用栈和任务队列在变量作用域和提升部分,我们介绍了所谓执行上下文(ExecutionContext)的概念。在JavaScript代码的执行过程中,我们可能有一个全局上下文、多个函数上下文或块上下文;每个函数调用都会创建一个新的上下文和本地范围。而这些执行上下文栈就形成了所谓的执行上下文栈(ExecutionContextStack),就像上面介绍的JavaScript是单线程的事件循环机制,同时只会执行单个事件,而其他事件在所谓的执行堆栈中。排队等候:从JavaScript内存模型来看,我们可以将内存分为调用栈(CallStack)、堆(Heap)和队列(Queue)等几个部分:调用栈会记录所有的函数调用信息,当我们调用一个函数时,它的参数和局部变量会被压入栈中;执行后,栈顶元素将被弹出。堆中存放着大量的非结构化数据,例如程序分配的变量、对象等。该队列包含一系列未决信息和关联的回调函数。每个JavaScript运行时都必须包含一个任务队列。当调用栈为空时,运行时会从队列中取出一条消息,并执行其关联的函数(即创建栈帧的过程);运行时将递归调用函数并创建调用堆栈,直到函数调用堆栈被完全清除,然后从任务队列中移除消息。换句话说,例如,按钮点击或HTTP请求响应将作为消息存储在任务队列中;需要注意的是只有这些事件的回调函数存在时才会被放入任务队列,否则直接忽略。例如,对于以下代码块:functionfire(){constresult=sumSqrt(3,4)console.log(result);}functionsumSqrt(x,y){consts1=square(x)consts2=square(y)constsum=s1+s2;returnMath.sqrt(sum)}functionsquare(x){returnx*x;}fire()及其对应的函数调用图(从这里整理)为:这里还值得一提的是Promise.then是异步执行的,同时创建一个Promise实例(executor)是同步执行的,比如下面的代码:(functiontest(){setTimeout(function(){console.log(4)},0);newPromise(functionexecutor(resolve){console.log(1);for(vari=0;i<10000;i++){i==9999&&resolve();}console.log(2);}).then(function(){console.log(5);});console.log(3);})()//输出结果为://1//2//3//5//4可以参考Promise规范中关于promise.then的部分:promise.then(onFulfilled,onRejected)2.2.4onFulfilledoronRejectedmustnotbecalleduntiltheexecutioncontextstackcontainsononlyplatformcode.[3.1].这里的“platformcode”表示engine,environment,和promise实现代码。在实践中,这个要求确保onFulfilled和onRejected同步执行,在调用事件循环之后,并且有一个新的堆栈。这可以被实现ementedwitheithera“macro-task”mechanismsuchassetTimeoutorsetImmediate,orwitha“micro-task”mechanismsuchasMutationObserverorprocess.nextTick.Sincethepromiseimplementationisconsideredplatformcode,itmayitselfcontainatask-schedulingqueueor“trampoline”inwhichthehandlersarecalled.规范要求,onFulfilled必须在执行上下文栈(ExecutionContextStack)只包含平台代码(platformcode)平台代码指的是引擎、环境、Promise实现代码等才可以执行。实际上,此要求确保onFulfilled在调用then的事件循环之后异步执行(使用新堆栈)。3.MacroTask(Task)和MicroTask(Job)在面试中,我们经常会遇到如下代码题,主要是测试不同JavaScript任务的执行顺序://testcodeconsole.log('main1');//该函数只能在Node.js环境下使用process.nextTick(function(){console.log('process.nextTick1');});setTimeout(function(){console.log('setTimeout');process.nextTick(function(){console.log('process.nextTick2');});},0);newPromise(function(resolve,reject){console.log('promise');resolve();}).then(function(){console.log('promisethen');});console.log('main2');//执行结果main1promisemain2process.nextTick1promisethensetTimeoutprocess.nextTick2我们已经在JavaScript的主线程中引入了上一篇文章当遇到异步调用时,这些异步调用会立即返回一个值,这样主线程就不会阻塞在这里。真正的异步操作会由浏览器执行,主线程清空当前调用栈后,会按照先进先出的顺序读取任务队列中的任务。JavaScript中的任务分为两种类型:MacroTask和MicroTask。在ES2015中,MacroTask指的是Task,而MicroTask指的是Job。典型的MacroTask包括setTimeout、setInterval、setImmediate、requestAnimationFrame、I/O、UI渲染等。MicroTask包括process.nextTick、Promises、Object.observe、MutationObserver等。两者的关系可以用下图来说明:参考whatwg规范中的描述:一个事件循环(EventLoop)会拥有一个或多个任务队列(TaskQueue,也称为TaskSource),其中TaskQueue是MacroTaskQueue,而EventLoop只有一个MicroTaskQueue。每个TaskQueue都保证按照回调入队的先后顺序依次执行,这样浏览器就可以从里面到JS/DOM,保证动作的顺序发生。现有的MicroTask队列会在Task执行之间被清空,MacroTask或MicroTask中产生的MicroTask也会被推入MicroTask队列执行。参考如下代码:functionfoo(){console.log("Startofqueue");bar();setTimeout(function(){console.log("Middleofqueue");},0);Promise.resolve().then(function(){console.log("Promiseresolved");Promise.resolve().then(function(){console.log("Promiseresolvedagain");});});console.log("Endofqueue");}functionbar(){setTimeout(function(){console.log("Startofnextqueue");},0);setTimeout(function(){console.log("Endofnextqueue");},0);}foo();//输出StartofqueueEndofqueuePromiseresolvedPromiseresolvedagainStartofnextqueueEndofnextqueueMiddleofqueue***上面代码中的TaskQueue是foo(),foo()调用bar()建立一个新的TaskQueue,bar()调用后,foo()生成一个MicroTask并被压入唯一的MicroTask队列。我们再总结一下JavaScriptMacroTask和MicroTask的执行顺序。当执行栈(调用栈)为空时,开始依次执行:《这一段在我笔记里也放了好久,无法确定是否拷贝的。。。如果有哪位发现请及时告知。。。(*ฅ́˘ฅ̀*)♡》1)将最早的任务(任务A)放入任务队列2)如果任务A为null(任务队列为空),则跳过直接到第6步3)将当前运行的任务设置为任务A4)执行任务A(即执行回调函数)5)将当前运行的任务设置为null并移除任务A6)执行微任务队列a:选择最早的任务taskXbinmicrotask:如果taskX为null(则microtask队列为空),直接跳转到gc:SetcurrentrunningtasktotaskXd:ExecutetaskXe:setcurrentrunningtasktonullandremovethetaskXf:Selectmicrotask中最早的任务,跳到bg:结束microtask队列1)跳到第4步,简单分析一下vue.js中nextTick在Vue.js中的实现,它会异步执行DOM更新;当观察到数据变化时,Vue会打开一个队列,缓冲在同一个事件循环中发生的所有数据变化。如果同一个观察者被多次触发,它只会被推入队列一次。这种缓冲时的重复数据删除对于避免不必要的计算和DOM操作很重要。然后,在下一个事件循环“tick”中,Vue刷新队列并执行实际(去重)工作。Vue在内部尝试使用原生的Promise.then和MutationObserver来实现异步队列。如果执行环境不支持,它将使用setTimeout(fn,0)代替。《因为本人失误,原来此处内容拷贝了 https://www.zhihu.com/question/55364497 这个回答,造成了侵权,深表歉意,已经删除,后续我会在 github 链接上重写本段》而当我们想在数据更新后进行一些DOM操作时,需要使用nextTick函数来添加回调://HTML
