1.简介本文介绍了JavaScript运行机制。这部分比较抽象。先说一道面试题:console.log(1);setTimeout(function(){console.log(3);},0);console.log(2);数码打印的顺序是什么?这道题看似简单,但如果不了解JavaScript的工作原理,就很容易做错。问题的答案是依次输出123。如果您有疑问,下面有详细的解释。2、理解JS的单线程概念JavaScript语言的一大特点就是单线程,也就是说同一时间只能做一件事。那么,为什么JavaScript不能有多线程呢?这样可以提高效率啊。JavaScript的单线程与它的使用有关。作为一种浏览器脚本语言,JavaScript的主要目的是与用户交互和操作DOM。这就决定了它只能是单线程的,否则会带来非常复杂的同步问题。例如,假设JavaScript同时有两个线程,一个线程向某个DOM节点添加内容,另一个线程删除这个节点,那么浏览器应该以哪个线程为基础呢?因此,为了避免复杂性,JavaScript从一开始就是单线程的,这已经成为这门语言的核心特征,以后也不会改变。3、理解任务队列(消息队列)单线程是指所有的任务都需要排队,只有上一个任务完成后才会执行下一个任务。如果前一个任务耗时很长,后一个任务就得一直等下去。JavaScript语言的设计者意识到了这个问题,把所有的任务分为两种,一种是同步任务(synchronous),一种是异步任务(asynchronous)。同步任务是指在主线程上排队等待执行的任务,只有上一个任务执行完才能执行下一个任务;异步任务是指不进入主线程而是进入“任务队列”(taskqueue)的任务,只有当“任务队列”通知主线程有异步任务可以执行时,任务才会进入执行的主线程。异步任务包括宏任务和微任务(后面会介绍)。下面我们通过两个例子来说明同步任务和异步任务的区别:console.log("A");while(true){}console.log("B");最终输出是什么?如果你的答案是A,恭喜你答对了,因为这是一个同步任务,程序是自上而下执行的。当遇到while()的死循环时,下面的语句无法执行。console.log("A");setTimeout(function(){console.log("B");},0);while(true){}最终输出是什么?如果你的答案是A,恭喜你对js的运行机制有了一个大概的了解!题目中的setTimeout()是一个异步任务。在所有同步任务执行完之前,任何异步任务都不会执行,下面会详细说明。4.理解EventLoop异步执行的运行机制如下:所有同步任务都在主线程上执行,形成一个执行上下文栈。除了主线程之外,还有一个“任务队列”。只要异步任务有运行结果,就会在“任务队列”中放入一个事件。一旦“执行栈”中的所有同步任务都执行完毕,系统就会读取“任务队列”,看看里面有什么事件。那些对应的异步任务结束等待状态,进入执行栈,开始执行。主线程不断重复上面的第三步。主线程从“任务队列”中读取事件,这个过程是连续不断的,所以整个运行机制也称为EventLoop(事件循环)。只要主线程是空的,它就会读取“任务队列”,这是JavaScript的运行机制。这个过程会自己重复。下图很好地说明了这一点。5.哪些语句会被放入异步任务队列,什么时候放入一般而言,异步任务队列中会放入以下四种:setTimeout和setlntervalDOM事件PromiseAjax异步请求ES6运行的javascript代码分两个阶段:1.分析---提前所有函数定义,所有变量声明提前,变量赋值不提前SetTimeout例子和Ajax例子详解:例子1for(vari=0;i<5;i++){setTimeout(function(){console.log(i);},1000);}最后的输出是什么?当for循环遇到一次setTimeout()时,并不会立即将setTimeout()放入异步队列,而是等待一秒后再放入任务队列。一旦“执行栈”中的所有同步任务执行完毕(即for循环结束,此时i已经5),系统会读取setTimeout()(有5个)有一直存入“任务队列”,所以答案输出5个5。如上所述,当到达指定时间时,定时器会将相应的回调函数插入到“任务队列”的尾部。这就是“定时器”功能。关于定时器的重要补充:定时器包括两个方法:setTimeout和setInterval。他们的第二个参数是指定他们的回调函数将延迟多少毫秒/周期。第二个参数需要注意以下几点:当第二个参数为default时,默认为0;当指定值小于4毫秒时,增加到4ms(4ms是HTML5标准规定的,对于2010及之前的浏览器是10ms);也就是说,至少需要4毫秒,setTimeout()将其放入任务队列。示例2$.ajax({url:"xxxxx",success:function(result){console.log("a")}})setTimeout(function(){console.log("b")},100)setTimeout(函数(){console.log("c")})console.log("d");Ajax只有在加载完成后才会被放入异步队列。至于这个周期的不确定性,有两种情况:①大于100ms,最终结果为dcba;②不到100ms,最后的结果是dcab。6.Microtask和Macrotask上面我们提到异步任务分为macrotasks和microtasks。可以有多个宏任务队列,只有一个微任务队列。宏任务包括:script(全局任务)、setTimeout、setInterval、setImmediate、I/O、UI渲染。Microtasks包括:newPromise().then(callback),process.nextTick,Object.observe(deprecated),MutationObserver(html5新特性)。微任务呢?由于执行代码入口是全局任务脚本,而全局任务是宏任务,当栈为空执行同步任务时,会优先执行微任务队列中的任务。microtask队列中的任务全部执行完后,会读取macrotask队列中的top任务。在macrotasks的执行过程中,会遇到microtasks,依次加入到microtasks队列中。栈为空后,再次读取微任务队列中的任务,以此类推。一句话概括上面的流程图:当一个宏任务队列中的所有任务都执行完后,会检查是否还有一个微任务队列。如果有则先执行microtask队列中的所有任务,如果没有则查看是否还有其他macrotask队列。下面我们通过两个例子来介绍上面的过程:Promise.resolve().then(()=>{console.log('Promise1')setTimeout(()=>{console.log('setTimeout2')},0)})setTimeout(()=>{console.log('setTimeout1')Promise.resolve().then(()=>{console.log('Promise2')})},0)最后输出就是Promise1,setTimeout1,Promise2,setTimeout2开始执行栈的同步任务并检查是否有microtask队列,在上题中存在(只有一个),然后执行microtask队列中的所有任务到输出Promise1,同时会生成宏任务setTimeout2,然后查看宏任务队列。宏任务setTimeout1会在setTimeout2之前执行宏任务setTimeout1,输出setTimeout1会在执行宏任务setTimeout1时生成一个微任务Promise2,放入微任务队列中,然后去清除微任务队列中的所有任务并输出Promise2。清空微任务队列中的所有任务后,会去宏任务队列中再取一个。这一次,setTimeout2console.log('------------------开始----------------');setTimeout(()=>{console.log('setTimeout');},0)newPromise((resolve,reject)=>{for(vari=0;i<5;i++){console.log(i);}resolve();//修改promise实例对象的状态为成功状态}).then(()=>{console.log('promise实例回调执行成功');})console.log('----------结尾---------');7.题外话如果要输出0~4,上面的例子应该是这样的修改什么?将var改为letfor(leti=0;i<5;i++){setTimeout(function(){console.log(i);},1000);}2.为(vari=0;添加立即执行函数i<5;i++){(function(i){setTimeout(function(){console.log(i);},1000);})(i)}3.你也可以像这样添加一个闭包for(vari=1;i<5;i++){vara=function(){varj=i;setTimeout(function(){console.log(j);},1000)}a();}文章发表于2018年11月15日重新编辑,如果觉得文章对你有帮助,欢迎点赞和关注我的GitHub博客,非常感谢!8.参考资料JavaScript运行机制详解:说说JavaScript单线程的EventLoop关于事件循环机制的那些事
