异步问题

回调地狱

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

  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的操作,如果我们希望能够并行操作的话,只能重写fetchData1var 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

优化:更优雅的异步代码?相关推荐

  1. python写出的程序如何给别人使用-涨姿势!这些小技巧让小白也可以写出更优雅的Python代码!...

    原标题:涨姿势!这些小技巧让小白也可以写出更优雅的Python代码! 一.前言 我前两天回答了两个Python相关的问题,收到了很多赞,从答案被收藏的情况来看,确实对不少人都很有帮助,所以我也很开心. ...

  2. 前端通信:ajax设计方案(五)--- 集成promise规范,更优雅的书写代码(改迭代已作废,移步迭代10)...

    该迭代已作废,最新的请移步这里:https://www.cnblogs.com/GerryOfZhong/p/10726306.html 距离上一篇博客书写,又过去了大概几个月了,这段时间暂时离开了这 ...

  3. 6种更优雅书写Python代码!

    1 简介 一些比较熟悉pandas的读者朋友应该经常会使用query().eval().pipe().assign()等pandas的常用方法,书写可读性很高的「链式」数据分析处理代码,从而更加丝滑流 ...

  4. pdb断点命令_Python使用pdb更优雅的调试代码

    什么是pdb 不知道大家在用Python写代码出现报错时是怎样调试的,从报错提示定位回去一步一步check每一行?如果没有IDE或者命令行写代码时又该怎样快速调试?这时如果使用pdb进行调试将会异常方 ...

  5. 如何让你的代码变得更优雅?这些代码规范和技巧必须知道(进阶必备,建议收藏)

    如何做一名优秀的程序猿?从改善每一行代码开始 1. 看你的代码就知道你几斤几两 2. 编程标准 3. 命名规范 4. 松耦合,高复用 5. 函数优化 6.条件优化 7. 循环优化 8. 如何提升js性 ...

  6. 用Async函数简化异步代码

    Promise 在 JavaScript 上发布之初就在互联网上流行了起来 - 它们帮开发人员摆脱了回调地狱,解决了在很多地方困扰 JavaScript 开发者的异步问题.但 Promises 也远非 ...

  7. 更效率、更优雅 | 阿里巴巴开发者工具不完全盘点

    简介: 阿里巴巴将自身在各类业务场景下的技术积淀,通过开源.云上实现或工具等形式对外开放,本文将精选了一些阿里巴巴的开发者工具,希望能帮助开发者们提高开发效率.更优雅的写代码. 从人工到自动化,从重复 ...

  8. 用 Async 函数简化异步代码

    Promise 在 JavaScript 上发布之初就在互联网上流行了起来 - 它们帮开发人员摆脱了回调地狱,解决了在很多地方困扰 JavaScript 开发者的异步问题.但 Promises 也远非 ...

  9. 使用这45个小技巧,帮助你写出更优雅的代码

    前言 不知道大家有没有经历过维护一个已经离职的人的代码的痛苦,一个方法写老长,还有很多的if else ,根本无法阅读,更不知道代码背后的含义,最重要的是没有人可以问,此时只能心里默默地问候这个留坑的 ...

最新文章

  1. JAVA Web项目中所出现错误及解决方式合集(不断更新中)
  2. spring中事件发布设计与实现
  3. 关于Django的时区设置TIME_ZONE,USE_TZ的问题
  4. QT 使用全局变量的方法
  5. 超音速亚原子Enterprise Java
  6. 商城商品购买数量增减的完美JS效果
  7. 作者:张丽丽(1984-),女,中国科学院计算机网络中心助理研究员
  8. Traffic Manager Overview
  9. 基于SSLStrip的HTTPS会话劫持
  10. 多种方法破解Windows 系统密码
  11. vue仿163musicPC端
  12. 2021春招Java面试题大全(精华六)
  13. 如何寻找基因的启动子——NCBI版
  14. DFT的简单介绍(下)
  15. 被final修饰的变量到底能不能被修改
  16. Kubeadm部署高可用K8S集群
  17. javascript数组 去重
  18. Android使用lottie加载json动画只显示部分以及 You must set an images folder before loading an image.
  19. 怎么在mac下运行映像dmg_Mac制作dmg镜像重新安装系统方法
  20. MAC下载python

热门文章

  1. codeforces 706B B. Interesting drink(二分)
  2. java继承与初始化
  3. 为什么我们不要 .NET 程序员
  4. 从Uboot到Linux技术
  5. 9.11 strace:跟踪进程的系统调用 、ltrace:跟踪进程调用库函数
  6. 链表的基本操作 java_Java-实现链表的基本操作
  7. 脚本升级_手把手教你升级到Database 19c(3)| 终章教程
  8. python 数组赋值_LeetCode基础算法题第182篇:一维数组的运行总和
  9. python编写界面遍历_python和pywin32实现窗口查找、遍历和点击
  10. mysql性能调优与架构设计 51cto_MySQL 数据库性能优化之表结构优化