JS异步原理

JS异步原理

JavaScript的任务执行模式

JavaScript作为单线程语言,有两种任务的执行模式:同步模式和异步模式。同步模式就是代码自上而下依次执行。在异步模式下,异步任务又分为微任务和宏任务两种。而微任务和宏任务执行的事件队列也不同。
其中微任务和宏任务主要是以下几种:

宏任务 微任务
setTimeout requestAnimationFrame(有争议)
setInterval MutationObserver(浏览器环境)
MessageChannel Promise.[ then/catch/finally ]/async和await
I/O,事件队列 process.nextTick(Node环境)
setImmediate(Node环境) queueMicrotask
script(整体代码块)

事件循环的过程

  1. 首先执行当前代码(同步任务),直到遇到第一个宏任务或微任务。
  2. 如果遇到微任务,则将它添加到微任务队列中,继续执行同步任务。
  3. 如果遇到宏任务,则将它添加到宏任务队列中,继续执行同步任务。
  4. 当前任务执行完毕后,JavaScript 引擎会先执行所有微任务队列中的任务,直到微任务队列为空。
  5. 然后执行宏任务队列中的第一个任务,直到宏任务队列为空。
  6. 重复步骤 4 和步骤 5,直到所有任务都被执行完毕。 需要注意的是,微任务比宏任务优先级要高,因此在同一个任务中,如果既有微任务又有宏任务,那么微任务会先执行完毕。而在不同的任务中,宏任务的执行优先级要高于微任务,因此在一个宏任务执行完毕后,它才会执行下一个宏任务和微任务队列中的任务。 举个例子,假设当前代码中有一个 setTimeout 和一个 Promise,它们分别对应一个宏任务和一个微任务。那么执行顺序如下:
    1). 执行当前代码,将 setTimeout 和 Promise 添加到宏任务和微任务队列中。
    2). 当前任务执行完毕,JavaScript 引擎先执行微任务队列中的 Promise 回调函数。
    3). 微任务队列为空后,再执行宏任务队列中的 setTimeout 回调函数。 需要注意的是,在一些特殊情况下,微任务和宏任务的执行顺序可能会发生变化,比如在使用 MutationObserver 监听 DOM 变化时,它会被视为一个微任务,但是它的执行顺序可能会比其他微任务更靠后。因此,需要根据具体情况来理解和处理微任务和宏任务的执行顺序。

总的来说,当我们分析一段代码的输出顺序的时候,可以把这段代码看作成一个大的任务。在这段任务中,先执行同步任务,然后根据异步任务的执行顺序依次把微任务和宏任务放到对应的微任务或者宏任务的队列中,然后再先执行完微任务队列中的任务,再执行宏任务队列中的任务。

例题

下面展示一些 内联代码片

async function async1() {
  console.log('1');
  await async2();
  console.log('2');
}
 
async function async2() {
  console.log('3');
}
 
console.log('4');
 
setTimeout(function() {
    console.log('5');
}, 0);  
 
async1();
 
new Promise(function(resolve) {
    console.log('6');
    resolve();
  }).then(function() {
    console.log('7');
});
 
console.log('8');

// 输出顺序为:4 1 3 6 8 2 7 5

JS异步原理_第1张图片

原因

  1. 1-10行为函数声明,从第12行开始,同步任务最优先执行,所以首先输出4
  2. setTimeout是异步任务中的宏任务,放到宏任务执行队列中。
  3. 接下来执行async1函数,其中async1函数中嵌套了async2函数。由于async1函数中含有await关键字,说明async1函数中需要等到async2函数返回的then函数中执行完毕之后才会执行接下来的console.log(‘2’)。所以此时console.log(‘2’)可以作为异步任务放到微任务队列中。
  4. 由于async2函数中并没有await关键字所以async2函数中会直接执行Promise.resolve()函数,所以从程序运行的结果来看没有await关键字的async2函数和普通函数没有区别。所以async2函数中的console.log(‘3’)和async1中的console.log(‘1’)一样,直接在执行栈中依次输出。所以输出1 3
  5. 接下来是promise构造函数和then函数。promise构造函数里面的任务为同步任务,所以输出 6。then函数中的任务作为微任务放到微任务队列中。
  6. 接下来是同步任务,输出8
  7. 主执行栈已经为空,此时需要执行异步任务。由于微任务先行原则,按照队列先进先出的原则,依次输出2 7
  8. 微任务队列为空之后,执行宏任务队列。输出5

Promise原生方法手动实现

Promise.all

Promise.all函数:

  • 参数:一个数组。其中数组的值如果是非Promise的值,那么将该值可看作是一个成功的Promise且成功返回的值为该值。
  • 返回值:(为一个Promise对象)
    • 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
    • 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 3000)
})

const p2 = Promise.resolve(2)

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(3)
  }, 1000)
})

const p4 = Promise.reject('err4')

const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('err5')
  }, 1000)
})

p4.catch((err) => err)

p5.catch((err) => err)

// p4, p5为失败的promise实例,需要定义catch否则会报错

Promise.myAll = (promises) => {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new Error('传入的参数不是数组!'))
    }

    if (promises.length === 0) {
      return resolve([])
    }

    const res = []
    let counter = 0

    promises.forEach((p, i) => {
      Promise.resolve(p)
        .then((value) => {
          counter++
          res[i] = value
          if (counter === promises.length) {
            resolve(res)
          }
        })
        .catch((err) => {
          return reject(err)
        })
    })
  })
}

Promise.myAll([p1, p2, p5])
  .then((res) => {
    console.log('succcess:', res)
  })
  .catch((err) => {
    console.log('error:', err)
  })
// error: err5

由于p4和p5属于失败的promise实例,如果不给它添加异常处理的话,那么后面使用Promise.resolve()将它的状态传递到下一层级的promise实例中的时候会出现没有定义已成处理的错误,所以在这里要给p4,p5加上catch的异常处理。

Promise.race

Promise.race函数:

  • 参数:和Promise.all的参数完全一样。为一个数组,如果数组内有非Promise的值会将该值转化为返回成功结果的Promise实例。
  • 返回值:(为一个Promise对象)
    只要p1,p2,p3之中有一个实例率先改变状态,p的状态就会跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数。
Promise.myRace = (promises) => {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new Error('传入的参数不是数组!'))
    }

    if (promises.length === 0) {
      return resolve([])
    }

    promises.forEach((p) => {
      Promise.resolve(p)
        .then((res) => {
          return resolve(res)
        })
        .catch((err) => {
          return reject(err)
        })
    })
  })
}

你可能感兴趣的:(javascript,开发语言,ecmascript)