深挖【let, for与定时器】引发的疑惑

更新日期: 2022-05-24阅读: 628标签: 定时器

经典的问题

在一些文章中或者工作面试问题上,会遇见这种看似简单的经典问题。

for(var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  });
}
console.log('hello word');
/*output
'hello word'
5
5
5
5
5
*/

新手第一次看到这个问题由于没有深入了解setTimeout方法的执行机制就会得到错误的结果。

/*output
0
1
2
3
4
'hello world'
*/

对于老鸟来说这种问题不足挂齿,但是如果你是新手正在学习js的路上如火如荼或是刚好遇到了此类问题一知半解,那么这篇文章将带给你视野和解答。 小小问题背后实则包含丰富有趣的学问。

认识单线程、任务队列和事件循环

单线程

JS是典型的单线程语言,所谓单线程就是只能同时执行一个任务。
之所以是单线程而不是多线程,是为了避免多线程对同一dom对象操作的冲突。比如a线程创造一div元素而b线程同时想要删除这个div元素那么就会出现矛盾。所以单线程是JS的核心特征。

知识延申:操作系统的进程和线程:

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
一个进程至少有一个线程,复杂的进程有多个线程。操作系统通过多核cpu快速交替执行这些线程就给人一种同时执行的感觉。

任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,后一个任务才会执行。前面的任务耗时过长,后面的任务也得硬着头皮等待。而任务执行慢通常不是cpu性能不行,而是I/O设备操作耗时长比如Ajax操作从网络读取数据)。

JS设计者意识到,遇到这种情况主线程可以完全不管I/O设备的结果,先挂起等待结果的任务,然后执行排在后面的任务。直到I/O设备返回了结果,再回过来执行先前挂起的任务。

所以,设计者把计算机的程序任务可以分为两种,同步任务和异步任务。同步任务:直接进入主线程执行的任务。前面的任务执行完,后面的才能执行,按顺序一个接一个的执行;异步任务:不会直接进入主线程,而是通过“任务队列”(task queue)通知“主线程队列”准备就绪才会进入主线程执行。

具体来说整个机制如下:

  1. 所有同步任务都在主线程上执行,生成一个执行栈(execution context stack)。
  2. 主线程外单独划分出一个任务队列(task queue)。异步任务在同步任务执行时也不会“偷懒”,同时运行得到结果。然后异步任务会生成一个对应的通知事件放置于“任务队列”。
  3. 待执行栈中的同步任务执行完毕,系统就会读取任务队列中的通知事件,通知事件所对应的异步任务就会结束等待状态进入执行栈开始执行。并且通知事件遵循先进先出原则。
  4. 主线程会不断重复以上三步。
    机制流程示意图:
    执行栈一空就会读取任务队列,如此往复,这就是JS的运行机制。


事件和回调函数的关系
任务队列中的通知事件包括了I/O设备事、用户点击、页面滚动等等。只要指定了回调函数(callback)这些事件就会进入任务队列,等待主线程读取。

回调函数(callback)的代码会被任务队列挂起。所以需要异步执行某个程序时就请使用回调函数,主线程读取任务队列时会先检查通知事件是否包含【定时器】确认执行时间之类的。

事件循环

主线程读取任务队列事件是往复循环的,整个机制被称之为事件循环(event loop)。
接下来参考Philip Roberts的演讲《Help, I'm stuck in an event-loop》深挖事件循环


从上面的图示我们能够看到,主线程执行时产生两个事物,分别是堆(heap)和栈(stack),栈会调用各种外部的Webapi,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取任务队列,依次执行那些事件所对应的回调函数。

定时器[setTimeout]

回过头来看文章开头那段代码

for(var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  });
}
console.log('hello word');

从前面【事件循环】小节我们知道了,setTimeout属于异步任务,它会生成一个事件(对应指定的回调函数)放进任务队列挂起,直到栈中的同步任务都执行完毕后,系统读取任务队列拿到通知事件对应的回调函数再放进栈执行并返回结果。

所以实质上可以看作(取巧方便理解,非实质):

// 同步执行
for(var i = 0; i < 5; i++) {
}
// 同步执行
console.log('hello word');

// 异步执行
console.log(i);
console.log(i);
console.log(i);
console.log(i);
console.log(i);

作用域 + 闭包

作用域简单的说就是js程序当前执行的语境,或者值和表达式可访问和引用的语境。对象在这个语境中才能才能访问和引用这个语境中的其他对象。子作用域的对象可以访问和引用父作用域中的对象,反之不行。
特殊的是一个函数对象在JS中被创建的时候同时创建了闭包,闭包是由该函数对象和它所在的语境而构成的一个组合。通常返回一个函数的引用。

// 一个典型的闭包
function makeFunc() {
  var text = "hello world";
  function displayName() {
      console.log(text);
  }
  return displayName;
}
var myFunc = makeFunc();
myFunc();

回过头来看文章开头那段代码,我们就可以利用闭包的原理让定时器打印出0, 1, 2, 3, 4。

for(var i = 0; i < 5; i++) {
  ((i) => {
    setTimeout(function () {
    console.log(i);
	});
  })(i);
}
console.log('hello word');

在上面的代码中,使用了一个技巧立即函数给计时器单独提供了一个新的作用域,加上里面的计时器就刚好组成了一个异步的闭包组合,而且是立刻调用的。

通过上面的手段就可以很好的避免var声明的循环变量暴露在全局作用域带来的问题。从而打印出0, 1, 2, 3, 4。

另外通过let声明循环变量也是很好的解决手段,let允许你声明一个被限制在块作用域中的变量、语句或者表达式,这个就是块级作用域。

for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  });
}
console.log('hello word');

let是ES6语法,而块级作用域的出现解决了var循环变量泄露为全局变量的问题和变量覆盖的问题。

回到上面的代码,着重说下let是如何做到每次循环能够记忆当前i的值并传给下次循环的:

  1. 首先,在for循环中,设置循环变量的括号实质上是一个父作用域,而循环体是子作用域。
  2. let声明了该父作用域是块级作用域而不是全局作用域,每次循环i的值只对当前循环的块级作用域有效,就像是块级作用域是一支捕虫网,捕获循环更新的i值。循环一次就会更新块级作用域以及变量i,好比拿新的捕虫网来捕获新i。
  3. 说白了,每次循环变量i会重新声明初始化i。实质上是JS引擎内部会记忆上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
  4. 循环体内部函数会先访问本身的块级作用域,没有i就继续向上查询循环体作用域,没有i向上查询父作用域拿到当前循环记忆(捕获)的i值最后打印出来。
  5. 细心的朋友其实已经发现了,循环体内部函数 + 往上查询的块级作用域语境刚好组成了类似闭包的组合。

对于不能兼容ES6的浏览器,我们也可以使用ES5try...catch...语句,形成类似闭包的效果。

for(var i = 0; i < 5; i++) {
  try {
    throw(i)
  } catch(j) {
    setTimeout(function () {
    console.log(j);
	});
  }
}
console.log('hello word');
来自:https://www.cnblogs.com/mxyulin/archive/2022/05/24/16305976.html

链接: https://www.fly63.com/article/detial/11581

js中setTimeout和setInterval的深入理解:它们之间的区别,原理,“异步“等

这篇文章将带你深入理解js中定时器是如何工作的,setTimeout和setInterval的原理是什么?

为什么尽量别用setInterval

在开发一个在线聊天工具时,经常会有过多少毫秒就重复执行一次某操作的需求。“没问题”,大家都说,“用setInterval好了。”我觉得这个点子很糟糕。

你可能不知道的setInterval的坑

之前印象中一直记得setInterval有一些坑,但是一直不是很清楚那些坑是什么。setInterval会无视代码的错误、setInterval会无视任何情况下定时执行、、setInterval不能确保每次调用都能执行

setInterval和setTimeout的区别以及setInterval越来越快问题的解决方法

setInterval()和setTimeout()方法都是js原生的定时方法,当然它们两个的作用也是不同的,并且最近在做上下滚动公告栏的时候,发现了setInterval()非常令人抓狂的问题,那就是用setInterval()做的定时滚动会随着浏览器页面切换变得无法控制!为什么会说无法控制呢

如何通过setTimeout理解JS运行机制详解

setTimeout()函数:用来指定某个函数或某段代码在多少毫秒之后执行。它返回一个整数,表示定时器timer的编号,可以用来取消该定时器。JavasScript引擎是基于事件驱动和单线程执行的,JS引擎一直等待着任务队列中任务的到来

node-schedule 全局内关闭定时器

用Cron表达式完成定时器,全局内关闭定时器需要获取到定时器的引用,scheduleJob存在第四个参数,然而readme中没有提及,可知API

js 随机点名

主要是利用定时器,点击开始IDE时候不断的执行,并同时生成随机数,利用数组的下标完成展示。主要用到的知识点:setInterval,Math.random()

js定时器setTiemout、setInterval

JS提供了一些原生方法来实现延时去执行某一段代码,下面来简单介绍一下setTiemout、setInterval、setImmediate、requestAnimationFrame。JS提供了一些原生方法来实现延时去执行某一段代码,下面来简单介绍一下。

Js定时器越走越快的问题

之前在项目中写了定时器来做循环播放,但是总是会有越走越快的问题,开始是以为前后的HTML代码拼接的有问题,时间紧急的情况下反复改了很多也没什么效果,后来发现是js定时器的问题,在这里记录一下。

JS 定时器的4种写法及介绍

JS提供了一些原生方法来实现延时去执行某一段代码,下面来简单介绍一下setTiemout、setInterval、setImmediate、requestAnimationFrame。setTimeout: 设置一个定时器,在定时器到期后执行一次函数或代码段

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!