JavaScript中的回调函数
徐徐 抱歉选手

回调函数

回调函数本质是一个在特定时刻特定事件下执行的函数,意思就是这个函数他和某个时间或事件相关联了,到达那个时间点或关注的事件被触发了,回调函数将被执行。

回调函数的作用是实现异步操作。程序不需要一直等待某个事件的发生才去执行回调函数,而是等事件发生后再去执行。

一个比较好理解的回答:

回调函数(callback)是什么? - 朱栩的回答 - 知乎

回调函数与JavaScript事件处理机制

参考资料

JavaScript运行机制:事件驱动编程详解

深入浅出JavaScript事件循环机制(下)

深入浅出Javascript事件循环机制(上)

Philip Roberts: Help, I’m stuck in an event loop.

event loop & callback queue

主线程与工作线程

提高CPU吞吐量的几种方式:多线程+同步I/O,单线程事件循环+异步I/O。

由于“JavaScript是一门单线程的语言”,因而采用异步IO+事件循环。这里的单线程,是指JavaScript引擎(如chrome的v8引擎)中负责解释和执行js代码的线程只有一个,称它为主线程。

但还存在其他由其他API提供的线程,如处理AJAX请求的线程,处理DOM事件的线程,读写文件的线程,这些被称作工作线程

同步任务、异步任务与事件循环

在主线程上挨个排队等待执行的任务,被称作同步任务。这些同步任务构成了执行栈(execution context stack),执行栈中的任务必须等前面一个执行完了,才能执行后面一个。

执行上下文栈(execution context stack):当JavaScript代码执行的时候,会进入不同的执行上下文,这些执行上下文会构成了一个执行上下文栈(Execution context stack,ECS)。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。

每一个执行上下文都可以抽象为一个拥有三个属性的对象。

1
2
3
4
5
executionContextObj = {
scopeChain: { /* 变量对象(variableObject)+ 所有父执行上下文的变量对象*/ },
variableObject: { /*函数 arguments/参数,内部变量和函数声明 */ },
this: {}
}

每一次JavaScript解释器调用执行上下文,每次调用都会创建,激活两个阶段。

创建阶段会依据传入参数、函数声明和变量声明设置执行上下文的上面三个属性,但不涉及具体的值,只把所有声明归纳。创建阶段依次处理顺序为:函数参数arguments,扫描上下文的函数声明,扫描上下文的变量声明并初始化为undefined。因此,就可以理解JavaScript提升了。

可以看出函数提升优先于变量提升;函数提升是把整个函数的定义与声明提升到最前面,变量只单单提升声明;但是函数表达式的声明是遵循变量提升规则的。因此,也就可以理解函数提升、变量提升的关系了。

激活阶段会在创建完成后随着代码内部一行行只派变量的值或者函数的引用。

被交由工作线程处理的任务,就是异步任务。异步任务在工作线程上被运行/处理得出结果后,被放到消息队列callback queue中。不管现在主线程怎么样了,工作线程把异步任务运行出了结果,就会转交这个异步任务到消息队列。

callback的内容就是告诉主线程:我这边工作线程上的结果出来了哈,作为参数传到了callback function中了,你在执行栈上执行一下回调函数就知道该怎么做了哈。

主线程上的所有同步任务都被执行完,也就是执行栈为空,执行栈才会去读消息队列,把消息队列中的回调函数放到执行栈中开始执行。主线程从消息队列中去读消息,这个过程是循环不断的。主线程把一个消息队列上的消息取出来放到执行栈上执行完毕,才去继续取下一个消息队列上的消息,取一个消息并执行的过程就叫做一次事件循环

对异步任务的再度划分macro-task/micro-task

要等到执行栈上的同步任务清空,才去消息队列中取异步任务放到执行栈执行。但是有些异步任务要优先解决,等不及被取出后再次被放到消息队列中去,这种情况下该怎么办?细分一下异步任务,可以分为macro-task与micro-task。

上面提到的同步任务称为task,多个同步任务组成一个task queue。异步任务即macro-task,多个异步任务组成macro-task queue,运行机制为take one item and one item only。而micro-task,是需要在本轮事件循环结束前、本轮事件循环所有task结束后执行的任务,多个micro-task组成micro-task queue,运行机制为运行直到清空队列(inluding any additional queued items),这种情况下event loop cannot continue until that queue has completely emptied and that is why it blocks rendering。

用下面的例子解释micro-task queue的运作方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
// 1 2 3 5 4

123的输出属于正常执行栈中同步任务task的执行流程。

那为什么接下来是5?因为promise.then属于micro-task,他会在本次事件循环结束之前,也就是访问macro-task消息队列之前,被执行。而setTimeout就在macro-task消息队列中等着下一次事件循环被取用。

常见的task/micro-task/macro-task分类

  • 每一个同步语句都是task,例如console.log()
  • macro-task一般都是异步任务,包括setTimeout、setInterval、setImmediate、I/O, UI rendering等
  • micro-task一般都是能够阻止下一次事件循环,从而阻止浏览器渲染的方法,包括process.nextTick、Promises.then、Object.observe等

回调函数与this

React官方文档事件处理一章节给出了一个例子,提到需要谨慎使用JSX回调中的this。

this.handleClick = this.handleClick.bind(this);

一旦删除构造函数中的这句语句:

  • 一开始会正常显示初始页面(这是因为构造函数中初始化了属性)。

  • 一旦进行点击操作,也就是通过“点击”事件触发回调函数handleClick(),就会导致TypeError: Cannot read property ‘setState’ of undefined——也就是handleClick()中的this是未定义的。

对于事件处理器/事件处理函数handleClick():

  • 为什么需要添加语句?

    从它被定义的外围环境来看,该函数被作为一个类的方法在class组件中,而类的方法的this默认是取决于他们如何被调用的,是不会绑定指向这个类的实例的。通过改写这个行为,让类中this的值指向这个类实例,也就是开头提到的bind()语句。

  • 为什么删除语句会报错?

    从它是一个函数内部环境来看:在非严格模式下this的值默认指向全局对象,在浏览器中就是window;在严格模式下,进入执行环境却没有设置this的值,this会保持undefined

参考:MDN Web Docs关于this的说明

  • 本文标题:JavaScript中的回调函数
  • 本文作者:徐徐
  • 创建时间:2020-10-06 12:07:37
  • 本文链接:https://machacroissant.github.io/2020/10/06/callback-function/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论