Callback问题

一、什么是回调函数?

    A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.

    编程分为两类:系统编程(system programming)和应用编程(application programming)。所谓系统编程,简单来说,就是编写库;而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用。系统程序员会给自己写的库留下一些接口,即API(application programming interface,应用编程接口),以供应用程序员使用。所以在抽象层的图示里,库位于应用的底下。当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。打个比方,你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。


        可以看到,回调函数通常和应用处于同一抽象层(因为传入什么样的回调函数是在应用级别决定的)。而回调就成了一个高层调用底层,底层再回过头来调用高层的过程。(我认为)这应该是回调最早的应用之处,也是其得名如此的原因。回调机制的优势从上面的例子可以看出,回调机制提供了非常大的灵活性。请注意,从现在开始,我们把图中的库函数改称为中间函数了,这是因为回调并不仅仅用在应用和库之间。任何时候,只要想获得类似于上面情况的灵活性,都可以利用回调。

    这种灵活性是怎么实现的呢?乍看起来,回调似乎只是函数间的调用,但仔细一琢磨,可以发现两者之间的一个关键的不同:在回调中,我们利用某种方式,把回调函数像参数一样传入中间函数。可以这么理解,在传入一个回调函数之前,中间函数是不完整的。换句话说,程序可以在运行时,通过登记不同的回调函数,来决定、改变中间函数的行为。这就比简单的函数调用要灵活太多了。

        中间函数和回调函数是回调的两个必要部分,不过人们往往忽略了回调里的第三位要角,就是中间函数的调用者。绝大多数情况下,这个调用者可以和程序的主函数等同起来,但为了表示区别,我这里把它称为起始函数(如上面的代码中注释所示)。之所以特意强调这个第三方,是因为我在网上读相关文章时得到一种印象,很多人把它简单地理解为两个个体之间的来回调用。譬如,很多中文网页在解释“回调”(callback)时,都会提到这么一句话:“If you call me, I will call you back.”我没有查到这句英文的出处。我个人揣测,很多人把起始函数和回调函数看作为一体,大概有两个原因:第一,可能是“回调”这一名字的误导;第二,给中间函数传入什么样的回调函数,是在起始函数里决定的。实际上,回调并不是“你我”两方的互动,而是ABC的三方联动。有了这个清楚的概念,在自己的代码里实现回调时才不容易混淆出错。另外,回调实际上有两种:阻塞式回调和延迟式回调。两者的区别在于:阻塞式回调里,回调函数的调用一定发生在起始函数返回之前;而延迟式回调里,回调函数的调用有可能是在起始函数返回之后。这里不打算对这两个概率做更深入的讨论,之所以把它们提出来,也是为了说明强调起始函数的重要性。网上的很多文章,提到这两个概念时,只是笼统地说阻塞式回调发生在主调函数返回之前,却没有明确这个主调函数到底是起始函数还是中间函数,不免让人糊涂,所以这里特意说明一下。

二、同步、异步

        首先我们得明白 JavaScript 本身就是一门事件驱动(观察者模式)和单线程的语言,它的特性是跟 JavaScript 的诞生背景是相关的。JavaScript 一开始设计出来就是为了丰富 Web 的交互效果也就是DOM操作,进行简单的表单验证。同样,Node.js 的执行程序本就是单线程,因为同样也是用 JavaScript(当初设计 Node.js 时决定使用 JavaScript 也是看重它单线程语言的特点)。单个线程如果遇到耗时操作例如 I/O操作,网络访问时则会阻塞该线程后面的执行流程,因此为了不阻塞这唯一的执行线程,因此就利用了异步的方式。所谓异步的方式就是让耗时操作在其他线程中完成的,我们姑且称他们为工作线程。当某些耗时操作完成后就需要将执行的结果返回给唯一的主线程,这一步则是通过 callback 函数完成的,同时也体现了事件驱动的特点。因此,我们可以说异步编程最直接的表现就是回调,但 callback 函数跟异步/同步并没有直接的关系。

例1:

function heavyCompute(n, callback){
    let count = 0,
        i,
        j;
    for (i = n; i > 0;--i){
        for(j = n; j > 0;--j){
            count += 1;
        }
    }
    callback(count);
}

heavyCompute(10000, function(count){
    console.log(count);
});

console.log('hello');

例2:

function a(){
	console.log('执行a函数');
	setTimeout(function(){
		console.log('执行a函数的延迟函数');
    },0);
};
function b(){
	console.log('执行b函数');
};
a();
b();

        以上代码会先执行函数a,而且不会等到a中的延迟函数执行完才执行函数b, 在延迟函数被触发的过程中就执行了函数b,当js引擎的event 队列空闲时才会去执行队列里等待的setTimeout的回调函数,这就是一个异步的例子。调用 setTimeout 函数会在一个时间段过去后在队列中添加一个消息。这个时间段作为函数的第二个参数被传入。如果队列中没有其它消息,消息会被马上处理。但是,如果有其它消息,setTimeout 消息必须等待其它消息处理完。因此第二个参数仅仅表示最少的时间 而非确切的时间所以即使,时间设置为0,也是会照样先执行函数b。

来看几个经典的回调函数代码:

1、异步请求的回调函数

$.get('ajax/test.html', function(data){
	$('.result').html(data);
	alert('Load was performed.');
});

2、点击事件的回调函数

$("target").click(function(){
	alert("Handler for .click() called.");
});

3、数组中遍历每一项调用的回调函数

this.tabs.forEach(function(tab,index){
	if(tab.selected){
		this.focustab = this.tabs[index];
    }
}.bind(this));

4、同步回调

function getNodes(parms, callback){
    var list = JSON.stringify(parms);
	typeof(callback) === 'function' && callback(list);
}
getNodes('[1,2,3]',function(nodes){
    //拿到nodes之后用它去做一些其他操作
});

    所以回调与同步、异步并没有直接的联系,回调只是一种实现方式,既可以有同步回调,也可以有异步回调,还可以有事件处理回调和延迟函数回调,这些在我们工作中有很多的使用场景,callback只是一个形参名。


js的单线程浏览器内核的多线程

    浏览器常驻三大线程:  js引擎线程,GUI渲染线程,浏览器事件触发线程

    看到此图你是不是会豁然开朗许多,因为浏览器是一个多线程的执行环境,在浏览器的内核中分配了多个线程,最主要的线程之一即是js引擎的线程,同时js事件队列中的异步请求,交互事件触发,定时器等事件都是由浏览器的事件触发线程进行监听的,浏览器的事件触发线程被触发后会把任务加入到js 引擎的任务队列中,当js 引擎空闲时候就会开始执行该任务。

三、Promise

    Node是以异步(Async)回调著称的,其异步性提高了程序的执行效率,但同时也减少了程序的可读性。如果我们有几个异步操作,并且后一个操作需要前一个操作返回的数据才能执行,这样按照Node的一般执行规律,要实现有序的异步操作,通常是一层加一层嵌套下去。为了解决这个问题,ES6提出了Promise的实现。

    Promise 对象用于一个异步操作的最终完成(或失败)及其结果值的表示。简单点说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。

new Promise(
    /* executor */
    function(resolve, reject) {
        if (/* success */) {
            // ...执行代码
            resolve();
        } else { /* fail */
            // ...执行代码
            reject();
        }
    }
);
    其中,Promise中的参数executor是一个执行器函数,它有两个参数resolve和reject。它内部通常有一些异步操作,如果异步操作成功,则可以调用resolve()来将该实例的状态置为fulfilled,即已完成的,如果一旦失败,可以调用reject()来将该实例的状态置为rejected,即失败的。
        我们可以把Promise对象看成是一条工厂的流水线,对于流水线来说,从它的工作职能上看,它只有三种状态,一个是初始状态(刚开机的时候),一个是加工产品成功,一个是加工产品失败(出现了某些故障)。同样对于Promise对象来说,它也有三种状态:
        pending初始状态,也称为未定状态,就是初始化Promise时,调用executor执行器函数后的状态。
        fulfilled完成状态,意味着异步操作成功。
        rejected失败状态,意味着异步操作失败。
    它只有两种状态可以转化,即
        操作成功pending -> fulfilled
        操作失败pending -> rejected

    并且这个状态转化是单向的,不可逆转,已经确定的状态(fulfilled/rejected)无法转回初始状态(pending)。

问题1:

const p = function(){
    let num = Math.random();
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            num > 0.5 ? resolve(num) : reject(num);
        }, 1000);
    })
};


p().then(val => {
    console.info(`Status switches to fulfilled, and the value is ${val}`);
}, val => {
    console.info(`Status switches to reject, and the value is ${val}`);
})

问题2:

const p = function(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('Refused the request!');
        },0);
    })
};

const p2 = function(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(p())
        },0);
    })
};

p2().then(val => {
    console.info('Status switches to fulfilled');
    console.info(val);
}, val => {
    console.info('Status switches to reject');
    console.info(val);
});
上面的例子中:当Promise实例内部的fulfilled(或reject)传入的是Promise实例时,其状态以及then()方法的传值将由传入的Promise实例的状态决定。

参考:https://juejin.im/post/58f71c720ce463006bcc464b

         https://juejin.im/entry/5aeec4a4518825672033f5d4

         https://www.zhihu.com/question/19801131/answer/27459821

阅读更多
个人分类: 前端
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭