js执行顺序

563 阅读6分钟

JS执行顺序问题

遇到setTimeout与promise、async的执行顺序问题 之前理解的比较懵懂 着重记录一下

js的事件循环(event loop【同步和异步】)

javascript是一门单线程语言,在HTML5中提出了Web-Worker(链接:Web-Worker介绍与使用),但javascript是单线程这一核心仍未改变。所以一切javascript版的"多线程"都是用单线程模拟出来的。所有的任务需要排队一个一个执行,当一个任务执行完毕后在执行下一个任务。 但是任务在执行的过程,由于IO设备很慢, 必须等待得到结果后才继续执行下一个任务,导致效率极低;此时cpu是处于空闲状态的,完全可以执行其它任务。 那么可以在执行任务的时候完全不顾IO设备,把等待的任务挂起,先执行其它任务,等结果出来后再返回去执行之前的任务,这样就可以提升效率了。 所有的任务可分为两种,一种是同步任务(synchronous),一种是异步任务(asynchronous)

  1. 同步任务 在主线程上排队执行的任务,只有前一个任务执行完毕,才会执行后一个任务
  2. 异步任务 不进入主线程,而是进入任务队列的任务,只有等主线程任务执行完毕,任务队列开始通知主线程,请求执行任务,该任务才会进入主线程

图示(事件循环event loop):

15fdd88994142347.png

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环) 示例:
    let data = [];
    $.ajax({
        url:
        data:data,
        success:() => {
            console.log('发送成功!');
        }
    })
    console.log('代码执行结束');

解析:

  • ajax进入Event Table,注册回调函数success
  • 执行console.log()
  • ajax事件完成,回调函数success进入Event Queue
  • 主线程从Event Queue读取回调函数success并执行
主线程【js一直在做一个工作,就是从任务队列里提取任务,放到主线程里执行】
  • 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

  • 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

  • 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

  • 主线程不断重复上面的第三步

1038932-20190218164103689-121078727.png.png

setTimeout

  1. setTimeout执行时间与设定时间不对等导致原因
    setTimeout(() => {
        task()
    },3000)

    sleep(10000000)

解析:

  • task()进入Event Table并注册,计时开始
  • 执行sleep函数,很慢,非常慢,计时仍在继续
  • 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着
  • sleep终于执行完了,task()终于从Event Queue进入了主线程执行

上述的流程走完,setTimeout这个函数,是经过指定时间后,把要执行的任务(task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。

2.setTimeout(fn,0) 代码是否可以立即执行

setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行【即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒】

js的事件循环(【宏任务 微任务】)

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):包括Promise,process.nextTick等

注:宏观任务优先级:主代码块 > setImmediate > MessageChannel > setTimeout / setInterval 比如:setImmediate指定的回调函数,总是排在setTimeout前面

微任务优先级:process.nextTick > Promise > MutationObserver

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

示例:

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

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

    console.log('console');
  • 这段代码作为宏任务,进入主线程
  • 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue
  • 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue
  • 遇到console.log(),立即执行
  • 整体代码script作为第一个宏任务执行结束,看看有哪些微任务?发现then在微任务Event Queue里面,执行
  • 第一轮事件循环结束,开始第二轮循环,从宏任务Event Queue开始。发现了宏任务Event Queue中- --- setTimeout对应的回调函数,立即执行

15fdcea13361a1ec.png

    Promise.resolve().then(() => {
        console.log("1");  
        setTimeout(() => { 
          console.log("2");
        }, 0);
      });
      setTimeout(() => { 
        console.log("3");
        Promise.resolve().then(() => {
          console.log("4");
        });
      }, 0);
  • (红色):JS 引擎会把微观任务Promise存入执行栈,把宏观任务setTimeout存入 “任务队列”
  • (绿色):主线程率先运行执行栈中的代码,依次输入1,然后把绿框的setTimeout存入 “任务队列”
  • (蓝色):执行栈清空以后,会率先读取 “任务队列” 中最早存入的setTimeout(红框的那个),并把这个定时器存入栈中,开始执行。这个定时器中的代码都是微观任务,所以可以一次性执行,依次输出3 和 4
  • (紫色):重复第3步的操作,读取 “任务队列” 中最后存入的setTimeout(绿框的那个),输出2

20190525215707202.png.png

async 和 setimeout嵌套执行顺序问题

async与await转化

实际上async是promise的语法糖,我们要将其转换为promise,async会返回一个隐式的promise async function MDN的解释

    async function async1(){
      console.log('async1 start')
      await async2()
      console.log('async1 end')
    }
    async function async2(){
      console.log('async2')
    }
    async1();

可以转成

     function async1() {
        console.log("async1 start");
        const p = async2();
        return new Promise((resolve) => {
          resolve();
        }).then(() => {
          p.then(() => {
            console.log("async1 end");
          });
        });
      }
      function async2() {
        console.log("async2");
        return new Promise((resolve) => {
          resolve();
        });
      }
      async1();

前几天遇到的问题

    async function async1() {
        console.log("async1 start");
        await async2();
        await setTimeout(function(){
          console.log('111')
        })
        console.log("async1 end");
      }
      async function async2() {
        console.log("async2");
      }
      async1();
      
     

并不会按顺序输出 输出结果为 // 输出 async1 start async2 async1 end 111

可以转成

    function async1() {
        console.log("async1 start");
        const p = async2();
        return new Promise((resolve) => {
          resolve();
        }).then(() => {
          p.then(() => { 
            new Promise((resolve) =>{
                setTimeout(function(){
                    console.log('111')
                }) 
                resolve();
            }).then(() => { 
                console.log("async1 end");
            })
            
          });
        });
      }
      function async2() {
        console.log("async2");
        return new Promise((resolve) => {
          resolve();
        });
      }
      async1();
  • 这段代码作为宏任务,进入主线程
  • 先遇到console.log("async1 start"); 执行 // async1 start
  • 遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue 得到结果 // async2
  • 整体代码script作为第一个宏任务执行结束,看看有哪些微任务?发现then在微任务Event Queue里面,执行,调用async2函数,遇到promise立即执行,将then函数分发到微任务Event Queue,
  • 遇到Promise,new Promise立即执行,其中包含setTimeout,将其回调函数注册后分发到宏任务Event Queue,接着将then也分发到微任务Event Queue中
  • 宏任务执行完毕,查看Event Queue中的微任务,执行 // async1 end _ 微任务执行完毕,在一循环执行宏任务Event Queue中的setTimeout // 111