Open Xpig4432xyx opened 6 years ago
setTimeout(function(){
console.log('setTimeout')
},0)
for(var i = 0;i<10;i++){
console.log(i)
}
这个demo的结果是
0 1 2 3 4 5 6 7 8 9
setTimeout
这里是不对的
赞!有个意见,如果第一个例子,把 Promise 和 setTimeout 换个顺序,就能更好的引出:
为什么setTimeout要在Promise.then之后执行呢?
这个问题了,否则不清楚的同学容易认为
setTimeout、Promise 都是异步任务
此处的结果只是由于按顺序执行
script end
在哪里?
setTimeout(function(){ console.log('setTimeout') },0) for(var i = 0;i<10;i++){ console.log(i) } 这个demo的结果是 0 1 2 3 4 5 6 7 8 9 setTimeout
这里是不对的
那对的结果是什么?
@1044375652 没有问题吧
@1044375652 感觉没什么问题,跑了一下,也是这个结果。chrome75
@monw3c 给个你觉得正确的结果吧,不要误导别人啊
@monw3c 亲测没有问题 结果是OK的
前言
其实一开始对栈、堆的概念特别模糊,只知道好像跟内存有关,又好像事件循环也沾一点边。面试薄荷的时候,面试官正好也问到了这个问题,当时只能大方的承认不会。痛定思痛,回去好好的研究一番。 我们将从
JS的内存机制
以及事件机制
和大量的🌰(例子)
来了解栈、堆究竟是个什么玩意。概念比较多,不用死读,所有的🌰心里想一遍,浏览器console看一遍就很清楚了。 let's goJS内存机制
因为JavaScript具有自动垃圾回收机制,所以对于前端开发来说,内存空间并不是一个经常被提及的概念,很容易被大家忽视。特别是很多不专业的朋友在进入到前端之后,会对内存空间的认知比较模糊。
在JS中,每一个数据都需要一个内存空间。内存空间又被分为两种,栈内存(stack)与堆内存(heap)。
栈内存一般储存基础数据类型
最简单的🌰
我们定义一个变量a,系统自动分配存储空间。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问。
数据在栈内存中的存储与使用方式类似于数据结构中的堆栈数据结构,遵循后进先出的原则。
堆内存一般储存引用数据类型
堆内存的🌰
与其他语言不同,JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。看一下下面的图,加深理解。
比较

因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从栈中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。
测试
同学们自己在console里打一遍,再结合下面的图例,就很好理解了
内存机制我们了解了,又引出一个新的问题,栈里只能存基础数据类型吗,我们经常用的function存在哪里呢?
浏览器的事件机制
一个经常被搬上面试题的🌰
对象放在heap(堆)里,常见的基础类型和函数放在stack(栈)里,函数执行的时候在栈里执行。栈里函数执行的时候可能会调一些Dom操作,ajax操作和setTimeout定时器,这时候要等stack(栈)里面的所有程序先走(注意:栈里的代码是先进后出),走完后再走WebAPIs,WebAPIs执行后的结果放在callback queue(回调的队列里,注意:队列里的代码先放进去的先执行),也就是当栈里面的程序走完之后,再从任务队列中读取事件,将队列中的事件放到执行栈中依次执行,这个过程是循环不断的。
概念又臭又长,没关系,我们先粗略的扫一眼,接着往下看。
举一个🌰说明栈的执行方式
图解
执行栈里面最先放的是全局作用域(代码执行有一个全局文本的环境),然后再放one, one执行再把two放进来,two执行再把three放进来,一层叠一层。
最先走的肯定是three,因为two要是先销毁了,那three的代码b就拿不到了,所以是先进后出(先进的后出),所以,three最先出,然后是two出,再是one出。
那队列又是怎么一回事呢?
再举一个🌰
再再举一个🌰
再再再看一个🌰
所以,得出结论,永远都是栈里的代码先行执行,再从队列中依次读事件,加入栈中执行
stack(栈)里面都走完之后,就会依次读取任务队列,将队列中的事件放到执行栈中依次执行,这个时候栈中又出现了事件,这个事件又去调用了WebAPIs里的异步方法,那这些异步方法会在再被调用的时候放在队列里,然后这个主线程(也就是stack)执行完后又将从任务队列中依次读取事件,这个过程是循环不断的。
再回到我们的第一个🌰
setTimeout是宏任务,而Promise.then是微任务 这里的new Promise()是同步的,所以是立即执行的。
这就要引入一个新的话题宏任务和微任务(面试也会经常提及到)
宏任务和微任务
参考 Tasks, microtasks, queues and schedules(https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/?utm_source=html5weekly)
概念:微任务和宏任务都是属于队列,而不是放在栈中
一个新的🌰
宏任务(task)
浏览器为了能够使得JS内部宏任务与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->…) 鼠标点击会触发一个事件回调,需要执行一个宏任务,然后解析HTMl。但是,setTimeout不一样,setTimeout的作用是等待给定的时间后为它的回调产生一个新的宏任务。这就是为什么打印‘setTimeout’在‘promise1 , promise2’之后。因为打印‘promise1 , promise2’是第一个宏任务里面的事情,而‘setTimeout’是另一个新的独立的任务里面打印的。
微任务 (Microtasks)
微任务通常来说就是需要在当前 task 执行结束后立即执行的任务 比如对一系列动作做出反馈,或者是需要异步的执行任务而又不需要分配一个新的 task,这样便可以减小一点性能的开销。只要执行栈中没有其他的js代码正在执行且每个宏任务执行完,微任务队列会立即执行。如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行。微任务包括了mutation observe的回调还有接下来的例子promise的回调。
一旦一个pormise有了结果,或者早已有了结果(有了结果是指这个promise到了fulfilled或rejected状态),他就会为它的回调产生一个微任务,这就保证了回调异步的执行即使这个promise早已有了结果。所以对一个已经有了结果的promise调用.then()会立即产生一个微任务。这就是为什么‘promise1’,'promise2’会打印在‘script end’之后,因为所有微任务执行的时候,当前执行栈的代码必须已经执行完毕。‘promise1’,'promise2’会打印在‘setTimeout’之前是因为所有微任务总会在下一个宏任务之前全部执行完毕。
还是🌰
很好的解释了,setTimeout会在微任务(Promise.then、MutationObserver.observe)执行完成之后,加入一个新的宏任务中
多看一些🌰
总结回顾
栈:
堆:
广而告之
本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。
欢迎讨论,点个赞再走吧 。◕‿◕。 ~