经过结果倒推过程是咱们经常使用的思考模式,我在上一篇学习promise笔记中,有少许关于promise执行顺序的例子,经过倒推,我成功让本身对于js执行机制的理解一塌糊涂,js事件机制,事件循环是面试常考的点,弄懂它们是贼有必要的。web
回顾下我学习promise的心理历程:面试
let p = Promise.resolve(1); p.then(resp => console.log(resp)); console.log(2); //2 //1
哦,原来如此,同步代码会先执行,先输出1,因此then回调是异步。编程
let p1 = Promise.resolve(1); p1.then(resp => console.log(resp)); let p2 = Promise.resolve(2); p2.then(resp => console.log(resp)); //1 //2
哦!多个异步,先注册的回调先执行,原来如此。promise
setTimeout(() => console.log(2),0); let p1 = Promise.resolve(1); p1.then(resp => console.log(resp)); //1 //2
嗯????不是先注册的异步先执行?为啥这里先输出1,promise学习下来,成功让本身懵逼。浏览器
理解JS执行机制是很重要的,它会让你的代码调试更符合本身的预期,其次对于面试也很是有帮助。服务器
介绍js执行机制的文章挺多了,这里只是作个我的思路的整理,那么开始。网络
1、JavaScript中的同步异步dom
JavaScript是一门单线程非阻塞语言,在同一时间只能专心作一件事,若是前面的事情没作,后面的事情就得耐心的等着,这就是所谓的同步。异步
你会想,为何要同步?函数
JavaScript自己是一门浏览器脚本语言,更多负责用户的交互,dom操做之类;假设JS并不是单线程,我让两个行为同时操做一个dom对象,那岂不是乱套了。想一想咱们排队取餐吃饭,若是不排队,每每容易引起争吵,编程也是现实行为的抽象。
也许你会说,不是有web worker吗,但web worker属于浏览器的解决方法,并不是JavaScript;浏览器虽然能够开多个线程,但每一个线程仍然是单线程,并且也不被容许操做dom,这依旧没改变JS是单线程语言的事实。
let funA = () => { let NUM = 10000; while (NUM) { NUM--; }; console.log(1); }; let funB = () => console.log(2); funA(); //1 funB(); //2
在上述代码中,让10000进行自减若是让咱们脑补这个过程是很费时的,可是对于强大的js引擎来讲并非事,也要了太多时间;
但是恰恰存在xhr一类网络请求,发起请求网络可能存在延迟,服务器查数据也不知道要多久,反馈结果可能受多个不肯定因素影响,那可不成啊,我后面的程序不可能就这么一直等着。
因而异步诞生了,对于不肯定的网络请求,定时器之类,你不是耗时吗,那咱先备注不急着处理,就接着去忙同步的事情了,等手头上同步忙完了,再来处理先前备注的异步事件。
想一想咱们排队取餐吃饭,前面的哥们大声说道,牛肉面不要面只要牛肉,多葱多蒜少辣不吃香菜半小时后来取,老板也不会等他半小时把面取了再作后面顾客的生意,那真要这样,店子早倒闭了。
那么说完同步异步,咱们大概有了个抽象的概念,js会先执行同步,万一遇到异步,就先备注下有这个异步,等同步跑完了,咱再来处理异步的后续操做,那么站在js角度这个过程是什么样的,咱们接着说。
2、执行栈与任务队列
咱们都知道,当一个方法被调用时,JavaScript会生成一个属于此方法的执行环境,也叫执行上下文,这个上下文中存放着方法依赖的参数,变量以及做用域等等,怎么理解这个执行上下文呢,举个例子:
情景一:妈妈去水果店买了不少苹果。
我最爱吃这种水果了
情景二:妈妈去水果店买了不少橘子。
我最爱吃这种水果了
那么这种水果是?
一样一句话放在不一样情境下表达的意思不一样,同一个方法放在不一样执行环境下执行,结果也可能不一样,差很少这么个意思。
什么是执行栈呢?当调用一个方法A时,这个方法可能也会调用另外一个方法B,B还可能调用方法C,而JS只能同时一件事,因此方法B、C没执行完以前,方法A也不能被释放,那总得找个地方把这些方法按顺序存一存吧,存放的地方就是执行栈。
执行栈是存放同步方法调用的地方,听从先进后出的规则:
let A = () => { B() console.log(1); }; let B = () => { C() console.log(2); }; let C = () => { console.log(3); }; A();//3 2 1
上述代码站在执行机制角度来看,是这样的,你应该也能理解递归处理很差陷入死循环后爆栈是个什么状况了:
凭直觉来想,异步任务不可能直接在执行栈中执行,否则绝对存在堵塞的问题,那先存放在哪呢?放在了任务队列中。
那么到这里咱们又有了一个模糊的概念,同步任务与异步任务存放的地方不一样,有个问题,JavaScript怎么知道何时去执行异步任务呢?那就不得不说事件循环。
3、事件循环 (Event Loop)
当一个任务被执行,js会判断是否为同步任务,若是是同步,压入主线程当即执行;但若是是异步任务,请移步异步处理模块(Task Table),当异步任务有告终果,就将异步任务的回调函数注入到任务队列中等待。
当主线程的同步任务执行完毕执行栈为空,js引擎就会读取任务队列中的第一个任务加入到执行栈执行,当此任务完成,继续重复此类操做,这也就是事件循环了。
那么到这里,咱们知道js引擎会利用事情循环机制来处理同步异步问题;那么问题又来了,还记得文章开头第三个例子吗,定时器和promise都是异步,为何后面的promise反而比前面的定时器先执行,难道异步任务也有本身的前后顺序?这里就得引出宏任务与微任务了。
4、宏任务与微任务
咱们先对宏任务微任务作个大概分类:
macro-task(宏任务):setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)
micro-task(微任务):Promise,process.nextTick,MutaionObserver
不少面孔没见过,不要紧,好歹咱们知道了定时器是宏任务,new Promise是微任务。我把上面的例子搬下来:
setTimeout(() => console.log('我第一'), 1000); let p1 = Promise.resolve('我第二'); p1.then(resp => console.log(resp)); //我第二 //我第一
明明是定时器先进的异步处理模块,结果promise.then还要早于定时器先执行,为何呢?
这是由于,异步任务又分为宏任务与微任务两种,当执行栈为空,JS引擎会优先处理微任务队列的任务,等到微任务队列处理完成,才会处理宏任务队列的任务。
setTimeout(() => console.log('我第一'), 2000); let p1 = Promise.resolve('我第二'); p1.then(resp => console.log(resp)); setTimeout(() => console.log('我第三'), 1000); let p2 = Promise.resolve('我第四'); p2.then(resp => console.log(resp)); //我第二 //我第四 //我第三 //我第一
上述代码中,无论你异步是怎么个执行顺序,最终在执行栈中,老是先处理微任务,最后处理宏任务。
那么我在这里说,对于任务队列,是先进先出的顺序,你确定要喷我了,睁眼说瞎话,要是先进先出,怎么等待2000ms的定时器比等待1000ms的定时器晚执行?那这里就得聊聊定时器时间的具体意义了。
5、有趣的定时器
按期器分为一次性定时器setTimeout与周期性定时器setInterval,前者是等待N秒以后执行回调一次没了,后者是每隔N秒执行回调一次。
有这么一个定时器:
setTimeout(() => console.log('我第一'), 3000);
站在宏观思想上理解,这行代码的意思是这个定时器将在三秒后触发,但站在微观的角度上,3000ms并不表明执行时间,而是将回调函数加入任务队列的时间,这也是为什么存在定时器执行与所设置等待时间不符的问题所在。
setTimeout(() => console.log('我第一'), 3000);
setTimeout(() => console.log('我第二'), 3000);
你猜这两个定时器怎么执行?先等三秒打印“我第一”,再等三秒打印“我第二”吗?其实不是,真正执行是是等待三秒后几乎无间隔的同时打印2个结果。
咱们能够脑补下执行顺序,首先遇到第一个定时器,告诉异步处理模块,等待三秒后将回调加入任务队列,而后又调用了第二个定时器,一样是3秒后将回调加入任务队列。
等到执行栈为空,去任务队列拿任务,执行第一个console,这要不了多久,因而几乎无时差的又去任务队列拿第二个任务,这也致使了为何2次输出几乎在同时进行。
两个定时器等待时间相同,但第一个定时器回调仍是先进入任务队列,因此先触发,这也印证了任务队列先进先出的规则。
因此当咱们使用周期定时器setInterval时,也会遇到执行间隔与所设时间不符的状况,好比前面有个贼复杂的操做,致使周期定时器按时间不停给任务队列加入回调,等到前面任务跑完,这时你会发现前面所积累的回调像憋久了同样一下所有一块儿执行了。
那么到这里这篇文章大概记录完成了。
欢迎你们关注我,我会不断进步。
参考资料: