XFE 技术 生活 文集

javascript单线程异步与执行机制

🔖 javascript 👀 10 🕒 2018-03-04

js的单线程模型与游览器的进程/线程息息相关,在了解js单线程与异步的时候,建议先看看这篇文章

单线程/异步

js为什么是单线程

  1. js能够操作dom,如果js是多线程,在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个DOM,一个负责修改一个负责删除,这时就会出现问题。
  2. 虽然可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,Javascript在最初就选择了单线程执行。

js为什么需要异步

  • 由于GUI线程和JS引擎线程互斥,所以当js执行的时候,页面渲染就会被挂起,如果js执行时间过长,或者因为I/O操作(同步ajax请求)的等待,就会让页面'卡死',造成渲染阻塞

js如何实现异步

  • 通过事件驱动机制,来实现异步任务等待,当js主线程空闲后,自动去拿异步任务进行执行

js的事件驱动机制

  1. 事件驱动机制(event driven)通过事件循环(event loop)和事件队列(event queue)来实现。
  2. 事件队列,也称消息队列,是一个先进先出的队列,它里面存放着各种事件消息。事件循环是指主线程重复从消息队列中取消息、执行的过程。
  3. js将程序分为同步任务和异步任务,同步任务都在主线程上执行,形成一个执行栈。
  4. 主线程之外,事件触发线程管理着消息队列,只要异步任务有了运行结果,就在消息队列之中放置一个事件消息(通常都关联着回调函数)。
  5. 一旦执行栈中的所有同步任务执行完毕(此时js引擎空闲),系统就会去读取事件队列,将可运行的异步任务添加到执行栈中,开始执行。
  6. js引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件循环。
  7. 贴两张图表示

test.png

bg2014100802.png

js的异步编程模型

传统异步回调的问题

  • 代码可读性
  • 流程控制
  • 异常和错误处理

异步编程的变革

  • Promise
  • Generator
  • Async/await

执行机制

setTimeout/setInterval

  1. 定时器并不是由js引擎计数的,因为js引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确,因此是通过单独线程来计时并触发的。
  2. 当使用setTimeout/setInterval时,定时器线程会开始计时,计时完成后就会将特定的事件推入事件队列中。
  3. 虽然很多定时器设置时间为0毫秒后就推入事件队列,但实际上W3C在HTML标准中规定,低于4ms的时间间隔算为4ms。
  4. setTimeout/setInterval每次都精确的隔一段时间推入一个事件 。但是,事件的实际执行时间不一定准确,因为存在代码执行时间。
  5. 如果setInterval代码在再次添加到队列之前还没有完成执行,就会导致定时器代码连续运行好几次,而之间没有间隔,造成累积效应。
  6. 当把浏览器最小化显示等操作时,setInterval并不是不执行程序, 它会把setInterval的回调函数放在队列中,等浏览器窗口再次打开时,一瞬间全部执行。
  7. 当函数执行时,如果发现同一个定时器已经有多个在等待执行的任务,只会执行1次,后面的会被忽略掉,示例代码如下:
var sleep = function(time) {
    var date = new Date(); 
    while(new Date() - date <= time) {}
}
var time = new Date();
var a = setInterval(function(){
    sleep(200);
    console.log(new Date() - time);
    if(new Date() - time > 700) clearInterval(a);
}, 100);

// 303
// 506
// 708

目前一般认为的最佳方案是:用setTimeout模拟setInterval,或者特殊场合直接用requestAnimationFrame

macrotask与microtask

起手示例

console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
    console.log('promise1');
}).then(function() {
    console.log('promise2');
});
console.log('script end');

// script start
// script end
// promise1
// promise2
// setTimeout

为什么呢?因为Promise涉及到一个新的概念:microtask。其实,js的任务任务类型分为两者:macrotask和microtask。

概念描述

macrotask(宏任务)——可以理解为是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中取到,放入执行栈中执行的任务)

  • 每一个task会从头到尾执行完毕,过程中不会执行其它
  • 浏览器为了能够使得js内部task与dom任务能够有序的执行,会在一个task执行结束后,在下一个task执行开始前,对页面进行重新渲染 (task->渲染->task->...)

microtask(微任务)——可以理解是在当前task执行结束后立即执行的任务

  • 也就是在当前task任务后,下一个task之前,在渲染之前
  • 在一个宏任务执行完后,就会将在它执行期间产生的所有微任务都执行完毕

类型归属

  • macrotask——主代码块,setImmediate,setTimeout/setInterval
  • microtask——promise,process.nextTick

运行机制

  1. 执行一个宏任务,主栈中没有就从事件队列中获取。
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中。
  3. 宏任务执行完毕后,立即依次执行当前微任务队列中的所有微任务。
  4. 当微任务执行完毕,开始检查渲染,然后GUI线程接管渲染。
  5. 渲染完毕后,JS线程继续接管,开始下一个宏任务。

js_macrotask_microtask.png

再看看如下代码:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

结果输出为:1,7,6,8,2,4,3,5,9,11,10,12。(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

补充

  1. 宏任务中的任务都是放在一个事件队列中,而这个队列由事件触发线程维护。
  2. 微任务中的任务都是放在到微任务队列中,等待当前宏任务执行完毕后执行,而这个队列由js引擎线程维护。
  3. 在node环境的微任务执行中,process.nextTick的优先级高于promise。
  4. 在node环境的宏任务执行中,setImmediate的优先级高于setTimeout。

注意

  1. 关于promise,官方版本中,是标准的微任务形式。但在polyfill中,一般都是通过setTimeout模拟的,所以是宏任务形式。
  2. 有一些浏览器执行结果不一样,它们可能把microtask当成macrotask来执行了, 有些浏览器可能并不标准。
  3. HTML5新特性MutationObserver属于微任务,优先级小于Promise,一般是Promise不支持时才会这样做。
  4. HTML5新特性MessageChannel属于宏任务,优先级是:setImmediate->MessageChannel->setTimeout。

参考