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 相关文章推荐
javascript 事件处理、鼠标拖动效果实现方法详解
May 11 Javascript
图片img的src不变让浏览器重新加载实现方法
Mar 29 Javascript
jQuery DOM插入节点操作指南
Mar 03 Javascript
Js与Jq 获取页面元素值的方法和差异对比
Apr 30 Javascript
JS实现单击输入框弹出选择框效果完整实例
Dec 14 Javascript
基于jQuery实现鼠标点击导航菜单水波动画效果附源码下载
Jan 06 Javascript
关于网页中的无缝滚动的js代码
Jun 09 Javascript
微信小程序授权获取用户详细信息openid的实例详解
Sep 20 Javascript
Vue实现active点击切换方法
Mar 16 Javascript
vue中使用带隐藏文本信息的图片、图片水印的方法
Apr 24 Javascript
vue常用高阶函数及综合实例
Feb 25 Vue.js
利用promise及参数解构封装ajax请求的方法
Mar 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
为IP查询添加GOOGLE地图功能的代码
2010/08/08 PHP
浅析is_writable的php实现
2013/06/18 PHP
PHP判断一个gif图片是否为动态图片的方法
2014/11/19 PHP
php文件上传简单实现方法
2015/01/24 PHP
PHP实现简单爬虫的方法
2015/07/29 PHP
Yii2 rbac权限控制之rule教程详解
2016/06/23 PHP
JavaScript入门教程(8) Location地址对象
2009/01/31 Javascript
js select常用操作控制代码
2010/03/16 Javascript
JavaScript中创建类/对象的几种方法总结
2013/11/29 Javascript
js实现点击添加一个input节点
2014/12/05 Javascript
整理Javascript数组学习笔记
2015/11/29 Javascript
【JS+CSS3】实现带预览图幻灯片效果的示例代码
2016/03/17 Javascript
Bootstrap选项卡与Masonry插件的完美结合
2016/07/06 Javascript
js实现楼层效果的简单实例
2016/07/15 Javascript
jQuery的Read()方法代替原生JS详解
2016/11/08 Javascript
浅析上传头像示例及其注意事项
2016/12/14 Javascript
浅谈js停止事件冒泡 阻止浏览器的默认行为(阻止超连接 #)
2017/02/08 Javascript
Vue组件通信之Bus的具体使用
2017/12/28 Javascript
axios全局请求参数设置,请求及返回拦截器的方法
2018/03/05 Javascript
vue动态路由配置及路由传参的方式
2018/05/23 Javascript
详解vue 兼容IE报错解决方案
2018/12/29 Javascript
Vue将props值实时传递 并可修改的操作
2020/08/09 Javascript
[04:40]2016个国际邀请赛中国区预选赛场地——华西村观战指南
2016/06/25 DOTA
[46:49]完美世界DOTA2联赛PWL S3 access vs Rebirth 第二场 12.19
2020/12/24 DOTA
Python标准模块--ContextManager上下文管理器的具体用法
2017/11/27 Python
python一行sql太长折成多行并且有多个参数的方法
2018/07/19 Python
python的pstuil模块使用方法总结
2019/07/26 Python
python 协程中的迭代器,生成器原理及应用实例详解
2019/10/28 Python
Pytorch使用MNIST数据集实现基础GAN和DCGAN详解
2020/01/10 Python
阿玛尼美妆俄罗斯官网:Giorgio Armani Beauty RU
2020/07/19 全球购物
越南母婴用品购物网站:Kids Plaza
2020/04/09 全球购物
优秀学生党员先进事迹材料
2014/05/29 职场文书
学生打架检讨书
2014/10/20 职场文书
幼儿园托班教育随笔
2015/08/14 职场文书
如何理解及使用Python闭包
2021/06/01 Python
船舶调度指挥系统——助力智慧海事
2022/02/18 无线电