以小见大——从setTimeout引伸JS的几大特性

前言

最近在复习JS基础,回新手村整理下笔记
回想当初看书的过程,有两位朋友曝光度极高
那就是setTimeoutsetIntervalhtml

他们的一些迷惑行为,初看实在让人摸不着头脑,但其实背后暴露出了JS的几大特性
若是能全盘理解,也就能基本掌握JS的一些原理了node

先介绍一下

setInterval,是每隔一段时间执行一次函数,而setTimeout则是一段时间后进行,他们的用法基本同样,因此下面也就只讲解setTimeoutchrome

setTimeout(function (a,b) { console.log(a+b) }, 2000, 1, 2)promise

  • 第一个参数: 推迟执行的回调函数,也能够直接写函数名
  • 第二个参数: 推迟的毫秒数

若是不设置浏览器会自动配置时间,在IE,FireFox中,第一次配可能给个很大的数字,100ms上下,日后会缩小到最小时间间隔,Safari,chrome,opera则多为10ms上下。浏览器

  • 从第三个参数开始,是给回调函数传的参数
  • 返回值:定时器的id

能够经过clearTimeout(id)来清除这个定时器
或者也可使用setInterval的清除方法clearInterval()
只不过从语义上来讲不推荐bash

迷惑一:执行顺序

虽然setTimeout能够定时执行函数,但实际上它的执行时间不是精确的 这就要说到它的原理了
咱们先看一个极端的例子,把第二个参数设置为0闭包

setTimeout(function () {
    console.log('1')
}, 0)
console.log('2')
//2 1
复制代码

虽然设置为0了,但也不是当即执行的
这个API是浏览器提供的,因此浏览器处理后会将setTimeout要执行的匿名函数添加到异步队列
须要等待到函数调用栈清空以后,即全部可执行代码执行完毕以后,才会开始执行执行这个异步队列,而且是先进先出
而setTimeout设定的延迟时间,并不是相对于setTimeout执行这一刻,而是相对于其余代码执行完毕这一刻。app

大体过程如上,理解了异步的过程大概也就明白执行顺序了
可是,其实这不够全面
出了新手村以后碰见了各式各样的新的朋友 好比 promise

迷惑二:比promise的优先性差?

setTimeoutpromise都是异步的,按照队列先进先出的顺序来讲
若是给setTimeout设置为0,同时放置在promise以前,那应该执行完同步代码以后就执行setTimeout的函数异步

setTimeout(function () {
    console.log('setTimeout1');
}, 0);
new Promise(function (resolve) {
    resolve();
}).then(function () {
    console.log('then1')
})
console.log('script end')
复制代码

但实际上的输出结果是async

// script end
// then1
// setTimeout1
复制代码

.then()setTimeout优先执行了
那再有个async函数的话,执行顺序又是什么呢
若是不能坚决地回答,那说明咱们以前的理解必定还差了点东西

Event Loop

完整的事件循环如上图

异步队列还分为Task(宏任务)队列MicroTask(微任务)队列
在最新标准中,它们被分别称为task与jobs。
MicroTask会优先于Task执行。

  • 宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O(包括各类键鼠事件), UI rendering(不肯定).
  • 微任务:process.nextTick, Promise, Object.observer,MutationObserver,callback

同时,Javascript引擎在执行Microtask队列的时候,若是期间又加入了新的Microtask,则该Microtask会加入到以前的Microtask队列的尾部,保证Microtask先于Task队列执行。

  1. 先在执行栈中执行整个script。
  2. 遇到微任务和宏任务,分别添加到微任务队列和宏任务队列中去。
  3. 当前宏任务执行完毕,当即执行微任务队列中的任务
  4. 当前微任务队列中的任务执行完毕,检查渲染,GUI线程接管渲染。
  5. 继续执行下一个宏任务从事件队列中取。

因此在咱们写下setTimeout(fn,0)的时候他并非在当时当即执行,是从下一个Event loop开始执行,便是等当前全部脚本执行完再运行,就是"尽量早"。

引用自https://juejin.im/post/5b93829de51d450e7579b171

若是想挑战一下,能够看一下这道题目

输出结果

async/await其实就是 promise的语法糖
async function 声明将定义一个返回 AsyncFunction对象的异步函数。
当调用一个 async 函数时,会返回一个 Promise 对象。
await以后的函数语句至关于被包裹在 .then()里面,因此被推动了微任务队列

注意:
上面的测试结果是谷歌浏览器73版本以后输出的结果
在谷歌浏览器73版本之前,以及node中,promise的优先级都要大于这个await给出的回调函数,因此即使在任务队列中await的回调是先进入的,也要在promise.then()以后执行
也就是说async1 endthen3的顺序会颠倒

不过在73版本以后,为了不await的执行须要至少3次tick,性能比较慢,因此 使用对PromiseResolve的调用来更改await的语义,以减小在公共awaitPromise状况下的转换次数。
若是传递给await的值已是一个Promise,那么这种优化避免了再次建立Promise包装器,在这种状况下,咱们从最少三个microtick到只有一个microtick
因此上图async1 end 会在then3前面

迷惑三:this指针

var x=1
function hhh () {
var x=2
setInterval(function() {
console.log(x)
console.log(this.x)},1000)}
// 2 1
复制代码

this指针的指向是最使人头疼的,四种绑定很是反直觉
函数中的this指向的是执行上下文,而这个例子中匿名函数最终执行的环境就是浏览器,因此this.x就是window.x

关于执行上下文

当浏览器加载script的时候,默认直接进入Global Execution >Context(全局上下文),将全局上下文入栈。若是在代码中调用了函数,则会建立Function Execution Context(函数上下文)并压入调用栈内,变成当前的执行环境上下文。当执行完该函数,该函数的执行上下文便从调用栈弹出返回到上一个执行上下文。 能够看着这个图感觉一下

但不少时候,咱们看不清函数究竟是在什么上下文执行的,因此 ES6的箭头函数必定程度上解决了这个问题, this指向的是声明时的上下文

还有相似这样的例子

function User(login) {
this.login = login;
this.sayHi = function() {
console.log(this.login);
}
}
var user = new User('John');
setTimeout(user.sayHi, 1000);
// undefined
复制代码

能够这样调用来解决问题

setTimeout(function() {
	user.sayHi();
}, 1000);
复制代码

或者利用bind进行绑定
也能够用call或者apply方法,可是会致使函数当即执行,失去延时效果

setTimeout(user.sayHi.bind(user), 1000);
复制代码

迷惑四:闭包

var x=1
function hhh () {
var x=2
setTimeout(function() {
    console.log(x)
1000)}
// 2
复制代码

由于在调用setTimeout时发生了闭包
而匿名函数在执行时虽然已经不在hhh函数环境里了,但被定义的时候被告知:执行的时候你去调用hhh函数的x,已经绑定给你了
注意,在定义时只是进行绑定,并无真正传参\

因此下面会发生下面这个老生常谈的问题

for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
//输出5个5,且每隔一秒一次
复制代码

上面这段话,咱们能够把它翻译一下

var i = 5;
function timer() {
console.log(i);
}
setTimeout( timer, 1 * 1000 );
setTimeout( timer, 2 * 1000 );
setTimeout( timer, 3 * 1000 );
setTimeout( timer, 4 * 1000 );
setTimeout( timer, 5 * 1000 );
复制代码

因此一秒一次,以及输出都是5
由于在定义匿名函数的时候,使用了i值来设置时间
可是参数只是进行了绑定,真正执行的时候才会取到那个值,而此时i已经变成了5

解决办法有三种:

  • 把var改为let
  • 用当即函数包裹匿名函数
  • 利用setTimeout的第三个参数立即传参

方法蛮多,你们应该都会用,就不赘述了
但其实原理都同样,就是不把匿名函数的参数绑定到公用的i值上去,而是每次循环时,将i值保存在一个闭包中,当匿名函数执行时,则访问对应闭包保存的i值便可

总结

setTimeout和setInterval其实并不推荐被大量使用
尤为是setInterval,可能会出现间隔被跳过的问题,这个能够参考这篇文章 www.cnblogs.com/xiaohuochai…

但经过对他们进行研究,能够以小见大地理解JS运行机制 原理部分若是有写的不对的,欢迎指正

相关文章
相关标签/搜索