深入理解闭包及事件循环机制

144 阅读6分钟

一,深入理解闭包

u=1573003895,2017084580&fm=253&app=120&size=w931&n=0&f=JPEG&fmt=auto.webp

闭包是什么?

函数内部返回函数

函数外部的变量可以在函数内部被访问到,但是函数内部的变量无法在函数外部访问到

一句话就是 在一个作用域中可以访问另一个函数内部的局部变量的函数。

闭包的实现原理

//可以访问到函数外部的全局变量 
var n = 999; 
function f1() { 
    console.log(n);
} 
f1(); // 999 //无法访问到函数内部定义的局部变量 
function f1() { 
    var n = 999; 
} 
console.log(n); // n is not defined

如果想要得到函数内的局部变量,需要在函数内部再定义一个函数。如下可见。

function f1() { 
    var n = 999; 
    function f2() { 
        console.log(n); // 999 
    } 
}

函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量。

Javascripts拥有垃圾自动回收机制,当一个值在内存中失去引用时,就会将其回收,函数执行结束后就会失去引用,其占用的内存就会被回收但是闭包的存在会阻止这个过程。

var fn = null; 
function foo() { 
    var a = 2; 
    function innnerFoo() { 
        console.log(a); 
    } 
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn 
} 
function bar() { 
    fn(); // 此处的保留的innerFoo的引用 
} 
foo();
bar(); // 2

在上面的例子中,foo()执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象。所以此刻仍然能够访问到变量a的值。

这样,我们就可以称foo为闭包。

闭包的形成条件

函数嵌套

内部函数引用外部函数的局部变量

总结:

如果想要访问到一个函数内部的局部变量就需要在函数的内部在定义一个函数,在定义的函数里面引用其局部变量,将内部函数赋值给全局变量,然后在外面调用便可得到函数的局部变量,这个在函数内部定义的函数称为闭包

index-bg.jpg

二,事件循环机制(EventLoop)

why?

单线程: JavaScript是一种单线程的编程语言,同一时间只能做一件事,所有任务都需要排队依次完成。

为什么JS不能有多个线程呢?

JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准。

执行流程

浏览器的事件循环分为同步和异步任务

同步任务 : 在主线程上排队执行的任务,只有一个任务执行完毕,才能执行后一个任务

异步任务 : 不进入主线程,而进入“任务队列(task queue)”的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

分类:异步任务又分为宏任务和微任务。

所有同步任务都在主线程上执行,形成一个函数调用栈(执行栈),而异步则先放到任务队列(task queue)里,任务队列又分为宏任务(macro-task)与微任务 (micro-task)。

  • 宏任务:script(整体代码)、setTimout、setInterval

  • 微任务:new promise().then(回调),nextTick 若同时存在promise和nextTick,则先执行nextTick

执行过程

所有同步任务都在主线程上执行,形成一个执行栈(调用栈); 主线程之外,还存在一个‘任务队列’(task queue),浏览器中的各种 Web API 为异步的代码提供了一个单独的运行空间,当异步的代码运行完毕以后,会将代码中的回调送入到 任务队列中(队列遵循先进先出得原则) 一旦主线程的栈中的所有同步任务执行完毕后,调用栈为空时系统就会将队列中的回调函数依次压入调用栈中执行,当调用栈为空时,仍然会不断循环检测任务队列中是否有代码需要执行;

执行顺序

  • 先执行同步代码,

  • 遇到异步宏任务则将异步宏任务放入宏任务队列中,

  • 遇到异步微任务则将异步微任务放入微任务队列中,

  • 当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,

  • 微任务执行完毕后再将异步宏任务从队列中调入主线程执行,

  • 一直循环直至所有任务执行完毕。

总结:

先执行同步任务,当遇到异步任务根据种类放入各自任务队列,当同步任务执行完毕之后执行异步微任务,如果异步任务中还有异步任务则继续放入异步任务队列,继续执行,直到执行完所有任务。

eg:

setTimeout(()=>{ 
    new Promise(resolve =>{ 
        resolve(); 
    }).then(()=>{ 
        console.log('test'); 
    }); 
    console.log(4); 
}); 

new Promise(resolve => { 
    resolve(); 
    console.log(1) 
}).then( () => { 
    console.log(3); 
    Promise.resolve().then(() => { 
        console.log('before timeout'); 
    }).then(() => { 
        Promise.resolve().then(() => { 
            console.log('also before timeout') 
        }) 
    }) 
}) 
console.log(2); //输出:1 2 3 before timeout also before timeout 4 test

结果分析:

  • 遇到setTimeout,异步宏任务,将() => {console.log(4)}放入宏任务队列中

  • 遇到new Promise,Promise在实例化的过程中所执行的代码都是同步进行的,所以输出1

  • 而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中

  • 遇到同步任务console.log(2),输出2;主线程中同步任务执行完

  • 从微任务队列中取出任务到主线程中,输出3,此微任务中又有微任务

  • Promise.resolve().then(微任务a).then(微任务b),将其依次放入微任务队列中

  • 从微任务队列中取出任务a到主线程中,输出 before timeout

  • 从微任务队列中取出任务b到主线程中,任务b又注册了一个微任务c,放入微任务队列中

  • 从微任务队列中取出任务c到主线程中,输出 also before timeout;微任务队列为空

  • 从宏任务队列中取出任务到主线程,此任务中注册了一个微任务d,将其放入微任务队列中,

  • 接下来遇到输出4,宏任务队列为空

  • 从微任务队列中取出任务d到主线程 ,输出test,微任务队列为空,结束