由节流函数引起出对event-loop的思考,顺便刷刷爆款题

引子

当我在看节流函数的时候,碰到了setTimtout,因而从js运行机制挖到了event-loop。那么我们就先从这个简单的节流函数看起。html

// 节流:若是短期内大量触发同一事件,那么在函数执行一次以后,该函数在指定的时间期限内再也不工做,直至过了这段时间才从新生效。
function throttle (fn, delay) {
    let sign = true;
    return function () {    // 闭包,保存变量的值,防止每次执行次函数,值都被重置
        if (sign) {
            sign = false;
            setTimeout (() => {
                fn();
                sign = true;
            }, delay);
        } else {
            return false;
        }
    }
}
window.onscroll = throttle(foo, 1000);
复制代码

那么这个节流函数是怎么实现的节流呢?前端

让咱们来看一下它的执行步骤(假设咱们一直不停的在滚动):node

  1. 当咱们打开页面,代码执行到window.onscroll = throttle(foo, 1000)就会直接执行 throttle函数,定义了一个变量 sign 为 true,而后碰到了 return 跳出 throttle函数,并返回另外一个匿名函数。
  2. 而后咱们滚动页面,那么就会触发 onscroll 事件,执行 throttle函数。而此时咱们的 throttle函数,实际就是执行 return 的那个匿名函数。由于闭包的缘故,保存了 sign的值(感受还要填个闭包的坑...),此时的sign 是 true。就执行 if判断,把sign 改成 false。而后碰到了定时器,咱们如今不用管定时器的回调函数的内容。
  3. 咱们还一直在滚动,那么又触发了 onscroll事件,因而继续进行 if else 判断。此时 sign 已是false了,什么都没有发生。
  4. 继续,咱们一直不停的在滚动,仍是触发了 onscroll事件,由于 sign 仍是false,因此仍是什么都没有发生。
  5. 一直重复步骤4,直到1s之后的那个 onscroll事件执行完成后,咱们的setTimeout被执行了,首先执行了咱们的须要被执行的fn()函数,而后把 sign置为 true。又开始跟前面同样,执行 if判断了。

那么为何在执行了 if判断的过程当中,碰到了setTimeout,咱们的sign并无被改成true,从而一直的执行 if判断呢?那么就须要聊一聊js的运行机制了。终于要进正题了,真不容易...chrome

js运行机制

先看一下阮一峰大佬的segmentfault

(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。promise

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

(3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。bash

(4)主线程不断重复上面的第三步。markdown

我本身归类就是js中有:闭包

  • 同步任务和异步任务

  • 宏任务(macrotask)和微任务(microtask)

  • 主线程(同步任务) - 全部同步任务都在主线程上执行,造成一个执行栈。

  • 任务队列(异步任务):当异步任务有告终果,就在任务队列中放一个事件。

  • JS运行机制:当"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列"

其中宏任务包括:script(主代码), setTimeout, setInterval, setImmediate, I/O, UI rendering

微任务包括:process.nextTick(Nodejs), Promises, Object.observe, MutationObserver

这里咱们注意到,宏任务里有 script,也就是咱们的正常执行的主代码。

事件循环 event-loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,因此整个的这种运行机制又称为Event Loop(事件循环)。此机制具体以下:主线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查microtask队列是否为空(执行完一个任务的具体标志是函数执行栈为空),若是不为空则会一次性执行完全部microtask。而后再进入下一个循环去任务队列中取下一个任务执行。

我又给总结了一下笼统的过程:script(宏任务) - 清空微任务队列 - 执行一个宏任务 - 清空微任务队列 - 执行一个宏任务, 如此往复。

  • 先执行script里的同步代码(此时是宏任务)。碰到异步任务,放到任务队列。
  • 查找任务队列有没有微任务,有就把此时的微任务所有按顺序执行 (这就是为何promise会比setTimeout先执行,由于先执行的宏任务是同步代码,setTimeout被放进任务队列了,setTimeout又是宏任务,在它以前先得执行微任务(就好比promise))。
  • 执行一个宏任务(先进到队列中的那个宏任务),再把此次宏任务里的宏任务和微任务放到任务队列。
  • ...一直重复二、3步骤

要作到心中有队列,有先进先出的概念

借用前端小姐姐的一张图来解释:

event-loop2

如今再看开头的节流函数,就明白为何碰到了setTimeout,咱们的sign并无被改成true了把。

那咱们继续,看一下最近看到的爆款题。

开始闯关

第一关

看这段代码

console.log('script start');

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

new Promise((resolve) => {
    resolve('Promise1');
}).then((data) => {
    console.log(data);
});

new Promise((resolve) => {
    resolve('Promise2');
}).then((data) => {
    console.log(data);
});

console.log('script end');
复制代码

对照这上面的执行过程不可贵出结论,script start -> script end -> Promise1 -> Promise2 -> setTimeout1

就算 setTimeout 不延时执行,它也会在 Promise以后执行,谁让js就是先执行同步代码,而后去找微任务再去找宏任务了呢。

懂了这里,那咱们继续咯。

第二关

setTimeout(() => {
    console.log('setTimeout1');

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

    Promise.resolve().then(data=>{
        console.log('setTimeout 里的 Promise');
    });
}, 0);

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

Promise.resolve().then(() => {
    console.log('Promise1');
});
复制代码

根据前面的流程

  1. 执行script,看到了第一个 setTimeout 放入任务队列,看到了第二个 setTimeout 放到任务队列。看到了Promise.then() 放到任务队列,并无同步代码。
  2. 检查微任务,发现了 Promise.then() 打印Promise1
  3. 检查发现没有别的微任务了,检查宏任务,此时有两个宏任务(两个setTimeout),可是规则告诉咱们,只执行一个宏任务,由于队列是先进先出的原则,执行先进入队列的那个 setTimeout,打印 setTimeout1。又发现了 一个 setTimeout,放进任务队列。看见了 Promise.then() ,打印setTimeout 里的 Promise
  4. 检查宏任务,发现了宏任务,执行先进的那个,因此打印setTimeout2
  5. 检查微任务,没有。
  6. 检查宏任务,打印setTimeout3

搞清楚了这个,那咱们再继续玩儿玩儿?

第三关

console.log('script start');

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

new Promise((resolve) => {
    console.log('Promise3');
    resolve();
}).then(() => {
    console.log('Promise1');
});

new Promise((resolve) => {
    resolve();
}).then(() => {
    console.log('Promise2');
});

console.log('script end');
复制代码

再来看看这个代码的执行结果呢。

script start -> Promise3 -> script end -> Promise1 -> Promise2 -> setTimeout1

有些朋友可能会说,不是说好了 Promise 是微任务,要在主代码执行之后才执行嘛,你个 Promise3 咋叛变了。

其实 Promise3 没有叛变,以前说的 Promise微任务是.then()执行的代码。而在new Promise的回调函数里的代码是同步任务。

第四关

咱们继续看关于promise的

setTimeout(()=>{
    console.log(1) 
},0);

let a=new Promise((resolve)=>{
    console.log(2)
    resolve()
}).then(()=>{
    console.log(3) 
}).then(()=>{
    console.log(4) 
});

console.log(5);
复制代码

这个输出 2 -> 5 -> 3 -> 4 -> 1。你想对了嘛?

这个要从Promise的实现来讲,Promise的executor是一个同步函数,即非异步,当即执行的一个函数,所以他应该是和当前的任务一块儿执行的。而Promise的链式调用then,每次都会在内部生成一个新的Promise,而后执行then,在执行的过程当中不断向微任务(microtask)推入新的函数,所以直至微任务(microtask)的队列清空后才会执行下一波的macrotask。

第五关

promise继续进化

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})
复制代码

直接上解释吧。

遇到这种嵌套式的Promise不要慌,首先要心中有一个队列,可以将这些函数放到相对应的队列之中。

Ready GO

第一轮

  • current task: promise1是当之无愧的当即执行的一个函数,参考上一章节的executor,当即执行输出[promise1]
  • micro task queue: [promise1的第一个then]

第二轮

  • current task: then1执行中,当即输出了then11以及新promise2的promise2
  • micro task queue: [新promise2的then函数,以及promise1的第二个then函数]

第三轮

  • current task: 新promise2的then函数输出then21和promise1的第二个then函数输出then12
  • micro task queue: [新promise2的第二then函数]

第四轮

  • current task: 新promise2的第二then函数输出then23
  • micro task queue: []

END

可能有人会对第二轮的队列表示疑问,为何是 ”新promise2的then函数“ 先进了队列,而后才是 ”promise1的第二个then函数“ 进入队列?”新promise2的第二then函数“ 为何有没有在这一轮中进入到队列中来呢?

看不懂不要紧,咱们来调试一下代码:

在打印完 promise2 之后,19行先执行到了 })这里,而后到了then这里。

再下一步,到了 promise1的第二个})这里了。并无执行20行的console.log。

由此看出:promise2的第一个then进入任务队列中了。并无被执行.then()。

继续执行,打印 then21

由此得出:promise1的第二个then放入异步队列中,并无被执行。程序执行到这里,宏任务算是执行完了。检查微任务,此时队列中放着 [ '新promise2的then函数', 'promise1的第二个then函数'] ,也就是第二轮所写的队列。

这一步,到了promise2的二个then前面的})

往下执行到了这里,又碰到了异步,放入队列中去。

此时队列: [ 'promise1的第二个then函数' ,'promise2的第二个then函数' ]

打印 promise1 的 then12

先进先出,因此先执行了 'promise1的第二个then函数' 。

此时队列: [ 'promise2的第二个then函数' ]

最后才输出了 then23


第六关 async/await

截至到上一关,我本觉得我已经彻底掌握了event-loop。后来我看到了 async/await , async await是generatorPromise 的语法糖这个你们应该都知道,可是打印以后跟我预期的不太同样,顿时有点儿蒙圈,后来一分析,原来如此。

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

async  function async2() {
    console.log( 'async2');
}

console.log("script start");

setTimeout(function () {
    console.log("settimeout");
},0);

async1();

new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
console.log('script end'); 
复制代码

这段代码也算是网红代码了,我已经不下三个地方见过了...

先仔细想想应该输出什么,而后打印一下看看。(chrome 73版本打印结果)

script start
async1 start
async2
promise1
script end
async1 end
promise2
settimeout
复制代码

直接从async开始看起吧。

当程序执行到了async1();的时候

  • 首先输出async1 start

  • 执行到await async2();,会从右向左执行,先执行async2(),打印async2,看见await,会阻塞代码去执行同步任务。

async/await仅仅影响的是函数内的执行,而不会影响到函数体外的执行顺序。也就是说async1()并不会阻塞后续程序的执行,await async2()至关于一个Promise,console.log("async1 end");至关于前方Promise的then以后执行的函数。

如此一来,就能够得出上面的结果了。

可是,你也许打印出来会是下面这样的结果:

clipboard.png

这个就跟V8有关系了(在chrome 71版本中,我打印出的是图片中的结果)。至于async/await和promise到底谁会先执行,这里偷个懒,你们看 小美娜娜:Eventloop不可怕,可怕的是赶上Promise里的版本4有很是详细的解读。

第七关: Node: process和setImmediate (node11之后的版本)

先看第一个代码,思考一下答案

async function async1() {
    console.log("async1 start");
    await  async2();
    console.log("async1 end");
}
async  function async2() {
    console.log( 'async2');
}
console.log("script start");
setTimeout(function () {
    console.log("settimeout");
});
async1()
new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
setImmediate(()=>{
    console.log("setImmediate")
})
process.nextTick(()=>{
    console.log("process")
})
console.log('script end'); 
复制代码

再看下面的代码,思考一下答案,会是不同的吗?

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start')
setTimeout(function () {
  console.log('settimeout')
}, 1000)
async1()
new Promise(function (resolve) {
  console.log('promise1')
  resolve()
}).then(function () {
  console.log('promise2')
})
setImmediate(() => {
  console.log('setImmediate')
})
process.nextTick(() => {
  console.log('process')
})
console.log('script end')
复制代码

先看答案: 第一个

script start
async1 start
async2
promise1
script end
process
async1 end
promise2
setTimeout
setImmediate
复制代码

第二个

script start
async1 start
async2
promise1
script end
process
async1 end
promise2
setImmediate
setTimeout
复制代码

阿勒?setTimeout和setImmediate顺序竟然不同了。这是为啥呢

7.1 setImmediate

由于 setImmediate 是在I/O回调只有当即执行。

对于以上代码来讲,setTimeout 可能执行在前,也可能执行在后。 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的 进入事件循环也是须要成本的,若是在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调 若是准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了

我的测试了一下,setTimeout(fn, 2)的时候,也是先执行setTimeout,若是设置为3ms或者以上的时候,会先执行setImmediate。

但当两者在异步i/o callback内部调用时,老是先执行setImmediate,再执行setTimeout

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})
// immediate
// timeout
复制代码

7.2 process.nextTick

这个函数实际上是独立于 Event Loop 以外的,它有一个本身的队列,当每一个阶段完成后,若是存在 nextTick 队列,就会清空队列中的全部回调函数,而且优先于其余 microtask 执行。

setTimeout(() => {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)
process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
复制代码

参考文章:

安歌:浅谈js防抖和节流

阮一峰:JavaScript 运行机制详解:再谈Event Loop

前端小姐姐:完全搞懂浏览器Event-loop

小美娜娜:Eventloop不可怕,可怕的是赶上Promise

隆金岑:js事件循环机制(浏览器端Event Loop) 以及async/await的理解

浪里行舟: 浏览器与Node的事件循环(Event Loop)有何区别?

相关文章
相关标签/搜索