JavaScript的执行顺序是单线程执行,上一个任务执行完毕后执行下一个,由事件循环机制处理程序中多个代码块的执行。
需要注意的是,“异步”和“并行”他们的意义完全不同。“异步”指的是关于现在和将来的时间间隙,而“并行”是能够同时发生。
并行计算最常见的工具是“进程”和“线程”。进程和线程独立运行,并可能同时运行。甚至在一些计算机上,多个线程能桐乡耽搁进程的内存。
与之相对的是,事件循环把自身的工作分为一个个任务并顺序执行,不运行对共享内存并行访问和修改。通过分立想成中彼此合作的事件循环,并行和顺序执行可以共存。
由于JavaScript的单线程特性,代码块的执行具有原子性。也就是说,一旦代码块被执行,那么必须得等他执行完毕才可以执行下一个代码块,这成为完整运行的特性。
在ES6中,有一个新的概念建立在事件循环队列之上,叫做任务队列。
任务队列
单线程意味着任务需要排队,一个任务执行完毕后才可以执行下一个任务。但这样遇到需要耗时间的任务(比如http请求或者setTimeout),这时候等着就很不合适了。可以将需要等待的任务挂起来等待,这边先继续往下执行任务。等执行完毕了再回头去处理挂起来的任务。
ps:这里说下Web Worker,Web Worker允许创建多个子线程来执行JavaScript脚本,但是由于子线程受主线程控制,并且无法操作DOM,所以这并没有改变JavaScript单线程的本质。
任务有两种:
- 同步任务(synchronous) 主线程上排队执行的任务,一个个顺序执行。
- 异步任务(asynchronous) 不进入主线程,而进入任务队列(task queue)的任务。
只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体执行逻辑:
- 所有同步任务都在主线程上执行,形成一个执行栈。
- 主线程之外,存在一个任务队列,异步任务都会挂起,放在
Event Table
并注册回调函数。当等待的事情完成时,Event Table会将这个任务的回调函数移入到任务队列中,等待执行。 - 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,对应的异步任务结束等待状态,就进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
逻辑如图所示:
异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
定时器和任务队列的关系
定时器功能主要由setTimeout
和setInterval
这两个函数完成。他们内部的运行机制完全一样,区别在于setTimeout
是一次性执行,而setInterval
是反复的执行。
setTimeout
setTimeout
的第一个参数是回调函数,第二个是推迟执行的毫秒数,之后的参数是附加参数,当定时器到期的时候传递给回调函数的参数。
如果setTimeout
的第二个参数设置为0(或者不设置),就表示当前代码执行完毕(执行栈清空)后,立即执行回调函数。
HTML5标准规定setTimeout
的第二个参数毫秒值不可低于4毫秒,如果低于这个值,就会自动增加。老版本的浏览器都将最短间隔设置为10毫秒。对于DOM的变动(涉及到页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次,所以使用requestAnimationFrame()
的效果比使用setTimeout()
效果好。
如果setTimeout
到时了,回调函数也进入到了主线程,但前面排队了20多个任务,那么也需要等待其他任务执行完毕才可以。所以setTimeout
定时器精度可能不高,只能确保在定时的时间之后运行。
异步任务的优先级
异步任务之间存在一个执行优先级,按照优先级执行。任务分为“宏任务(macro task)”和“微任务(micro task)”。
在挂起任务时,js引擎会将所有任务按照类别分到这两个队列中,首先在宏任务队列中(task queue)取出第一个任务,执行完毕后取出微任务队列中的所有任务顺序执行,之后再取宏任务,周而复始,直至两个队列的任务都取完。
ps:注意,是从宏任务中取一个,从微任务中取所有。。。😂
微任务
微任务包括:promise
、Object.observe
、MutationObserver
宏任务
宏任务包括:script
、setTimeout
、setInterval
、setImmediate
、I/O
、UI rendering
。
任务的优先级:process.nextTick > promise.then > setTimeout > setImmediate
例子:
1 | setImmediate(function () { |
总而言之,浏览器中事件循环的执行为:当前执行栈执行完成时,立即优先处理微任务,再去处理宏任务,同一次事件循环中,微任务先于宏任务执行。