settimeout函数用法(js的settimeout方法)

笔者以前面试的时候经常遇到写一堆setTimeout,setImmediate来问哪个先执行。本文主要就是来讲这个问题的,但是不是简单的讲讲哪个先,哪个后。笼统的知道setImmediate比setTimeout(fn, 0)先执行是不够的,因为有些情况下setTimeout(fn, 0)是会比setImmediate先执行的。要彻底搞明白这个问题,我们需要系统的学习JS的异步机制和底层原理。本文就会从异步基本概念出发,一直讲到Event Loop的底层原理,让你彻底搞懂setTimeout,setImmediate,Promise, process.nextTick谁先谁后这一类问题。

同步和异步

同步异步简单理解就是,同步的代码都是按照书写顺序执行的,异步的代码可能跟书写顺序不一样,写在后面的可能先执行。下面来看个例子:

const syncFunc = () => {  const time = new Date().getTime();  while(true) {    if(new Date().getTime() - time > 2000) {      break;    }  }  console.log(2);}console.log(1);syncFunc();console.log(3);

上述代码会先打印出1,然后调用syncFunc,syncFunc里面while循环会运行2秒,然后打印出2,最后打印出3。所以这里代码的执行顺序跟我们的书写顺序是一致,他是同步代码:

settimeout函数用法(js的settimeout方法)

可以看到我们中间调用的asyncFunc里面的2却是最后输出的,这是因为setTimeout是一个异步方法。他的作用是设置一个定时器,等定时器时间到了再执行回调里面的代码。所以异步就相当于做一件事,但是并不是马上做,而是你先给别人打了个招呼,说xxx条件满足的时候就干什么什么。就像你晚上睡觉前在手机上设置了一个第二天早上7天的闹钟,就相当于给了手机一个异步事件,触发条件是时间到达早上7点。使用异步的好处是你只需要设置好异步的触发条件就可以去干别的事情了,所以异步不会阻塞主干上事件的执行。特别是对于JS这种只有一个线程的语言,如果都像我们第一个例子那样去while(true),那浏览器就只有一直卡死了,只有等这个循环运行完才会有响应。

JS异步是怎么实现的

我们都知道JS是单线程的,那单线程是怎么实现异步的呢?事实上所谓的”JS是单线程的”只是指JS的主运行线程只有一个,而不是整个运行环境都是单线程。JS的运行环境主要是浏览器,以大家都很熟悉的Chrome的内核为例,他不仅是多线程的,而且是多进程的:

settimeout函数用法(js的settimeout方法)

流程讲解如下:

主线程每次执行时,先看看要执行的是同步任务,还是异步的API同步任务就继续执行,一直执行完遇到异步API就将它交给对应的异步线程,自己继续执行同步任务异步线程执行异步API,执行完后,将异步回调事件放入事件队列上主线程手上的同步任务干完后就来事件队列看看有没有任务主线程发现事件队列有任务,就取出里面的任务执行主线程不断循环上述流程

定时器不准

Event Loop的这个流程里面其实还是隐藏了一些坑的,最典型的问题就是总是先执行同步任务,然后再执行事件队列里面的回调。这个特性就直接影响了定时器的执行,我们想想我们开始那个2秒定时器的执行流程:

主线程执行同步代码遇到setTimeout,将它交给定时器线程定时器线程开始计时,2秒到了通知事件触发线程事件触发线程将定时器回调放入事件队列,异步流程到此结束主线程如果有空,将定时器回调拿出来执行,如果没空这个回调就一直放在队列里。

上述流程我们可以看出,如果主线程长时间被阻塞,定时器回调就没机会执行,即使执行了,那时间也不准了,我们将开头那两个例子结合起来就可以看出这个效果:

const syncFunc = (startTime) => {  const time = new Date().getTime();  while(true) {    if(new Date().getTime() - time > 5000) {      break;    }  }  const offset = new Date().getTime() - startTime;  console.log(`syncFunc run, time offset: ${offset}`);}const asyncFunc = (startTime) => {  setTimeout(() => {    const offset = new Date().getTime() - startTime;    console.log(`asyncFunc run, time offset: ${offset}`);  }, 2000);}const startTime = new Date().getTime();asyncFunc(startTime);syncFunc(startTime);

执行结果如下:

settimeout函数用法(js的settimeout方法)

上图需要注意以下几点:

一个Event Loop可以有一个或多个事件队列,但是只有一个微任务队列。微任务队列全部执行完会重新渲染一次每个宏任务执行完都会重新渲染一次requestAnimationFrame处于渲染阶段,不在微任务队列,也不在宏任务队列

所以想要知道一个异步API在哪个阶段执行,我们得知道他是宏任务还是微任务。

常见宏任务有:

script (可以理解为外层同步代码)setTimeout/setIntervalsetImmediate(Node.js)I/OUI事件postMessage

常见微任务有:

Promiseprocess.nextTick(Node.js)Object.observeMutaionObserver

上面这些事件类型中要注意Promise,他是微任务,也就是说他会在定时器前面运行,我们来看个例子:

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

上述代码的输出是1,3,5,4,2。因为:

先输出1,这个没什么说的,同步代码最先执行console.log(‘2’);在setTimeout里面,setTimeout是宏任务,“2”进入宏任务队列console.log(‘5’);在Promise.then里面,进入微任务队列console.log(‘3’);在Promise构造函数的参数里面,这其实是同步代码,直接输出console.log(‘4’);在then里面,他会进入微任务队列,检查事件队列时先执行微任务同步代码运行结果是“1,3”然后检查微任务队列,输出“5,4”最后执行宏任务队列,输出“2”

Node.js的Event Loop

Node.js是运行在服务端的js,虽然他也用到了V8引擎,但是他的服务目的和环境不同,导致了他API与原生JS有些区别,他的Event Loop还要处理一些I/O,比如新的网络连接等,所以与浏览器Event Loop也是不一样的。Node的Event Loop是分阶段的,如下图所示:

settimeout函数用法(js的settimeout方法)

上面的这个流程说简单点就是在一个异步流程里,setImmediate会比定时器先执行,我们写点代码来试试:

console.log('outer');setTimeout(() => {  setTimeout(() => {    console.log('setTimeout');  }, 0);  setImmediate(() => {    console.log('setImmediate');  });}, 0);

上述代码运行如下:

settimeout函数用法(js的settimeout方法)

好像是setTimeout先输出来,我们多运行几次看看:

settimeout函数用法(js的settimeout方法)

我们还是来理一下流程:

我们代码基本都在readFile回调里面,他自己执行时,已经在poll阶段遇到setTimeout(fn, 0),其实是setTimeout(fn, 1),塞入后面的timers阶段遇到setImmediate,塞入后面的check阶段遇到nextTick,立马执行,输出’nextTick 1’到了check阶段,输出’setImmediate’,又遇到个nextTick,立马输出’nextTick 2’到了下个timers阶段,输出’setTimeout’

这种机制其实类似于我们前面讲的微任务,但是并不完全一样,比如同时有nextTick和Promise的时候,肯定是nextTick先执行,原因是nextTick的队列比Promise队列优先级更高。来看个例子:

const promise = Promise.resolve()setImmediate(() => {  console.log('setImmediate');});promise.then(()=>{    console.log('promise')})process.nextTick(()=>{    console.log('nextTick')})

代码运行结果如下:

settimeout函数用法(js的settimeout方法)

总结

本文从异步基本概念出发一直讲到了浏览器和Node.js的Event Loop,现在我们再来总结一下:

  1. JS所谓的“单线程”只是指主线程只有一个,并不是整个运行环境都是单线程
  2. JS的异步靠底层的多线程实现
  3. 不同的异步API对应不同的实现线程
  4. 异步线程与主线程通讯靠的是Event Loop
  5. 异步线程完成任务后将其放入任务队列
  6. 主线程不断轮询任务队列,拿出任务执行
  7. 任务队列有宏任务队列和微任务队列的区别
  8. 微任务队列的优先级更高,所有微任务处理完后才会处理宏任务
  9. Promise是微任务
  10. Node.js的Event Loop跟浏览器的Event Loop不一样,他是分阶段的
  11. setImmediate和setTimeout(fn, 0)哪个回调先执行,需要看他们本身在哪个阶段注册的,如果在定时器回调或者I/O回调里面,setImmediate肯定先执行。如果在最外层或者setImmediate回调里面,哪个先执行取决于当时机器状况。
  12. process.nextTick不在Event Loop的任何阶段,他是一个特殊API,他会立即执行,然后才会继续执行Event Loop
(0)
小多多的头像小多多创始人

相关推荐

发表回复

登录后才能评论