任务

84 阅读4分钟

一、概要

  • JavaScript是一个单线程的脚本语言

执行顺序:主任务(同步任务) -> 微任务 -> 宏任务

在当前的微任务没有执行完成时,是不会执行下一个宏任务的

1.面试题

setTimeout(_ => console.log(4))//宏

new Promise(resolve => {
  resolve()
  console.log(1)//直接执行
}).then(_ => {
  console.log(3)//微
})

console.log(2)//直接执行

//输出:
//1 -> 2 -> 3 -> 4
setTimeout(() => console.log(1));//宏
setImmediate(() => console.log(2));//宏
process.nextTick(() => console.log(3));//微
Promise.resolve().then(() => console.log(4));//微
(() => console.log(5))();//直接执行

//输出
//5 -> 3 -> 4 -> 1-> 2

2.复杂的任务处理

setTimeout(() => {
    console.log('set1')
    new Promise((resolve, reject) => {
        console.log('pr1')
        resolve()
    }).then( () => {
        console.log('then1')
    })
});

setTimeout(() => {
    console.log('set2')
});

new Promise((resolve, reject) => {
    console.log('pr2')
    resolve()
}).then( () => {
    console.log('then2')
})

new Promise((resolve, reject) => {
    console.log('pr3')
    resolve()
    setTimeout(() => {
        console.log('set3')
    });
}).then( () => {
    console.log('then3')
})

setTimeout(() => {
    console.log('set4')
    new Promise((resolve, reject) => {
        console.log('pr4')
        resolve()
    }).then( () => {
        console.log('then4')
    })
});

setTimeout(() => {
    console.log('set5')
});

console.log('1')

// node输出:pr2 -> pr3 -> 1 -> then2 -> then3 -> set1 -> pr1 -> set2 -> set3 -> set4 -> pr4 -> set5 -> then1 -> then4

// Chrome输出:pr2 -> pr3 -> 1 -> then2 -> then3 -> set1 -> pr1 -> then1 -> set2 -> set3 -> set4 -> pr4 -> then4 -> set5

二、libuv

img

1.特点

负责各种回调函数的执行时间,毕竟异步任务最后还是要回到主线程,一个个排队执行

  1. 同步任务总是比异步任务更早执行
  2. 异步任务: 本次循环 | 次轮循环(循环:事件循环(event loop))
  3. Node 规定,process.nextTickPromise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeoutsetIntervalsetImmediate的回调函数,追加在次轮循环。

2.执行顺序

  1. 执行同步任务
  2. 发出异步请求
  3. 规划定时器生效时间
  4. 执行process.nextTick()等等
  5. 执行事件循环

2.1.事件循环

  1. timers
  2. I/O callbacks
  3. idle, prepare
  4. poll
  5. check
  6. close callbacks

img

2.2.事件点解释

2.2.1. timers

定时器阶段,处理setTimeout()setInterval()的回调函数。在这个阶段,主线程会检查当前时间是否满足定时器条件,满足则执行回调函数,否则进入下一阶段。

2.2.2. I/O callbacks

除了以下操作的回调函数,其他的回调函数都在这个阶段执行。

· setTimeout()和setInterval()的回调函数
· setImmediate()的回调函数
· 用于关闭请求的回调函数,比如socket.on('close', ...)
2.2.3. idle, prepare

该阶段只供 libuv 内部调用,可以忽略。

2.2.4. Poll

这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。

这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。

2.2.5. check

该阶段执行setImmediate()的回调函数。

2.2.6. Close callbacks

该阶段执行关闭请求的回调函数,比如socket.on('close', ...)

2.2.7. Demo
const fs = require('fs');

const timeoutScheduled = Date.now();

// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms`);
}, 100);

// 异步任务二:文件读取后,有一个 200ms 的回调函数
fs.readFile('test.js', () => {
  const startCallback = Date.now();
  while (Date.now() - startCallback < 200) {
    // 什么也不做
  }
});

// 执行顺序:
// timer(条件不符合) -> readFile(大约100ms) -> timer(条件不符合) -> readFile回调函数(200ms,执行到一半100ms后) -> timer(条件符合,但是readFile回调函数还未执行结束,等待执行结束) -> readFile回调函数执行结束 -> timer执行 -> 输出200+ms

三、同步任务

JavaScript是单线程,默认主任务执行。比如dom加载、css加载等等。

1.click事件

​ 类似于**dispatchEvent**,同步任务执行

document.body.addEventListener('click', _ => console.log('click'))

document.body.click()
document.body.dispatchEvent(new Event('click'))
console.log('done')

// > click
// > click
// > done

2.MutationObserver监听

连续多次触发,只会在最后一次触发回调

new MutationObserver(_ => {
  console.log('observer')
  // 如果在这输出DOM的data-random属性,必然是最后一次的值
  console.log('num = ' + document.body.getAttribute('data-num'));
}).observe(document.body, {
  attributes: true
})

document.body.setAttribute('data-num', Math.random())
document.body.setAttribute('data-num', Math.random()*10)
document.body.setAttribute('data-num', Math.random()*100)
document.body.setAttribute('data-num', Math.random()*10000)

// 输出:
// ovserver
// random = 3782.6350323922743(说明只执行了最后一次的回调函数)

四、宏任务

1.setTimeout

2.setInterval

3.I/O

4.setImmediate

该特性是非标准的,请尽量不要在生产环境中使用它

方法浏览器Node
setTimeout
setInterval
setImmediate
requestAnimationFrame

requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrameMDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行。

告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

5.Demo

setTimeout(() => console.log('1'));
setImmediate(() => console.log('2'));
console.log('3');
new Promise((re, rj) => {
    console.log('4')
    re()
}).then(()=> {
    console.log('5')
})

//浏览器: 3 -> 4 -> 5 -> 2 -> 1
//node: 3 -> 4 -> 5 -> 1 -> 2

五、微任务

任务浏览器Node
process.nextTick
MutationObserver
Promise.then catch finally

1.Promise.then

2.process.nextTick()

2.1.问题

class Lib extends require('events').EventEmitter {
  constructor () {
    super()

    this.emit('init')
  }
}

const lib = new Lib()

lib.on('init', _ => {
  // 这里将永远不会执行
  console.log('init!')
})

// 由于构造方法(实例化对象)是同步执行,new之后回调就跟着执行了,但是监听还未执行,导致不会执行

2.2.改造

​ 通过nextTick,将任务转为微任务,主任务队列为空后立即执行

class Lib extends require('events').EventEmitter {
  constructor () {
    super()

    process.nextTick(_ => {
  			this.emit('init')
    })
  }
}

const lib = new Lib()

lib.on('init', _ => {
  // 这里将永远不会执行
  console.log('init!')
})

// 输出
// init!

3.MutationObserver

listen for attribute changes on the dom.

4.参考文档

​ [动画]jakearchibald.com/2015/tasks-…

​ [官网]nodejs.org/en/docs/gui…