js异步相关知识

148 阅读6分钟

如何理解JS异步编程

JavaScript语言执行环境是“单线程”(单线程,就是指一次只能完成一件任务,如果有多个任务就必须排队等候,前面一个任务完成,再执行后面一个任务)。这种“单线程”模式执行效率较低,任务耗时长。 为了解决这个问题,提出了“异步模式”(异步模式,是指后一个任务不等前一个任务执行完就执行,每个任务有一个或多个回调函数)。 异步模式使得JavaScript在处理事务时非常高效,但也带来很多问题,如异常处理困难、嵌套过深。

基础知识

单线程与多线程

进程和线程

  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

提示:

  • 不同进程之间也可以通信,不过代价较大
  • 现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)

JavaScript 的设计就是为了处理浏览器网页的交互(DOM操作的处理、UI动画等),决定了它是一门单线程语言。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程,但是浏览器的渲染进程是提供多个线程的,如下:

  • JS引擎线程
  • 事件触发线程
  • 定时触发器线程
  • 异步http请求线程
  • GUI渲染线程
const t1 = new Date()

setTimeout(() => {

  const t3 = new Date()

  console.log('t3 - t1 =', t3 - t1)

}, 100)

let t2 = new Date()



while (t2 - t1 < 200) {

  t2 = new Date()

}

console.log('end')

调用栈(call stack)

调用栈是解释器(比如浏览器中的 JavaScript 解释器)追踪函数执行流的一种机制。当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。

function multiply(x, y) {

  return x * y;

}

function printSquare(x) {

  var s = multiply(x, x);

  console.log(s);

}

printSquare(5);

JS 执行机制

console.log('script start')



setTimeout(() => {

  console.log('timer 1 over')

}, 1000)



console.log('script end')



// script start

// script end

// timer 1 over

消息队列与事件循环

消息队列

也称为任务队列(task queue),是一个先进先出的队列,它里面存放着各种消息,即异步操作的回调函数,异步操作会将相关回调添加到任务队列中,而不同的异步操作添加到任务队列的时机也不同,如onclick,setTimeout,ajax处理的方式都不同,这些异步操作都是由浏览器内核的不同模块来执行的:

  1. onclick由浏览器内核的DOM Binding模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中;
  2. setTimeout会由浏览器内核的timer模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中;
  3. ajax会由浏览器内核的network模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中;

事件循环(Event Loop):

  • event loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。

    • 浏览器的Event Loop是在html5的规范中明确定义。
  • NodeJS的Event Loop是基于libuv实现的。

  • libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商。

console.log('script start')



setTimeout(function() {

    console.log('timer over')

}, 0)



Promise.resolve(() => {console.log('pr3')}).then(function() {

    console.log('promise1')

}).then(function() {

    console.log('promise2')

})



console.log('script end')



// script start

// script end

// promise1

// promise2

// timer over

宏任务和微任务

  • 宏任务/宏队列,macrotask,也叫tasks。 一些异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:

    • setTimeout
    • setInterval
    • setImmediate (Node独有)
    • requestAnimationFrame (浏览器独有)
    • I/O
    • UI rendering (浏览器独有)
  • 微任务/微队列,microtask,也叫jobs。 另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:

    • process.nextTick (Node独有)
    • Promise
    • Object.observe
    • MutationObserver
    • queueMicroTask
console.log('1')



new Promise((resolve, reject) => {

  console.log('2')

  resolve()

})

  .then(() => {

    console.log('3')

    return new Promise((resolve, reject) => {

      console.log('4')

      resolve()

    }).then(() => {

      console.log('5')

    })

  })

  .then(() => {

    console.log('6')

  })



console.log('7')



// script here

// 1 promise

// end here

// 1 promise then

// 2 promise

// 2 promise then

// another promise

浏览器中的 Event Loop

  1. 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
  2. 全局Script代码执行完毕后,调用栈Stack会清空;
  3. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
  4. 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
  5. microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
  6. 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
  7. 执行完毕后,调用栈Stack为空;
  8. 重复第3-7个步骤;

小结:

  1. 宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
  2. 微任务队列中所有的任务都会被依次取出来执行,直到microtask queue为空;
  3. 图中没有画UI rendering的节点,因为这个是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。
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')

    })

})

实现异步的四种方式

回调、Promise、generator、async

 // 用回调函数实现

const task = (timer, light, callback) => {

  setTimeout(() => {

    if (light === 'red') {

      red()

    } else if (light === 'green') {

      green()

    } else if (light === 'yellow') {

      yellow()

    }



    callback()

  }, timer)

}



const step = () => {

  task(3000, 'red', () => {

    task(2000, 'green', () => {

      task(1000, 'yellow', step)

    })

  })

}



step()



 // 用 promise 实现



const task = (timer, light) => {

  return new Promise((resolve, reject) => {

    setTimeout(() => {

      if (light === 'red') {

        red()

      } else if (light === 'green') {

        green()

      } else if (light === 'yellow') {

        yellow()

      }

      resolve()

    }, timer)

  })

}

const step = () => {

  task(3000, 'red')

    .then(() => {

      task(2000, 'green')

    })

    .then(() => {

      task(1000, 'yellow')

    })

    .then(step)

}

step()



 // 用 async 实现



const step = async () => {

  await task(3000, 'red')

  await task(2000, 'green')

  await task(1000, 'yellow')



  step()

}

step()

实际工作中对异步的改造