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

欢迎关注我的掘金鸭~

引子

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

// 节流:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。
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);

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

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

  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的运行机制了。终于要进正题了,真不容易…

js运行机制

先看一下阮一峰大佬的

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

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

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

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

我自己归类就是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))。
  • 执行一个宏任务(先进到队列中的那个宏任务),再把这次宏任务里的宏任务和微任务放到任务队列。
  • …一直重复2、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里的版本5有非常详细的解读。

参考文章:

安歌:浅谈js防抖和节流

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

前端小姐姐:彻底搞懂浏览器Event-loop

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值