用生命谱写代码的赞歌

0%

经典闭包循环与 Promise

经典闭包循环

  • 初始题目

    1
    2
    3
    4
    5
    6
    for (var i = 0; i < 5; i++) {
    setTimeout(function() {
    console.log(new Date, i);
    }, 1000);
    }
    console.log(new Date, i);

    上面的代码等同于:

    1
    2
    3
    4
    5
    6
    7
    for (var i = 0; i < 5; i++) {
    function foo() {
    console.log(new Date, i);
    }
    setTimeout(foo, 1000);
    }
    console.log(new Date, i);

    解析:setTimeout 里面传入的第一个匿名函数,等价于在 setTimeout 语句外面定义的一个函数。所以它的闭包范围是变量 i 所在的作用域,所以可以访问到 i。由于 i 是外面的变量,所有的 setTimeout 里面的函数都会指向同一个 i。

  • 闭包改造

    1. 普通闭包

      1
      2
      3
      4
      5
      6
      7
      8
      for (var i = 0; i < 5; i++) {
      (function(j) { /* j = i */
      setTimeout(function() {
      console.log(new Date, j);
      }, 1000);
      })(i);
      }
      console.log(new Date, i);

      解析:每次把新的变量 i 传进一个匿名的立即执行函数,每次 j 都能得到不同的 i ,因为 j 在匿名函数的作用域内,函数的执行作用域每执行一次都会重新生成,所以每次的 j 都不是同一个。

    2. 传参法(其实是把闭包匿名的立即执行函数单独拿出来,然后在 for 循环的时候,向这个函数传递参数)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function wrapper(j) {
      function foo() {
      console.log(new Date, i);
      }
      setTimeout(foo, 1000)
      }
      for (var i = 0; i < 5; i++) {
      wrapper(i)
      }
      console.log(new Date, i);

      注意:这里有一个容易犯错的点,setTimeout 的第一个参数要求的是一个函数的引用,而不是执行一个函数,所以只能传入函数名 foo 的形式,而不能传入 foo(a,b),这也意味着不能在 foo 上直接传参。所以,要传参的话,在 setTimeout 外面再包裹一层函数,然后定制编写 foo 函数。

      再补充一个重要知识点,如果一定要在 foo 中传参,又不想用闭包,可以使用 setTimeout 的第3个参数,从第3个参数往后的参数,都会传入 foo 里作为形参使用。

      所以,上面的代码也可以写成:

      1
      2
      3
      4
      5
      6
      for (var i = 0; i < 5; i++) {
      setTimeout(function(j) {
      console.log(new Date, j); /* 可以立即输出01234 */
      }, 1000 ,i);
      }
      console.log(new Date, i);

      如果硬是在 setTimeout() 中传入的 foo() 的形式,那么只会以正常任务的方式立即执行 foo(),而不会放入任务队列里去,也就是定时器失效。

Promise

上述循环用 ES6 和 Promise 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var tasks=[];
function output(j) {
var promise = new Promise( function(resolve, reject) {
setTimeout(function () {
console.log(new Date(), j);
resolve(j);
// console.log("这是一个小补充哟"+j);
},1000 * i);
});
promise.then(function (j) {
// console.log("这是then里的一点小补充哟"+j);
});
return promise;
}
for (var i = 0; i < 5; i++){
tasks.push(output(i)); //执行顺序:首先在这里将定时器设置好,
// 也就是循环设置定时器,由于执行时间很快,每次循环的间隔可以忽略不计,
// 所以可以认为是设置了5个时间分别为0~4s的定时器,已经开始计时。
// 计时后返回promise对象,放在tasks数组中
}
// 最后,在这里相当于是一个总的监听器,当前面4个任务都resolve以后,执行最后一个设置定时器任务,
// 到时间以后,执行输出5
// 关于执行顺序,前4个task是靠定时器的时间差别来决定先后输出顺序的,最后一个5的task,是依靠异步回调来执行的。
Promise.all(tasks).then(function () {
setTimeout(function () {
console.log(new Date(), i);
},1000);
});

解析:关于任务队列:

  1. 首先第一趟主任务队列走下来,执行了设置定时任务,将promise对象放入tasks数组,并设置好then回调的工作。
  2. 然后第二趟,执行定时任务队列,运行consolo.log语句,
  3. 然后遇到resolve,需要调用相应的then里面的回调语句(如果有的话)。
  4. 但是注意,这里调用then的时机,是在本次任务的主代码执行完毕后,也就是说,如果setTimeout语句中的resolve()后面还有执行语句,要先执行那些语句,最后才执行resolve对应的then回调。
  5. 要确定resolve相应的回调语句的执行顺序,可以执行以下上述代码(将代码里的两句console.log去掉注释即可):

最后强调一遍,resolve的回调函数是在本轮“事件循环”结束时执行,setTimeout(fn, 0)在下一轮“事件循环”开始时执行。

举例说明:

1
2
3
4
5
6
7
8
9
10
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
  • 作者:栗子酥小小
  • 链接:原文地址
  • 來源:简书
  • 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。