h-blog

异步处理方案系列- 1.callback

In computer programming, a callback is a reference to a piece of executable code that is passed as an argument to other code. –维基百科

引言

异步/异步操作,已经是前端领域一个老生常谈的话题.也是做前端开发中经常面临的一个问题.

然而异步的问题往往比较复杂且难于处理, 特别是异步问题还经常不是单独出现,往往存在比较多样的组合关系.

在实际处理中就显得更加复杂而难于处理. 特别是在 io 操作频繁,或者 node server 中,经常遇到非常复杂的组合型异步。

举个业务开发中常见的例子:

eg: 省市县三级级联问题

这个问题非常常见, 假设数据量较大, 我们大多数情况下不会一次加载所有的数据, 然后做前端级联的方案.

而是采取三个数据接口,在下拉改变的时候去动态请求的方式.这就形成一种很常见的多个异步串行的模型.

怎么处理这样的问题, 怎么较好的维护多个异步之间的关系, 怎么让代码正常执行的同时,在逻辑和结构上更可读呢?

我将会梳理

这几种处理方式. 加上两种模式

列出一个系列的博客去讨论这个问题. 看我们在不同阶段, 使用不同技术,如何处理相同的问题. 在不同方案之间横向对比, 去深入了解 技术变迁以及背后的处理思路和逻辑的变化.

callback

回调理解

什么是回调呢? 这么问似乎有点多余, 每个写过 javascript 的开发者, 或多或少都会接触到回调. 回调的使用成本很低, 实现回调函数就像传递一般的参数变量一样简单.由于函数式编程极好的支持,以至于这项技术使用基本没有障碍.我们随手就能写出一个回调

Ajax.get('http://xxxx', {}, (resp) => {
    // .....
})

但是呢,要真给回调下一个定义, 也还真不好回答.

我们不妨从一些侧面去看看回调

抽象层面

抽象一些的描述呢,大致就是: 回调函数来自一种著名的编程范式——函数式编程,函数式编程最主要的技术之一就是回调函数.

回调函数的本质是一种模式(一种解决常见问题的模式),因此回调函数也被称为回调模式。

回调中的 this

通常回调中出现 this 则需要格外小心,这可能经常导致难以预测的问题。我们一般的处理思路:

callback 业务模型

说这么多, 我们不如从代码的角度去解决一个串行的异步模型.

为了说明问题, 我们将问题简化成 A B C 三个异步(可能是 io, 网络请求, 或者其他.为了方面描述, 我们采用 settimeout 来模拟), 这三个异步耗时不确定, 但是必须按照 A B C 的顺序处理他们的返回结果.

处理这个问题, 我们基本上有两种思路:

  1. 控制异步发出的顺序, 在 a 返回之后再发 b 请求, 这样将问题串行化(省市县模型中经常需要省的返回值去请求省所对应的市).
  2. 同时发出异步请求,控制处理的顺序.

方案一: 串行化请求

// 模拟 ajax 函数
function ajax(url) {
    return function (cb) {
        setTimeout(function() {
            cb({
                url
            });
        }, Math.random() * 3000);
    }
}

// 初始化出三个请求
const A = ajax('/ofo/a');
const B = ajax('/ofo/b');
const C = ajax('/ofo/c');

// 控制请求顺序
log('ajax A send...');
A(function (a) {
    log('ajax A receive...');

    log('ajax B send...');
    B(function (b) {
        log('ajax B receive...');

        log('ajax C send...');
        C(function (C) {
            log('ajax C receive...');
        });
    })
})

代码很简单, 大多是方案也是这么走的, 因为 A 的返回值可以作为 B 的参数. 但是相应的这个模式的总时间必定大于三个请求的时间之和.输出如下:

ajax A send...
ajax A receive...
ajax B send...
ajax B receive...
ajax C send...
ajax C receive...

方案二: 自由请求,串行化处理

是相对不那么通用的方案, 但是处理没有直接数据依赖的串行请求非常合适.

// 发送容器
const sender = [];
// 稍作改造
function ajax(url, time) {
    return function(cb) {
        // 记录发送顺序, 必须有序
        sender.push(url);
        setTimeout(function() {
            const data = {
                from: url,
                reso: 'ok'
            };

            // 将 data, 回调传递给一个处理函数
            dealReceive({url, cb, data});
        }, time);
    }
}


// 按照顺序处理返回结果

// 返回结果容器
const receiver = {};
function dealReceive({url, cb, data}) {
    // 记录返回结果.可以无序
    receiver[url] = {cb, data};
    for (var i = 0; i < sender.length; i++) {
        let operate = receiver[sender[i]];
        if(typeof operate === 'object') {

            operate.cb.call(null, operate.data);
        } else {
            return;
        }
    }
}

// 手动模拟出请求时间, A 最耗时.b 最快, 更好说明问题
const A = ajax('/ofo/a', 4000);
const B = ajax('/ofo/b', 600);
const C = ajax('/ofo/c', 2000);


// 注意我们的调用方式 是没有任何控制的
// A,B,C 依次发出. 还可以按照这个顺序处理 A,B,C 的返回值
A(function (a) {
    log(a);
});

B(function (b) {
    log(b);
});

C(function (c) {
    log(c);
});

输出:

{"from":"/ofo/a","reso":"ok"}
{"from":"/ofo/b","reso":"ok"}
{"from":"/ofo/c","reso":"ok"}

这种方案总耗时基本上是耗时最长的 ajax 的耗时。

值得注意的是, A,B,C 的调用上没有做任何控制. A 最耗时, 但是要最最先处理 A 的返回数据. 实现这一点的关键就在于我们 dealReceive 有个轮询, 这个轮询不是定时触发的,而是每当请求回来时, 触发轮询. 整个过程轮询 3 次.

基本上 callback 处理组合异步模型的思路说完了.串行是容易处理的一种模型, 如果出现 c 依赖 a,b 都正确返回的模型时, 基本上我们暴力一点就是转化为串行关系. 尽管 a, b 没有关系. 或者呢我们就在 a, b 的回调里做标志位. 和 dealReceive 类似.

单个异步不需要有太多处理, callback 的一些细节也不做讨论. 主要讨论是回调在实际场景中的处理问题方案

作用

  1. 原始异步问题处理
  2. 避免重复代码 (DRY—Do Not Repeat Yourself)在通用功能的地方更好地实现抽象
  3. 拿到宿主(回调之行所在的环境)函数执行状态(钩子)
  4. 有更多定制的功能

回调两面性

我们还是落入俗套的分析一下回调的优缺点.其实主要是缺点.

使用回调上的建议: 没有使用障碍导致回调的滥用, 大部分问题都用了简单的回调堆叠来解决. 实际上我们有很多基于回调的模式可以避免这些问题.比如: cps, cps 进一步转化为 thunk. cps thunk.等等.

这样看来, 回调没有缺点, 是这样么? 不是的. 回调有非常致命的机制上的缺点, 这个问题可能在 node 中爆发,除非自身改变,或者被吃掉。

所谓的机制就是:你可能在用回调处理复杂问题的时候,对自己能力产生怀疑,这些异步之间的关系是那么难以梳理清晰,而又难以写出容易维护的代码.

其实这都不是你的错.

最终的结果就是: 你崩溃了

注:系列博客陆续推出,稍安勿躁。