Skip to content

Latest commit

 

History

History
284 lines (214 loc) · 8.45 KB

单线程与异步.md

File metadata and controls

284 lines (214 loc) · 8.45 KB

单线程与异步


单线程

JS 只有一个主线程,主线程执行完执行栈的任务后,去检查异步的任务队列,如果异步事件触发,则将其加到主线程的执行栈。这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环)。

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

1

定时器

除了放置异步任务的事件,"任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。

定时器功能主要由 setTimeout()setInterval() 这两个函数来完成。

HTML5 标准规定了 setTimeout() 的第二个参数的最小值(最短间隔),不得低于 4 毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为 10 毫秒。

另外,对于那些 DOM 的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每 16 毫秒执行一次。这时使用 requestAnimationFrame()的效果要好于 setTimeout()

Node.js 的 Event Loop

Node.js 提供了另外两个与"任务队列"有关的方法:process.nextTicksetImmediate

process.nextTick()

process.nextTick 方法可以在当前"执行栈"的尾部----下一次 Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B() {
    console.log(2);
  });
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);

// 1
// 2
// TIMEOUT FIRED

setImmediate()

setImmediate 方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次 Event Loop 时执行,这与 setTimeout(fn, 0)很像。

setImmediate(function A() {
  console.log(1);
  setImmediate(function B() {
    console.log(2);
  });
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);

// 1--TIMEOUT FIRED--2  or
// TIMEOUT FIRED--1--2

上述代码封装在 setImmediate() 时,结果固定了。

Node.js 文档中称,setImmediate 指定的回调函数,总是排在 setTimeout 前面。实际上,这种情况只发生在递归调用的时候。

setImmediate(function() {
  setImmediate(function A() {
    console.log(1);
    setImmediate(function B() {
      console.log(2);
    });
  });

  setTimeout(function timeout() {
    console.log('TIMEOUT FIRED');
  }, 0);
});

// 1--TIMEOUT FIRED--2

两者区别

多个 process.nextTick 语句总是在当前"执行栈"一次执行完,多个 setImmediate 可能则需要多次 loop 才能执行完。

另外,由于 process.nextTick 指定的回调函数是在本次"事件循环"触发,而 setImmediate 指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。

在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。

在行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行完,而 setImmediate()在每轮循环中执行链表中的一个回调函数。

process.nextTick(function() {
  console.log('nextTick延迟执行1');
});

process.nextTick(function() {
  console.log('nextTick延迟执行2');
});

setImmediate(function() {
  console.log('setImmediate延迟执行1');
  process.nextTick(function() {
    console.log('强势插入');
  });
});

setImmediate(function() {
  console.log('setImmediate延迟执行2');
});
console.log('正常执行');

// console.log("正常执行");
// nextTick延迟执行1
// nextTick延迟执行2
// setImmediate延迟执行1
// setImmediate延迟执行2
// 强势插入

宏任务与微任务

任务队列又分为 macro-task(宏任务)与 micro-task(微任务),在最新标准中,它们被分别称为 task 与 jobs。

1. macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
1. micro-task大概包括: process.nextTick, Promise,  MutationObserver(html5新特性)

setTimeout/Promise 等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。

setTimeout 是一个宏任务源,写在里面的回调函数会加到宏任务队列中。

Promise 是一个微任务源,写在里面 resolve 以及 reject 回调会被加到微任务队列中。

事件循环可以分为这样的一个过程:分别是 宏任务->执行栈->微任务。

2

setTimeout(function() {
  console.log('timeout1');
});

new Promise(function(resolve) {
  console.log('promise1');
  for (var i = 0; i < 1000; i++) {
    i == 99 && resolve();
  }
  console.log('promise2');
}).then(function() {
  console.log('then1');
});

console.log('global1');

// promise1
// promise2
// global1
// then1
// timeout1

综合例子

console.log('golb1');

setTimeout(function() {
  console.log('timeout1');
  process.nextTick(function() {
    console.log('timeout1_nextTick');
  });
  new Promise(function(resolve) {
    console.log('timeout1_promise');
    resolve();
  }).then(function() {
    console.log('timeout1_then');
  });
});

setImmediate(function() {
  console.log('immediate1');
  process.nextTick(function() {
    console.log('immediate1_nextTick');
  });
  new Promise(function(resolve) {
    console.log('immediate1_promise');
    resolve();
  }).then(function() {
    console.log('immediate1_then');
  });
});

process.nextTick(function() {
  console.log('glob1_nextTick');
});

new Promise(function(resolve) {
  console.log('glob1_promise');
  resolve();
}).then(function() {
  console.log('glob1_then');
});

setTimeout(function() {
  console.log('timeout2');
  process.nextTick(function() {
    console.log('timeout2_nextTick');
  });
  new Promise(function(resolve) {
    console.log('timeout2_promise');
    resolve();
  }).then(function() {
    console.log('timeout2_then');
  });
});

process.nextTick(function() {
  console.log('glob2_nextTick');
});

new Promise(function(resolve) {
  console.log('glob2_promise');
  resolve();
}).then(function() {
  console.log('glob2_then');
});

setImmediate(function() {
  console.log('immediate2');
  process.nextTick(function() {
    console.log('immediate2_nextTick');
  });
  new Promise(function(resolve) {
    console.log('immediate2_promise');
    resolve();
  }).then(function() {
    console.log('immediate2_then');
  });
});

// nextTick队列 会比 Promise.then队列先执行
// setImmediate 的任务队列会在 setTimeout 队列的后面执行

// log:
// golb1  glob1_promise  glob2_promise
//     glob1_nextTick  glob2_nextTick  glob1_then  glob2_then
// timeout1  timeout1_promise  timeout2  timeout2_promise
//     timeout1_nextTick  timeout2_nextTick  timeout1_then  timeout2_then
// immediate1  immediate1_promise  immediate2  immediate2_promise
//     immediate1_nextTick  immediate2_nextTick  immediate1_then  immediate2_then

3