相信有一定JavaScript程序编写经验的童鞋一定写过回调函数,我也不例外,然而起初写回调函数是看别人代码就是这么写的,依葫芦画瓢,对于回调函数的理解也停留在JavaScript中函数可以作为参数传递,并未深究。然而当接触到es6语法中的promise继而es7中的async/await时彻底懵,痛定思痛,从头梳理。
在理解为什么要使用回调函数以及回调函数的执行过程之前,需要进行很长的铺垫~~~
执行上下文、函数调用栈、任务队列
首先介绍JavaScript中最为重要的一个概念—— 执行上下文(execution context)。执行上下文定义了变量或函数有权访问的其他数据,并且将这些数据赋给与之对应的变量对象(variable object)。当代码在一个上下文中执行时,会创建变量对象的一个作用域链,作用域链的用途是保证执行上下文对有权访问的变量和函数的有序访问。作用域链的下一个变量对象来自包含上下文,再下一个变量对象来自下一个包含上下文,这样一直延伸到全局执行上下文,全局执行上下文的变量对象始终是作用域链的最后一个对象。执行上下文大致包括一下三种情况:
- 全局上下文:全局执行上下文是最外围的一个执行上下文,根据ECMAScript所在的宿主不同,表示全局执行上下文的对象也不同,在浏览器中,全局执行上下文对象被认为是window对象,在node环境中则是global对象。
- 函数上下文: 当执行流进入一个函数时,会把该函数的执行上下文推入一个栈,该栈称其为函数调用栈,栈底永远是全局上下文,栈顶则是当前的执行上下文,执行完毕后则被推出函数调用栈。
- eval:尽量不使用,存在安全问题。
JavaScript中除了函数调用栈之外还有任务队列,当在执行过程中遇到类似于setTimeout, ajax, DOM操作时,会交给浏览器的其他模块进行处理,例如webkit的webcore模块分别提供了DOM Binding、network、timer模块来处理底层实现。等到这些模块处理完这些操作的时候将回调函数放入任务队列中,之后等函数调用栈中的task执行完之后再去执行任务队列之中的回调函数(回调函数终于出现了~~~)。
单线程特性
看到这儿,有人会问为什么JavaScript为什么要搞个函数调用栈和任务队列两套东东呢?像其他语言类似C++,我可以在执行某个函数的时候暂停然后再去执行其他函数嘛!原因在于JavaScript的一大特点——单线程,该线程中拥有唯一的事件循环。相对于其它多线程语言而言,单线程的优点在于Run-to-completion,只有在当前消息执行完成之后才会继续执行其他消息,不会中途停止,然而单线程带来的问题也很显然,比如当浏览器正在执行某个执行时间很长的消息时,用户点击了网页的某个按钮或拖动了滚动条,浏览器无法执行该消息导致页面无法响应。
回调函数
为了解决JavaScript单线程带来的问题,回调函数应运而生。例如典型的AJAX请求,若是使用回调函数进行处理,代码就可以继续进行其他任务,而无需空等:
function fn(url, cb) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readystate === 4 && xhr.status === 200) {
console.log('enhen')
cb()
}
};
xhr.open('GET', url)
xhr.send()
}
fn('test.html', function() {
console.log('hhh')
})
console.log('hohoho')
执行结果如下:
hohoho
enhen
hhh
enhen
hhh
enhen
hhh
在上面这个例子中,xhr.onreadystatechange是一个event handler,会被交给浏览器内核的其他模块进行处理,当状态发生变化事件被触发时,event handler将会将绑定的函数添加到任务队列中(个人觉得上面例子的函数命名有误导性质,真正的回调函数应该时绑定在xhr.onreadystatechange上的匿名函数,而非函数名为cb的函数)。
写了一晚上,算是撸顺了,希望睡一觉起来不要又懵了?亲你看懂了嘛~~~