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中的prototype.bind()方法介绍
Apr 04 Javascript
js如何判断用户是否是用微信浏览器
Jun 05 Javascript
jquery插件jSignature实现手动签名
May 04 Javascript
原生JS实现旋转木马式图片轮播插件
Apr 25 Javascript
JS实现去除数组中重复json的方法示例
Dec 21 Javascript
JavaScript实现的反序列化json字符串操作示例
Jul 18 Javascript
JavaScript中为事件指定处理程序的五种方式分析
Jul 27 Javascript
详解Vue CLI3配置解析之css.extract
Sep 14 Javascript
在js代码拼接dom对象到页面上的模板总结
Oct 21 Javascript
vue 弹窗时 监听手机返回键关闭弹窗功能(页面不跳转)
May 10 Javascript
AngularJs的$http发送POST请求,php无法接收Post的数据问题及解决方案
Aug 13 Javascript
关于element-ui表单中限制输入纯数字的解决方式
Sep 08 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数据库操作
2010/11/01 PHP
ThinkPHP采用原生query实现关联查询left join实例
2014/12/02 PHP
Zend Framework实现自定义过滤器的方法
2016/12/09 PHP
mac os快速切换多个PHP版本的方法
2017/03/07 PHP
jquery 插件学习(三)
2012/08/06 Javascript
window.location.reload()方法刷新页面弹出要再次显示该网页对话框
2013/04/24 Javascript
jquery与prototype框架的详细对比
2013/11/21 Javascript
JS+CSS实现仿新浪微博搜索框的方法
2015/02/24 Javascript
浅谈javascript原型链与继承
2015/07/13 Javascript
JavaScript实现的SHA-1加密算法完整实例
2016/02/02 Javascript
ajax图片上传,图片异步上传,更新实例
2016/12/30 Javascript
vue之数据交互实例代码
2017/06/16 Javascript
JS 判断某变量是否为某数组中的一个值的3种方法(总结)
2017/07/10 Javascript
vue里面v-bind和Props 利用props绑定动态数据的方法
2018/08/27 Javascript
原生JS实现的放大镜特效示例【测试可用】
2018/12/08 Javascript
PM2自动部署代码步骤流程总结
2018/12/10 Javascript
Vue 实现创建全局组件,并且使用Vue.use() 载入方式
2020/08/11 Javascript
微信小程序调用后台service教程详解
2020/11/06 Javascript
浅谈python字典多键值及重复键值的使用
2016/11/04 Python
教你使用python实现微信每天给女朋友说晚安
2018/03/23 Python
Ubuntu16.04/树莓派Python3+opencv配置教程(分享)
2018/04/02 Python
Django文件存储 自己定制存储系统解析
2019/08/02 Python
Python学习笔记之文件的读写操作实例分析
2019/08/07 Python
Python控制台实现交互式环境执行
2020/06/09 Python
Python3爬虫中Selenium的用法详解
2020/07/10 Python
浅谈HTML5 服务器推送事件(Server-sent Events)
2017/08/01 HTML / CSS
美国玛丽莎收藏奢华时尚商店:Marissa Collections
2016/11/21 全球购物
电子狗项圈:eDog Australia
2019/12/04 全球购物
你在项目中用到了xml技术的哪些方面?如何实现的?
2014/01/26 面试题
医学院学生的自我评价分享
2013/11/19 职场文书
学生安全承诺书
2014/05/22 职场文书
小学生九一八纪念日83周年演讲稿500字
2014/09/17 职场文书
python状态机transitions库详解
2021/06/02 Python
Opencv实现二维直方图的计算及绘制
2021/07/21 Python
船舶调度指挥系统——助力智慧海事
2022/02/18 无线电
中国古风插画师排行榜:夏达第一,第三是阴阳师姑获鸟皮肤创作者
2022/03/18 国漫