JS异步处理的进化史深入讲解


Posted in Javascript onAugust 25, 2019

前言

javascript是一门单线程的语言,也就是说一次只能完成一件任务,如果有多个任务,就需要排队进行处理。如果一个任务耗时很长,后面的任务也必须排队等待,这样大大的影响了整个程序的执行。为了解决这个问题,javascript语言将任务分为两种模式:

  • 同步:当我们打开网站,网页的页面骨架渲染和页面元素渲染,就是一大推同步任务。
  • 异步:我们在浏览新闻时,加载图片或音乐之类占用资源大且耗时久的任务就是异步任务。

本文主要针对近两年javascript的发展,主要介绍异步处理的进化史。目前,在javascript异步处理中,有以下几种方式:

JS异步处理的进化史深入讲解

callback

回调函数是最早解决异步编程的方法。无论是常见的setTimeout还是ajax请求,都是采用回调的形式把事情在某一固定的时刻进行执行。

 //常见的:setTimeout
 setTimeout(function callback(){
  console.log('aa');
 }, 1000);
 //ajax请求
 ajax(url,function callback(){
  console.log("ajax success",res);
 })

回调函数的处理一般将函数callback作为参数传进函数,在合适的时候被调用执行。回调函数的优点就是简单、容易理解和实现,但有个致命的缺点,容易出现回调地狱(Callback hell),即多个回调函数嵌套使用。造成代码可读性差、可维护性差且只能在回调中处理异常。

ajax(url, () => {
	//todo
	ajax(url1, () => {
		//todo
		ajax(url2, () => {
			//todo
		})
	})
})

事件监听

事件监听采用的是事件驱动的模式。事件的执行不取决于代码的顺序,而是某个事件的发生。

假设有两个函数,为f1绑定一个事件(jQuery的写法),当f1函数发生success事件时,执行函数f2:

f1.on('success',f2);

对f1进行改写:

function f1(){
	ajax(url,() => {
		//todo
		f1.trigger('success');//触发success事件,从而执行f2函数
	})
}

事件监听的方式较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。

发布订阅

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做 发布/订阅模式(publish-subscribe pattern),又称**观察者模式"(observer pattern) **。

//利用jquery的插件实现
//首先,f2向消息中心订阅success事件
jQuery.subscribe('success',f2);
//对f1进行改写:
function f1(){
	ajax(url,() => {
		//todo
		jQuery.publish('success');//当f1执行完毕后,向消息中心jQuery发布success事件,从而执行f2函数
	})
}
//f2执行完毕后,可以取消订阅
jQuery.unsubscribe('success',f2)

该方法和事件监听的性质类似,但我们可以通过消息中心来查阅一共有多少个信号,每个信号有多少个订阅者。

Promise

**Promise**是CommonJS工作组提出的一种规范,可以获取异步操作的消息,也是异步处理中常用的一种解决方案。Promise的出现主要是用来解决回调地狱、支持多个并发的请求,获取并发请求的数据并且解决异步的问题。

let p = new Promise((resolve, reject) => {
 //做一些异步操作
 setTimeout(()=>{
  let num = parseInt(Math.random()*100);
  	if(num > 50){
   resolve("num > 50"); // 如果数字大于50就调用成功的函数,并且将状态变成Resolved
  	}else{
   	reject("num <50");// 否则就调用失败的函数,将状态变成Rejected
  }
	},10000)
});
p.then((res) => {
	console.log(res);
}).catch((err) =>{
 console.log(err);
})

Promise有三种状态:等待pending、成功fulfied、失败rejected;状态一旦改变,就不会再变化,在Promise对象创建后,会马上执行。等待状态可以变为fulfied状态并传递一个值给相应的状态处理方法,也可能变为失败状态rejected并传递失败信息。任一一种情况出现时,Promise对象的 then 方法就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,均为 Function。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法)。

需要注意的是: Promise.prototype.then 和  Promise.prototype.catch 方法返回promise 对象, 所以可以被链式调用,如下图:

JS异步处理的进化史深入讲解

Promise的方法:

  • Promise.all(iterable):谁执行得慢,以谁为准执行回调。返回一个promise对象,只有当iterable里面的所有promise对象成功后才会执行。一旦iterable里面有promise对象执行失败就触发该对象的失败。对象在触发成功后,会把一个包iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合。
  • Promise.race(iterable): 谁执行得快,以谁为准执行回调。iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。
  • Promise.reject(err)与Promise.resolve(res)

Generators/yield

Generators是ES6提供的异步解决方案,其最大的特点就是可以控制函数的执行。可以理解成一个内部封装了很多状态的状态机,也是一个遍历器对象生成函数。Generator 函数的特征:

  • function关键字与函数名之间有一个星号;
  • 函数体内部使用yield表达式,定义不同的内部状态;
    • 通过yield暂停函数,next启动函数,每次返回的是yield表达式结果。next可以接受参数,从而实现在函数运行的不同阶段,可以从外部向内部注入不同的值。next返回一个包含value和done的对象,其中value表示迭代的值,后者表示迭代是否完成。

举个例子:

function* createIterator(x) {
 let y = yield (x+1)
 let z = 2*(yield(y/3))
 return (x+y+z)
}
// generators可以像正常函数一样被调用,不同的是会返回一个 iterator
let iterator = createIterator(4);
console.log(iterator.next()); // {value:5,done:false}
console.log(iterator.next()); // {value:NaN,done:false}
console.log(iterator.next()); // {value:NaN,done:true}
let iterator1 = createIterator(4);//返回一个iterator
//next传参数
console.log(iterator1.next());  // {value:5,done:false}
console.log(iterator1.next(12)); // {value:4,done:false}
console.log(iterator1.next(15)); // {value:46,done:true}

代码分析:

  • 当不参数时,next的value返回NaN;
  • 当传参数时,作为上一个yeild的值,在第一次使用next时,传参数无效,只有第二次开始,才有效。
  • 第一次执行next时,函数会被暂停在yeild(x+1),所以返回的是4+1=5;
  • 第二次执行next时,传入的12为上一次yeild表达式的值,所以y=12,返回的是12/3=4;
  • 第三次执行next时,传入的15为上一次yeild表达式的值,所以z=30,y=12;x=4,返回30+12+4=46

async/await

初入async/await

async/await在ES7提出,是目前在javascript异步处理的终极解决方案。

async 其本质是 Generator 函数的语法糖。相较于Generator放入改进如下:

  • 内置执行器:Generator 函数的执行必须靠执行器,而async函数自带执行器。其调用方式与普通函数一模一样,不需要调next方法;
  • 更好的语义:async表示定义异步函数,而await表示后面的表达式需要等待,相较于*和yeild更语义化;
  • 更广的适用性:co模块约定,yield命令后面只能是Thunk函数或 Promise对象。而 async 函数的await命令后面则可以是Promise 或者 原始类型的值;
  • 返回Promise:async 函数返回值是Promise对象,比 Generator函数返回的 Iterator对象方便,可以直接使用 then() 方法进行链式调用;

语法分析

async语法

用来定义异步函数,自动将函数转换为promise对象,可以使用then来添加回调,其内部return的值作为then回调的参数。

async function f(){
	return "hello async";
}
f().then((res) => {  //通过then来添加回调且内部返回的res作为回调的参数
	console.log(res);  // hello async
})

在异步函数的内部可以使用await,其返回的promise对象必须等到内部所以await命令后的promise对象执行完,才会发生状态变化即执行then回调。

const delay = function(timeout){ 
	return new Promise(function(resolve){
		return setTimeout(resolve, timeout);
 });
}
async function f(){
  await delay(1000);
  await delay(2000);
  return '完成';
}
f().then(res => console.log(res));//需要等待3秒之后才会打印:完成

await即表示异步等待,用来暂停异步函数的执行,只能在异步函数和promise使用,且当使用在promise前面,表示等待promise完成并返回结果。

async function f() {
  return await 1  //await后面不是Promise的话,也会被转换为一个立即为resolve的promise
};
f().then( res => console.log("处理成功",res))//打印出:处理成功 1
	 .catch(err => console.log("处理是被",err))////打印出:Promise{<resolved>:undefined}

错误处理

如果await后面的异步出现错误,等同于async返回的promise对象为reject,其错误会被catch的回调函数接收到。需要注意的是,当 async 函数中只要一个 await 出现 reject 状态,则后面的 await 都不会被执行。

let a;
async function f(){
  await Promise.reject("error")
  a = await 1    //该await并没有执行 
}
err().then(res => console.log(a))

怎么处理呢,可以把第一个await放在try/catch,遇到函数的时候,可以将错误抛出并往下执行。

async function f() { 
  try{ 
    await Promise.reject('error');  
  }catch(error){
    console.log(error);
  }
  return await 1
}
f().then(res => console.log('成功', res))//成功打印出1

如果有多个await处理,可以统一放在try/catch模块中,而且async可以使得try/catch同时处理同步和异步错误。

总结

通过以上六种javascript异步处理的常用方法,可以看出async/await可以说是异步终极解决方案了,最后看一下async/await用得最多的场景:

如果一个业务需要很多个异步操作组成,并且每个步骤都依赖于上一步的执行结果,这里采用不同的延时来体现:

//首先定义一个延时函数
function delay(time) {
  return new Promise(resolve => {
    setTimeout(() => resolve(time), time);
  });
}
//采用promise链式调用实现
delay(500).then(result => {
  return delay(result + 1000)
}).then(result => {
  return delay(result + 2000)
}).then(result => {
  console.log(result)  //3500ms后打印出3500
}).catch(error => {
  console.log(error)
}) 
//采用async实现
async function f(){
 const r1 = await delay(500)
 const r2 = await delay(r1+1000)
 const r3 = await delay(r2+2000)
 return r3
}
f().then(res =>{
 console.log(res)
}).catch(err=>{
 console.log(err)
})

可以看出,采用promise实现采用了很多then进行不停的链式调用,使得代码变得冗长和复杂且没有语义化。而 async/await首先使用同步的方法来写异步,代码非常清晰直观,而且使代码语义化,一眼就能看出代码执行的顺序,最后 async 函数自带执行器,执行的时候无需手动加载。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
JQuery中关于jquery.js与jquery.min.js的比较探讨
May 15 Javascript
jquery获取元素值的方法(常见的表单元素)
Nov 15 Javascript
JS动态增加删除UL节点LI及相关内容示例
May 21 Javascript
javascript适合移动端的日期时间拾取器
Nov 10 Javascript
jQuery插件之jQuery.Form.js用法实例分析(附demo示例源码)
Jan 04 Javascript
js中字符串编码函数escape()、encodeURI()、encodeURIComponent()区别详解
Apr 01 Javascript
基于JavaScript实现单选框下拉菜单添加文件效果
Jun 26 Javascript
jQuery拖拽通过八个点改变div大小
Nov 29 Javascript
javascript DOM的详解及实例代码
Mar 06 Javascript
js实现倒计时效果(小于10补零)
Mar 08 Javascript
Vue v2.5 调整和更新不完全问题
Oct 24 Javascript
javascript关于“时间”的一次探索
Jul 24 Javascript
Vue源码分析之Vue实例初始化详解
Aug 25 #Javascript
javascript导出csv文件(excel)的方法示例
Aug 25 #Javascript
JavaScript在web自动化测试中的作用示例详解
Aug 25 #Javascript
angularjs自定义过滤器demo示例
Aug 24 #Javascript
Jquery实现获取子元素的方法分析
Aug 24 #jQuery
微信小程序class封装http代码实例
Aug 24 #Javascript
微信小程序前端promise封装代码实例
Aug 24 #Javascript
You might like
php调用MySQL存储过程的方法集合(推荐)
2013/07/03 PHP
php操作xml入门之xml标签的属性分析
2015/01/23 PHP
PHP的Laravel框架结合MySQL与Redis数据库的使用部署
2016/03/21 PHP
JavaScript Undefined,Null类型和NaN值区别
2008/10/22 Javascript
JavaScript高级程序设计 事件学习笔记
2011/09/10 Javascript
JavaScript fontsize方法入门实例(按照指定的尺寸来显示字符串)
2014/10/17 Javascript
JavaScript中数据结构与算法(二):队列
2015/06/19 Javascript
JavaScript中两个字符串的匹配
2016/06/08 Javascript
url中的特殊符号有什么含义(推荐)
2016/06/17 Javascript
bootstrap监听滚动实现头部跟随滚动
2016/11/08 Javascript
微信小程序 常用工具类详解及实例
2017/02/15 Javascript
Javascript中Promise的四种常用方法总结
2017/07/14 Javascript
angularjs使用gulp-uglify压缩后执行报错的解决方法
2018/03/07 Javascript
手写Node静态资源服务器的实现方法
2018/03/20 Javascript
node实现生成带参数的小程序二维码并保存到本地功能示例
2018/12/05 Javascript
Vue动态组件与异步组件实例详解
2019/02/23 Javascript
javascript简单实现深浅拷贝过程详解
2019/10/08 Javascript
js实现淘宝首页的banner栏效果
2019/11/26 Javascript
javascript实现移动端红包雨页面
2020/06/23 Javascript
Python实现基于权重的随机数2种方法
2015/04/28 Python
Python中的深拷贝和浅拷贝详解
2015/06/03 Python
python的pdb调试命令的命令整理及实例
2017/07/12 Python
Python算法之图的遍历
2017/11/16 Python
浅谈Python用QQ邮箱发送邮件时授权码的问题
2018/01/29 Python
python画蝴蝶曲线图的实例
2019/11/21 Python
python 实现批量替换文本中的某部分内容
2019/12/13 Python
Python通过getattr函数获取对象的属性值
2020/10/16 Python
CSS3悬停效果案例应用
2012/11/21 HTML / CSS
iHerb俄罗斯:维生素、补品和天然产品
2020/07/09 全球购物
俄罗斯品牌服装和鞋子在线商店:BRIONITY
2020/03/26 全球购物
说一下mysql, oracle等常见数据库的分页实现方案
2012/09/29 面试题
工会工作先进事迹
2014/08/18 职场文书
简历自我评价优缺点
2015/03/11 职场文书
出国留学英文自荐信
2015/03/25 职场文书
离职员工给领导和同事的感谢信
2015/11/03 职场文书
python在package下继续嵌套一个package
2022/04/14 Python