js 异步

浏览器(或者说JS引擎)执行JS的机制是基于事件循环。

由于JS是单线程,所以同一时间能且只能执行一个任务,其他任务就得排队,后续任务必须等到前一个任务结束才能开始执行。

为了避免因为某些长时间任务造成的无意义等待,JS引入了异步的概念,用另一个线程来管理异步任务。

同步任务直接在主线程队列中顺序执行,而异步任务会进入另一个任务队列,不会阻塞主线程。等到主线程队列空了(执行完了)的时候,就会去异步队列查询是否有可执行的异步任务了(异步任务通常进入异步队列之后还要等一些条件才能执行,如ajax请求、文件读写),如果某个异步任务可以执行了便加入主线程队列,以此循环。

先来看一下实现异步的一些方式,下面用分类的方式列举一下
  1. 经典的回调函数
  • callback
  1. 监听事件
  • onload
  • new Image
function asynByImg( callback ) {
  var img = new Image();
  img.onload = img.onerror = img.onreadystatechange = function() {
    img = img.onload = img.onerror = img.onreadystatechange = null;
    callback(); 
  }
  img.src = "data:image/png,";
}
asynByImg(function(){
  console.log(1);
});
console.log(2);
  • 事件绑定
  • 发布/订阅模式
  • Message
  1. 延迟类
  • setTimeout
setTimeout(function() {
  console.log(1);
});
console.log(2);
  • setInterval
setInterval(function() {
  console.log(1);
});
console.log(2);
  • requestAnimationFrame
(function() {
  var requestAnimationFrame = 
    window.requestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.msRequestAnimationFrame;
  var startTime = window.mozAnimationStartTime || Date.now();
  requestAnimationFrame(draw);
  function draw (timestamp) {
    // 计算两次重绘的时间间隔
    var drawStart = timestamp || Date.now();
    var diff = drawStart - startTime;
    startTime = drawStart;
    requestAnimationFrame(draw);
  }
})()
  • setImmediate
  • process.nextTick()
  1. 异步功能函数
  • promise
  • ajax
  • async / await
  • Worker
  • Co / Generator
这些方式的执行顺序是什么样的?为什么?

先看下面的示例:

console.log('start')

const interval = setInterval(() => {  
  console.log('setInterval')
}, 0)

setTimeout(() => {  
  console.log('setTimeout 1')
  Promise.resolve()
      .then(() => {
        console.log('promise 3')
      })
      .then(() => {
        console.log('promise 4')
      })
      .then(() => {
        setTimeout(() => {
          console.log('setTimeout 2')
          Promise.resolve()
              .then(() => {
                console.log('promise 5')
              })
              .then(() => {
                console.log('promise 6')
              })
              .then(() => {
                clearInterval(interval)
              })
        }, 0)
      })
}, 0)

Promise.resolve()
    .then(() => {  
        console.log('promise 1')
    })
    .then(() => {
        console.log('promise 2')
    })

上面的执行结果依次打印下面的内容

start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
promise 5
promise 6
setInterval
。。。

想要知道上面的原理需要先来了解下面的几个内容

  1. 事件循环
    JavaScript在一个时间仅处理一个任务. 就是JavaScript在执行时, 存在一个执行队列, 依次执行队列中的任务, 不能同时执行多个任务。主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)关于事件循环详细说明可以查看这里。
    这个过程用一个现实的例子就像是去银行只有一个窗口,办理业务就要排队,好痛苦。但是如果一个任务需要执行很久很久,肿么办?等着呗。但是聪明的程序员怎么会忍受一个任务执行那么长时间,于是把任务分成了macrotasks和microtasks两类。
  2. macrotasks和microtasks
    macrotasks【同步任务】: script(整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
    microtasks【异步任务】: process.nextTick, Promises, Object.observe, MutationObserver
    同步任务以及异步任务会进入不同的“场所”,同步的进入主线程,异步的进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程执行完毕为空,就会去Event Queue读取对应的函数,进入主线程执行。

然后解析上面代码的执行:
1. 同步任务直接放入到主模块(主线程)任务队列执行. 异步任务挂起后台执行, 等待IO事件完成或行为事件被触发.
2. 系统后台执行异步任务, 如果某个异步任务事件发生(或者是行为事件被触发), 则将该任务push到任务队列中, 每个任务会对应一个回调函数进行处理. 这个步骤在后台一直执行, 因为就不断有事件被触发, IO不断完成, 任务被不断的加入到任务队列中.
3. 执行任务队列中的任务. 任务的具体执行是在执行栈中完成的. 当运行栈中一个任务的基本运行单元(称之为Frame, 桢)全部执行完毕后, 去读取任务队列中的下一个任务, 继续执行. 是一个循环的过程. 处理一个任务队列中的任务, 称之为一个tick.

即 macrotasks执行console.log('start') 执行后打印了‘start’; 然后macrotasks完成进入microtasks,执行了console.log('promise 1')console.log('promise 2');microtasks执行完成返回查看macrotasks执行了console.log('setInterval')console.log('setTimeout 1'); microtasks执行console.log('promise 3')console.log('promise 4'); 再次进入macrotasksconsole.log('setInterval')console.log('setTimeout 2');最后执行microtasks队列console.log('promise 5')console.log('promise 6'),到这里microtasks全部完成,后面执行macrotasks

备注:事件队列是遵循先进先出, 需要依次处理. 所以, 定时器函数运行时,如果遇到了定时的事件, 事件发生, 也仅仅是将该任务push入任务队列而已(并没有立即执行回调函数)。同时这也是为什么setTimeout预定的时间不一定会执行,例如你预定300ms后执行,它最早是300ms的原因

你可能感兴趣的:(js 异步)