一、单线程
js是单线程语言,浏览器只分配给js一个主线程,用来执行任务(函数),但一次只能执行一个任务。
场景描述:
那么现在有2个进程,process1 process2,由于是多进程的JS,所以他们对同一个dom,同时进行操作
process1 删除了该dom,而process2 编辑了该dom,同时下达2个矛盾的命令,浏览器究竟该如何执行呢?
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
二、队列任务
任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
单线程意味着一次只能执行一个任务,任务形成一个任务队列排队等候执行,但前端的某些任务是非常耗时的,比如网络请求,定时器和事件监听,如果让他们和别的任务一样,都老老实实的排队等待执行的话,执行效率会非常的低,甚至导致页面的假死。所以,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程,这些任务是异步的。
1)所有同步任务都在主线程上执行,形成一个[执行栈](http://www.ruanyifeng.com/blog/2013/11/stack.html)(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
console.log(1) // 是同步任务,放入主线程里
setTimeout(function(){
console.log(2)
},0) // 是异步任务,被放入event table, 0秒之后被推入event queue里
console.log(3) // 同步任务,放到主线程里
// 当 1、 3在控制条被打印后,主线程去event queue(事件队列)里查看是否有可执行的函数,执行setTimeout里的函数
任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
macro-task:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
- 从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。
- 当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
setTimeout(function() {
console.log('timeout1');
})
new Promise(function(resolve) {
console.log('promise1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function() {
console.log('then1');
})
console.log('global1');
- script任务执行时首先遇到了setTimeout,setTimeout为一个宏任务源,那么他的作用就是将任务分发到它对应的队列中。
- script执行时遇到Promise实例。Promise构造函数中的第一个参数,是在new的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then则会被分发到micro-task的Promise队列中去。
因此,构造函数执行时,里面的参数进入函数调用栈执行。for循环不会进入任何队列,因此代码会依次执行,所以这里的promise1和promise2会依次输出。
- script任务继续往下执行,最后只有一句输出了globa1,然后,全局任务就执行完毕了。
- 第一个宏任务script执行完毕之后,就开始执行所有的可执行的微任务。这个时候,微任务中,只有Promise队列中的一个任务then1,因此直接执行就行了,执行结果输出then1,当然,他的执行,也是进入函数调用栈中执行的。
- 当所有的micro-tast执行完毕之后,表示第一轮的循环就结束了。这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务macro-task开始。
- 这个时候,我们发现宏任务中,只有在setTimeout队列中还要一个timeout1的任务等待执行。因此就直接执行即可。