JavaScript异步操作

416 阅读5分钟

JavaScript是一门单线程语言,所谓单线程,就是指一次只能完成一件任务,如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推. 这种模式的好处是实现起来比较简单,执行环境相对单纯,坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行. 常见的浏览器无响应也就是假死状态,往往就是因为某一段Javascript代码长时间运行比如死循环,导致整个页面卡在这个地方,其他任务无法执行

执行机制

为了解决上述问题,Javascript将任务的执行模式分为两种:同步Synchronous与异步Asynchronous,同步或非同步,表明着是否需要将整个流程按顺序地完成,阻塞或非阻塞,意味着你调用的函数会不会立刻告诉你结果

  • 同步 同步模式就是同步阻塞,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的

    let i = 100;
    while(--i) {console.log(i);}
    console.log("while执行完毕后我才能执行");
    
  • 异步 异步执行就是非阻塞模式执行,每一个任务有一个或多个回调函数callback,前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的. 浏览器对于每个Tab只分配了一个Js线程,主要任务是与用户交互以及操作DOM等,而这也就决定它只能为单线程,否则会带来很复杂的同步问题,例如假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器无法确定以哪个线程的操作为准

    setTimeout(() => {console.log("我后执行")},0);
    // 注意:W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms,此外这与浏览器设定、主线程以及任务队列也有关系,执行时间可能大于4ms
    // 例如老版本的浏览器都将最短间隔设为10毫秒. 另外, 对于那些DOM的变动尤其是涉及页面重新渲染的部分, 通常不会立即执行, 而是每16毫秒执行一次
    console.log("我先执行");
    

异步机制

例子

setTimeout( ()  => console.log("我在很行时间之后才执行"), 0);
let i = 3000000;
while(--i) { }
console.log("循环测试完毕")

JavaScript实现异步是通过一个执行栈与一个任务队列来完成异步操作的, 所有同步任务都是在主线程上执行的, 形成执行栈; 任务队列中存放各种事件回调(也可以称作消息), 当执行栈中的任务处理完成后主线程就开始读取任务队列中的任务并执行,并不断往复循环。 例如上例中的setTimeout完成后的事件回调就存在任务队列中, 这里需要说明的是浏览器定时计数器并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果线程处于阻塞状态就会影响记计时的准确, 计数是由浏览器线程进行计数的. 当计数完毕, 就将事件回调加入任务队列. 同样HTTP请求在浏览器中也存在单独的线程, 也是执行完毕后将事件回调置入任务队列. 通过这个流程, 就能够解释为什么上例中setTimeout的回调一直无法执行,是由于主线程也就是执行栈中的代码没有完成, 不会去读取任务队列中的事件回调来执行, 即使这个事件回调早已在任务队列中

  • Event Loop 主线程从任务队列中读取事件, 这个过程是循环不断的, 所以整个的这种运行机制又称为Event Loop, Event Loop是一个执行模型, 在不同的地方有不同的实现, 浏览器和NodeJS基于不同的技术实现了各自的Event Loop. 浏览器的Event Loop是在HTML5的规范中明确定义, NodeJS的Event Loop是基于libuv实现的 在浏览器中的Event Loop由执行栈Execution Stack、后台线程Background Threads、宏队列Macrotask Queue、微队列Microtask Queue组成
  • 执行栈就是在主线程执行同步任务的数据结构, 函数调用形成了一个由若干帧组成的栈
  • 后台线程就是浏览器实现对于setTimeout, setInterval, XMLHttpRequest等等的执行线程
  • 宏队列, 一些异步任务的回调会依次进入宏队列, 等待后续被调用, 包括setTimeoutsetIntervalsetImmediate(Node)requestAnimationFrameUI renderingI/O等操作
  • 微队列, 另一些异步任务的回调会依次进入微队列, 等待后续调用, 包括Promiseprocess.nextTick(Node)Object.observeMutationObserver等操作

当JavaScript执行时, 都要进行以下流程

  • 首先将执行栈中代码同步执行,将这些代码中异步任务加入后台线程中
  • 执行栈中的同步代码执行完毕后,执行栈清空,并开始扫描微队列
  • 取出微队列队首任务, 放入执行栈中执行, 此时微队列执行出队操作
  • 当执行栈执行完成后,继续出队微队列任务并执行,直到微队列任务全部执行完毕
  • 最后一个微队列任务出队并进入执行栈后微队列中任务为空,当执行栈任务完成后,开始扫面微队列为空,继续扫描宏队列任务,宏队列出队,放入执行栈中执行,执行完毕后继续扫描微队列为空则扫描宏队列,出队执行
  • 重复以上步骤
// Step 1
console.log(1);

// Step 2
setTimeout( () => {
  console.log(2);
  Promise.resolve().then( () => {
    console.log(3);
  })
}, 0);

// Step 3
new Promise( (resolve, reject) => {
  console.log(4);
  resolve();
}).then( () => {
  console.log(5);
})

// Step 4
setTimeout( () => {
  console.log(6);
}, 0);

// Step 5
console.log(7);

以上代码输出: 1, 4, 7, 5, 2, 3, 6