为了账号安全,请及时绑定邮箱和手机立即绑定

JavaScript异步编程之回调函数

基本概念

当一个函数满足以下两点时,就可以称之为回调函数(callback )。

1、这个函数作为参数传递给另外一个函数。
2、这个函数会在未来某个时间点(条件得到满足时或执行流到达时)被调用。

例子:

(function(fn) {
    let n = Math.floor(Math.random() * 10 + 1);
    let _fn = fn();
    if (n < 5) {
        _fn.done();
    } else {
        _fn.fail();
    }
})(callback);

function callback() {
    return {
        done() {
            console.log("小于5");
        },
        fail() {
            console.log("大于5");
        }
    }
}

回调函数的异步模式

回调函数本身并没有同步和异步之分,同步或异步主要取决于发布回调函数的任务源是什么。例如事件、Ajax(XMLHttpRequest)、setTimeout / setInterval 和 Promise 本身就是异步任务源,因此回调函数也被异步调用。如果回调函数是放在一段程序的最后去执行,那是利用了JavaScript
本身的单线程缘故,属于同步任务。

例子:

同步执行回调函数

console.log(1);

(function(callback) {
    console.log(2);
    callback();
    console.log(3);
})(fn);

console.log(4);

function fn() {
    let arr = Array(1000000);
    arr.fill(1);
    console.log(arr.reduce((a, b) => a + b));
}
//输出: 1 2 1000000 3 4

异步执行回调函数

console.log(1);

(function(callback) {
    console.log(2);
    setTimeout(() => {
        callback();
    }, 0);
    console.log(3);
})(fn);

console.log(4);

function fn() {
    let arr = Array(1000000);
    arr.fill(1);
    console.log(arr.reduce((a, b) => a + b));
}
//输出: 1 2 3 4 1000000 

回调函数的意义

回调函数在形式上显然属于高阶函数的范畴,高阶函数的特点之一就是分解单个函数的复杂度,增强代码的可复用性。为了达到这个目的,我们往往需要抽离出那些容易变化的业务逻辑,把这部分业务逻辑放在回调函数中,这样一来可以分离业务代码中变化与不变的部分,增强了原函数的可复用性。
例如,我们想在页面上创建一组有序(顺序或倒叙)的数字按钮,其中按钮是使用什么规则去排序的,属于可变的部分,而创建按钮则是不变的部分,我们可以把可变的部分封装在函数参数中。

例子:

//不变的部分
function each(ary, callback) {
    let frag = document.createDocumentFragment();
    callback(ary).forEach(v => {
        let node = document.createElement("button");
        node.innerHTML = v;
        frag.appendChild(node);
    });
    document.body.appendChild(frag);
}

//变化的部分:从小到大排列
function sortEach(data) {
    let len = data.length;
    while (--len) {
        for (let i = 0; i < len; i++) {
            if (data[i] > data[i + 1]) {
                [data[i], data[i + 1]] = [data[i + 1], data[i]];
            }
        }
    }
    return data;
}

//变化的部分:从大到小排列
function reverseEach(data) {
    let len = data.length;
    while (--len) {
        for (let i = 0; i < len; i++) {
            if (data[i] < data[i + 1]) {
                [data[i], data[i + 1]] = [data[i + 1], data[i]];
            }
        }
    }
    return data;
}

//each([2, 3, 1, 5, 4], sortEach);
each([2, 3, 1, 5, 4], reverseEach);
//输出: 5 4 3 2 1

回调地狱

首先,考虑下面这个经典的“回调地狱”代码:

addEventListener("click", function handler() {
    setTimeout(function request() {
        ajax("http://some.url", function response(data) {
            if (data == "hello") {
                handler();
            } else if (data == "world") {
                request();
            }
        });
    }, 500);
});

一眼看去,我们似乎可以很快梳理出这样的异步执行顺序:等待 click 事件,然后等待定时器启动,然后等待 Ajax 响应返回。但是,这样的执行顺序,只有在最理想的状态下才会发生,并没有考虑可能导致执行顺序偏离的异常情况,例如,“定时器启动”这一步骤执行失败,那么就永远不会到达“ Ajax 响应”这一步,我们不得不转到“定时器启动”失败时的错误处理流程中。这只是我们列举的一种情况,而实际开发中,我们需要为每一个异步事件指定所有的可能性和路径,那么代码就会变得非常复杂,追踪异步流的难度会成倍增加。这就是回调地狱。
从根本上讲,回调地狱的出现,是因为我们丧失了自己程序一部分的控制权,这种“控制转移”导致我们不得不“核实”任何可能出现的情况。


如有错误,欢迎指正,本人不胜感激。

点击查看更多内容
2人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消