优化:更优雅的异步代码?

异步问题

回调地狱

异步编程中最常见的一种问题便是回调地狱。

  1. 单次ajax请求有多个回调响应
$.ajax({
    type: ‘get‘,
    url: ‘/path/to/calldata‘,
    success: function (response) {
        // todo

        sucCallback2(response);
        sucCallback3(response);
    }
})

我们产生多个success状态下的回调函数,或者多个ajax请求同时发送,全部success状态后执行回调。 如果需要在successCallback2完成后继续回调,就要一层一层的嵌套。代码不是纵向发展,而是横向发展,这就是js中的回调地狱。

回调地狱的出现是由于异步代码执行时间的不确定性及代码间的依赖关系引发的

又如:

// 一个动画结束后,执行下一个动画,下一个动画结束后再执行下一个动画
$(‘#box‘).animate({width: ‘100px‘}, 1000, function(){
    $(‘#box‘).animate({height: ‘100px‘}, 1000, function(){
        $(‘#box‘).animate({left: 100}, 1000);
    });
});

把第二个动画的执行内容放到了第一个动画的结束事件里,把第三个动画放到了第二个动画的结束事件里,这时候如果有很多这样的动画,那么就会形成回调地狱。

  1. 多个ajax请求希望有一个共同的回调响应
// 假设有多个ajax请求,希望在全部完成后执行回调函数。

function fetchData (url, sucCallBack, errCallBack) {
    return function () {
        $.ajax({
            type: ‘get‘,
            url: url,
            success: sucCallBack,
            error: errCallBack
        });
    }
}

function sucCallBack () {
    console.log(‘success‘);
}

function errCallBack () {
    console.log(‘error‘);
}

var fetchData1 = fetchData(‘/path/to/calldata1‘, sucCallBack, errCallBack);
var fetchData2 = fetchData(‘/path/to/calldata2‘, sucCallBack, errCallBack);

如果有两个相同的fetch data的操作,如果我们希望能够并行操作的话,只能重写fetchData1

var fetchData1 = fetchData(‘/path/to/data1‘, fetchData2, errorCb);

fetchData1();

在fetchData1成功后进行fetchData2操作并不是严格意义上的并行操作,之后在fetchData2的success状态的回调中,我们可以获得两次ajax请求的返回值。

延时对象

使用jquery 1.5版本之后的代码,可以用下面的方法进行一次ajax请求。

// 引入jquery
var fetchData = function (url) {
    return $.ajax({
        type: ‘get‘,
        url: url
    });
}

这样一次请求的内容就已经完成,$.ajax返回一个$.Deferred对象,那么我们就可以使用$.Deferred对象的api进行一些异步操作。

对于每一个$.Deferred对象来说,实例有多个方法,其中done方法代表异步完成时执行的方法,fail代表异步失败时执行的方法,这两个方法同时仍旧返回一个$.Deferred对象的实例。

继续上面的ajax操作,我们可以这样写成功和失败的回调:

// fetchData 接上

fetchData()        //执行函数返回一个Deferred对象实例
    .done()        //接受一个函数,ajax请求成功调用
    .fail()        //接受一个函数,ajax请求失败调用
    .done()        //第二个成功状态的回调方法
    .fail()        //ajax请求失败调用

同样的对于.then方法,接受两个函数,第一个代表成功时执行的方法,第二个代表失败时的执行方法。同样的,它也返回一个deferred对象实例。意味着也能进行连缀调用。

fetchData()
    .then(successFn, errorFn)        //第一次回调
    .then(successFn, errorFn)        //第二次回调
内部实现上,.done 和 .fail 都是基于 .then实现的

fetchData()                            fetchData()
    .done(successFn)    <===>            .then(successFn, null)
    .fail(errorFn)      <===>            .then(null, errorFn)

对于多个ajax同时请求,共同执行同一个回调函数这一点上,jquery有一个$.when方法,接受多个Deferred对象实例,同时执行。

var fetchData = function (url) {
    return $.ajax({
        type: ‘get‘,
        url: url
    });
}

var fetchData1 = fetchData(‘/path/to/calldata1‘);
var fetchData2 = fetchData(‘/path/to/calldata2‘);

$.when(fetchData1, fetchData2, function (data1, data2) {
    // fetchData1 响应为data1
    // fetchData2 响应为data2
})

完美。

优雅的异步代码

那么我们如何优雅的写好我们的异步代码呢?我主要列了以下5种常见方案:

1. callback

callback顾名思义便是回调,但并不是将回调内容放在异步方法里,而是放到外部的回调函数中,比如问题1的代码我们通过callback可以变成这样:

$(‘#box‘).animate({width: ‘100px‘}, 1000, autoHeight);

function autoHeight() {
    $(‘#box‘).animate({height: ‘100px‘}, 1000, autoLeft);
}

function autoLeft() {
    $(‘#box‘).animate({left: 100}, 1000);
}

如此我们看似异步的代码变成了同步的写法,避免了层层嵌套的写法,看上去也流畅了很多。同时使用callback也是异步编程最基础和核心的一种解决思路。

2. Promise

基于callback,Promise目前也被广泛运用,其是异步编程的一种解决方案,比传统的回调函数解决方案更合理和强大。相信了解ES6的同学肯定不会陌生。

比如我们现在有这样一个场景,我们需要异步加载一张图片,在图片加载成功后做一些操作,这里我不想用回调函数或者将逻辑写在图片的成功事件里,那么用Promise我们可以这样写:

let p = new Promise((resolve, reject) => {
    let img = new Image(); // 创建图片对象

    // 图片加载成功事件
    img.onload = function() {
        resolve(img); // 输出图片对象
    };

    // 图片加载失败事件
    img.onerror = function() {
        reject(new Error(‘load error‘)); // 输出错误
    };

    img.src = ‘xxx‘; // 图片路径
});

// Promise then回调
p
.then(result => {
    $(‘#box‘).append(result); // 成功后我们把图片放到页面上
})
.catch(error => {
    console.log(error); // 打印错误
})

通过Promise我们把图片构建加载的逻辑和成功或失败后的处理逻辑拆分了开来,将回调函数的嵌套,改成链式调用,同时使用Promise的catch事件回调后异常捕获也变得十分方便。

当然如果要等待多个异步请求完成执行某些操作,可以使用Promise.all方法,如:

let p = Promise.all([p1, p2, p3]); // 其中p1、p2、p3都是Promise实例

p.then(result => console.log(result));

当然Promise也有其相应的缺点,比如下一个then回调只能获取上一个then返回的数据,不能跨层获取,同时大量的then回调也会使代码不容易维护。

3. Generator

与Promise一样,Generator 函数也是 ES6 提供的一种异步编程解决方案,其会返回一个遍历器对象,异步任务需要暂停的地方我们可以使用yield语句,比如:

function* getData() {
    let result = yield fetch("xxx"); // 调用ajax,yield命令后面只能是 Thunk 函数或 Promise 对象

    console.log(result);
}

// 执行
let g = getData();
let result = g.next(); // { value: [object Promise], done: false }

result.value.then(data => {
    return data.json();
}).then(data => {
    g.next(data);
});

Generator中遇到yield的地方会进行暂停,所以我们需要手动调用next方法往下,next返回值的 value 属性便是我们需要的数据,这里是fetch方法返回的Promise对象,所以我们需要使用then回调,最后再调用g.next(data)结束并输出数据。

Generator 函数的缺点在于,我们每一次执行yield语句都需要手动进行next,不是很方便。

4. co

为了解决上方Generator函数需手动执行next方法的问题,TJ Holowaychuk大神编写了一个co函数库,能够使Generator 函数可以自动执行,比如原来我们需要这样:

let files = function* (){
    var f1 = yield readFile(‘/xxx/xxx‘); // 读取file1文件
    var f2 = yield readFile(‘/xxx/xxx‘); // 读取file2文件

    console.log(f1.toString());
    console.log(f2.toString());
};

files.next(); // 执行yield
files.next(); // 执行yield
使用co后:

var co = require(‘co‘);

co(files);
co 函数返回一个 Promise 对象,因此可以用 then 方法添加回调函数。

co(files).then(() => {
  console.log(‘执行完成‘);
});

最后我们可以看到我们没有手动执行next方法,也会打印出所读取的文件。

co模块虽然很好的帮助了我们解决了Generator函数必须靠执行器的问题,但是使用起来我们都需要额外引入一个模块,那么有没有更加方便的方式来解决呢?继续往下看。

5. async and await

async是 Generator 函数的语法糖,不同点在于其内置了执行器,也就是说async函数自带执行器。看一下下面的例子:

let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 1000);
});

let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(2);
    }, 1000);
});

async function waitFn() {
    let a = await p1; // await命令后面可以是 Promise 对象和原始类型的值,如果使原始类型最终也会返回为Promise对象
    let b = await p2;

    return a + b
}

// async函数的返回值是 Promise 对象, 可以用then方法指定下一步的操作
waitFn().then(result => {
    console.log(result);  // 2s后输出3
});

async函数内部return语句返回的值,会成为then方法回调函数的参数。因此这就像极了利用co包裹起来的Generator函数,只是把*替换成了async,把yield替换成了await。

可以说async and await 是ES7中最重要的一个特性,虽然其也存在一些弊端,但是相比较而言用其处理异步代码来说还是比较得心应手的。

deferred对象

使用的ajax方法操作ajax请求,会受到回调函数嵌套的问题。当然,jquery团队也发现了这个问题,在jquery 1.5版本之后,jQuery.Deferred对象为解决这类问题应运而出。之后,zapto等框架也推出相同api的deferred对象,来进行异步操作。

上面的$.ajax只是在$.deferred对象上封装了一层ajax操作。实际上,真正的$.Deferred对象是这样调用的:

function printA () {
    var deferred = new $.Deferred();
    setTimeout(function () {
        console.log(‘A‘);
        deferred.resolve(‘ is done.‘);
    }, 1000);
    return deferred;
}

function printB (msg) {
    var deferred = new $.Deferred();
    setTimeout(function () {
        console.log(‘B‘ + msg);
        deferred.resolve();
    }, 1000);
    return deferred;
}

printA()
    .then(printA)
    .then(printB)

每个函数维护一个Deferred对象,在每一个具有异步操作的函数执行成功后,指示全局deferred对象执行下一个函数,达到异步的效果。

新建完成$.Deferred实例deferred之后,调用deferred.resolve()代表成功完成响应,deferred.reject()即代表调用失败响应。

这里我们主要写一下这种调用方式实现的tiny版。

首先我们写一个Callback对象维护我们的回调函数队列

var Callbacks = function () {
    function Callbacks () {
        this.callbacks = [];
    }
    Callbacks.prototype.add = function (fn) {
        this.callbacks.add(fn);
        return this;
    }
    Callbacks.prototype.fire = function () {
        var len = this.callbacks.length;
        if(len) {
            this.callbacks.unshift()();
        }
    }
    return Callbacks;
}

这段代码逻辑很简单,Callbacks对象有两个方法,分别是add和fire,调用add则向当前的callbacks数组内新增一个function。fire方法,则从Callbacks中提取最前的一个callback,并执行它。

对于Deferred对象,我们至少需要resolve和reject两个方法。进行成功和失败的调用。并且能够进行链式调用。

var Deferred = function () {
    this.successCbs = new Callbacks();
    this.errorCbs = new Callbacks();
}
Deferred.prototype.then = function (successCb, errorCb) {
    this.successCbs.add(successCb);
    this.errorCbs.add(errorCb);
    return this;
}
Deferred.prototype.resolve = function () {
    this.successCbs.fire();
    return this;
}
Deferred.prototype.reject = function () {
    this.errorCbs.fire();
    return this;
}

这样简单完成之后,我们新建一个Deferred实例,就能够通过链式调用的方式进行异步操作。

var deferred = new Deferred();
function printA() {
    setTimeout(function () {
        console.log(‘A‘);
        deferred.resolve();
    }, 1000);
    return deferred;
}
function printB() {
    setTimeout(function () {
        console.log(‘B‘);
        deferred.resolve();
    }, 1000);
}

printA()
    .then(printB)
    .then(printA)

同样的,我们可以封装一个自制tiny-Deferred对象的tiny-ajax方法。

var ajax = function (options) {
    var xhrOptions = {
        type: options.type || ‘get‘,
        url: options.url || ‘/default/path‘,
        async: options.async || true
    };
    var deferred = new Deferred();
    var xhr = new XHRHttpRequest();
    xhr.open(xhrOptions.type, xhrOptions.url, xhrOptions.async);
    xhr.onload = function (result) {
        deferred.resolve(result);
    }
    xhr.onerror = function ()
    xhr.send();
    return deferred;
}

over

原文地址:https://www.cnblogs.com/yizhiamumu/p/9168849.html

时间: 2024-08-04 12:45:18

优化:更优雅的异步代码?的相关文章

使用 Promises 编写更优雅的 JavaScript 代码

你可能已经无意中听说过 Promises,很多人都在讨论它,使用它,但你不知道为什么它们如此特别.难道你不能使用回调么?有什么了特别的?在本文中,我们一起来看看 Promises 是什么以及如何使用它们写出更优雅的 JavaScript 代码. 您可能感兴趣的相关文章 开发中可能会用到的几个 jQuery 提示和技巧 精心挑选的优秀jQuery Ajax分页插件和教程 推荐几款很好用的 JavaScript 文件上传插件 精心挑选的优秀 jQuery 文本特效插件和教程 精心挑选12款优秀 jQ

框架基础:ajax设计方案(五)--- 集成promise规范,更优雅的书写代码

距离上一篇博客书写,又过去了大概几个月了,这段时间暂时离开了这个行业,让大脑休息一下.一个人旅行,一个人休息,正好也去完成一个目标 --- 拥有自己的驾照.当然,也把自己晒的黑漆马虎的.不过这一段时间虽然在技术上没有学太多东西,但是在心态上给了自己一个沉淀的机会,感觉自己变得更加沉稳和成熟,感觉这就是自己需要找到的自己,回归自我.好了,废话不多说了,虽然技术上没有学一些新的东西,但是欠的东西还是要补回来的.正如这篇博客,前端Promise规范的实现与ajax技术的集成,当时github上一个用户

TypeScript 异步代码类型技巧

在typescript下编写异步代码,会遇到难以自动识别异步返回值类型的情况,本文介绍一些技巧,以辅助编写更健全的异步代码. callback 以读取文件为例: readFile是一个异步函数,包含path和callback两个参数,callback的不进行声明类型的情况下,调用readFile后传入的callback无法正确识别到callback的err和rst的类型.通常在这种情况下,使用者很容易出现错用参数的情况,例如把rst当成一个字符串使用等.有了上面的类型描述后,下面的两种调用就能让

少年,是时候换种更优雅的方式部署你的php代码了

让我们来回忆下上次你是怎么发布你的代码的: 1. 先把线上的代码用ftp备份下来 2. 上传修改了的文件 3. 测试一下功能是否正常 4. 网站500了,赶紧用备份替换回去 5. 替换错了/替换漏了 6. 一台服务器发布成功 7. 登录每一台执行一遍发布操作 8. 加班搞定 9. 老板发飙 ... 尤其现在的互联网行业,讲究快速迭代,小步快跑.像bug修复或者小功能的修改几乎每天都发版本,大功能的版本迭代每周也差不多会有一次.相信不少同行们像我上面说的这样发布自己的代码吧.或者可能先进一点,直接

CSS 黑魔法小技巧,让你少写不必要的JS,代码更优雅

首页 登录注册 CSS 黑魔法小技巧,让你少写不必要的JS,代码更优雅 阅读 8113 收藏 927 2017-09-26 原文链接:github.com 腾讯云容器服务CSS,立即免费体验容器集群吧!cloud.tencent.com 之前不久,由于自己平时涉猎还算广泛,总结了一篇博客:这些JavaScript编程黑科技,装逼指南,高逼格代码,让你惊叹不已,没想到受到了大家的欢迎,有人希望能博主还能整理个 CSS 的一些黑魔法小技巧,无奈我 CSS 一直很渣,没什么干货,最近写了一个 Chro

怎么让你的Python代码更优雅!

3 个可以使你的 Python 代码更优雅.可读.直观和易于维护的工具. Python 提供了一组独特的工具和语言特性来使你的代码更加优雅.可读和直观.为正确的问题选择合适的工具,你的代码将更易于维护.在本文中,我们将研究其中的三个工具:魔术方法.迭代器和生成器,以及方法魔术. 加vx:tanzhouyiwan 免费领取Python学习资料 魔术方法 魔术方法可以看作是 Python 的管道.它们被称为"底层"方法,用于某些内置的方法.符号和操作.你可能熟悉的常见魔术方法是 __ini

【重构.改善既有代码的设计】11、处理概括关系【更优雅的继承】

11.处理概括关系[更优雅的继承] Pull Up Field(值域上移) 两个subclasses 拥有相同的值域.将此一值域移至superclass. Pull Up Method(函数上移) 有些函数,在各个subclass 中产生完全相同的结果.将该函数移至superclass. Pull Up Constructor Body(构造函数本体上移) 你在各个subclass 中拥有一些构造函数,它们的本体(代码)几乎完全一致. 在superclass 中新建一个构造函数,并在subcla

【原创】基于.NET的轻量级高性能 ORM - TZM.XFramework 之让代码更优雅

[前言] 大家好,我是TANZAME.出乎意料的,我们在立冬的前一天又见面了,天气慢慢转凉,朋友们注意添衣保暖,愉快撸码.距离 TZM.XFramework 的首秀已数月有余,期间收到不少朋友的鼓励.建议和反馈,在此致以深深的感谢. 不少围观的朋友经常问题我,.NET 体系下优秀的 O/RM 官方的有EF,第三方的有linq2db (国外).StackExchange/Dapper (国外).NHibernate (国外).PetaPoco (国外).Freesql (国内)等等,What's

如何写出优雅的CSS代码 ?(转)

对于同样的项目或者是一个网页,尽管最终每个前端开发工程师都可以实现相同的效果,但是他们所写的代码一定是不同的.有的优雅,看起来清晰易懂,代码具有可拓展性,这样的代码有利于团队合作和后期的维护:而有的混乱,虽然表达出了最终的效果,然而却晦涩难懂,显然团队成员在读这样的代码时就显得无从下手,更不利于后期的维护了.那么如何写出优雅的代码呢?下面我将以一个很小的项目就以下几个方面简单的表达一下自己的看法,如有不妥,望批评指正. 如何整理一个项目. 如何写出清晰易懂的HTML代码. 如何写出优雅的css代