JS异步开发总结

1 前言

众所周知,JS语言是单线程的。在实际开发过程中都会面临一个问题,就是同步操作会阻塞整个页面乃至整个浏览器的运行,只有在同步操作完成之后才能继续进行其他处理,这种同步等待的用户体验极差。所以JS中引入了异步编程,主要特点就是不阻塞主线程的继续执行,用户直观感受就是页面不会卡住。

2 概念说明

2-1 浏览器的进程和线程

首先可以确定一点是浏览器是多进程的,比如打开多个窗口可能就对应着多个进程,这样可以确保的是页面之间相互没有影响,一个页面卡死也并不会影响其他的页面。同样对于浏览器进程来说,是多线程的,比如我们前端开发人员最需要了解的浏览器内核也就是浏览器的渲染进程,主要负责页面渲染,脚本执行,事件处理等任务。为了更好的引入JS单线程的概念,我们将浏览器内核中常用的几个线程简单介绍一下:

  1. GUI渲染线程 负责渲染浏览器页面,解析html+css,构建DOM树,进行页面的布局和绘制操作,同事页面需要重绘或者印发回流时,都是该线程负责执行。

  2. JS引擎线程 JS引擎,负责解析和运行JS脚本,一个页面中永远都只有一个JS线程来负责运行JS程序,这就是我们常说的JS单线程。

    注意:JS引擎线程和GUI渲染线程永远都是互斥的,所以当我们的JS脚本运行时间过长时,或者有同步请求一直没返回时,页面的渲染操作就会阻塞,就是我们常说的卡死了

  3. 事件触发线程 接受浏览器里面的操作事件响应。如在监听到鼠标、键盘等事件的时候, 如果有事件句柄函数,就将对应的任务压入队列。

  4. 定时触发器线程 浏览器模型定时计数器并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 它必须依赖外部来计时并触发定时。

  5. 异步http请求线程 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

2-2 JS单线程

因为只有JS引擎线程负责处理JS脚本程序,所以说JS是单线程的。可以理解的是js当初设计成单线程语言的原因是因为js需要操作dom,如果多线程执行的话会引入很多复杂的情况,比如一个线程删除dom,一个线程添加dom,浏览器就没法处理了。虽然现在js支持webworker多线线程了,但是新增的线程完全在主线程的控制下,为的是处理大量耗时计算用的,不能处理DOM,所以js本质上来说还是单线程的。

2-3 同步异步

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

2-4 任务队列

任务队列就是用来存放一个个带执行的异步操作的队列,在ES6中又将任务队列分为宏观任务队列和微观任务队列。

宏任务队列(macrotask queue)等同于我们常说的任务队列,macrotask是由宿主环境分发的异步任务,事件轮询的时候总是一个一个任务队列去查看执行的,"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。

微任务队列(microtask queue)是由js引擎分发的任务,总是添加到当前任务队列末尾执行。另外在处理microtask期间,如果有新添加的microtasks,也会被添加到队列的末尾并执行

2-5 事件循环机制

异步时间添加到任务队列中后,如何控制他们的具体执行时间呢?JS引擎一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

ES5的JS事件循环参考图:

ES6的JS事件循环参考图:

理解了JS程序执行的基本原理,下面就可以步入正题,讨论一下我们在实际开发中,如何编写异步程序才能让自己的代码易读易懂bug少。

3 callback

在JavaScript中,回调函数具体的定义为:函数A作为参数(函数引用)传递到另一个函数B中,并且这个函数B执行函数A。我们就说函数A叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。

因此callback 不一定用于异步,一般同步(阻塞)的场景下也经常用到回调,比如要求执行某些操作后执行回调函数。

回调函数被广泛应用到JS的异步开发当中,下面分别列举几条开发中常用回调函数的情况,如:

  1. 时间延迟操作
setTimeout(function(){//该方法为回调方法//code
}, 1000)setInterval(()=>{//该方法为匿名回调方法//code
}, 1000)
复制代码
  1. nodeapi
//node读取文件
fs.readFile(xxx, 'utf-8', function(err, data) { //该方法为读取文件成功后出发的回调方法//code
});
复制代码
  1. ajax操作
$.ajax({type: "post",url: "xxx",success: function(data){//post请求成功回调方法//code},error: fucntion(e){//post请求错误回调方法//code}
})
复制代码

用回调函数的方法来进行异步开发好处就是简单明了,容易理解

回调函数的缺点, 用一个小的实例来说明一下:

method1(function(err, data) {//code1method2(function(err, data) { //code2method3(function(err, data) { //code3method4(D, 'utf-8', function(err, data) { //code4 });});});});
复制代码

如果说异步方法之前有明确的先后顺序来执行,稍微复杂的操作很容易写出上面示例的代码结构,如果加上业务代码,程序就显得异常复杂,代码难以理解和调试,这种就是我们常说的回调地狱。

如果想要实现更加复杂的功能,回调函数的局限性也会凸显出来,比如同时执行两个异步请求,当两个操作都结束时在执行某个操作,或者同时进行两个请求,取优先完成的结果来执行操作,这种都需要在各自的回调方法中监控状态来完成。

随着ES6/ES7新标准的普及,我们应该寻求新的异步解决方案来替代这种传统的回调方式。

4 Promise

ES6新增Promise对象的支持,Promise提供统一的接口来获取异步操作的状态信息,添加不能的处理方法。

Promise对象只有三种状态:

  1. pendding: 初始状态,既不是成功,也不是失败状态。
  2. fulfilled: 意味着操作成功完成。
  3. rejected: 意味着操作失败。

Promise的状态只能由内部改变,并且只可以改变一次。

下面看看用Promise来实现多级回调能不能解决回调地狱的问题

 function read(filename){return new Promise((resolve, reject) => {//异步操作code, Example:fs.readFile(filename, 'utf8', (err, data) => { if(err) reject(err);resolve(data);});})}read(filename1).then(data=>{return read(filename2)}).then(data => {return read(filename3)}).then(data => {return read(filename4)}).catch(error=>{console.log(error);})
复制代码

通过实践代码 我们发现用Promise可以像写同步代码一样实现异步功能,避免了层层嵌套的问题。

如何用Promise来实现同时发起多个异步操作的需求

  • 多个请求都完成后在执行操作
function loadData(url){return new Promise((resolve, reject)=>{$.ajax({type: "post",url: url,success: function(data){//post请求成功回调方法resolve(data)},error: fucntion(e){//post请求错误回调方法reject(e)}})})
}Promise.all([loadData(url1), loadData(url2), loadData(url3)])
.then(data => {console.log(data)
}).catch(error => {console.log(error);
})
复制代码
  • 多个请求有一个完成后(成功或拒绝)就执行操作
function loadData(url){return new Promise((resolve, reject)=>{$.ajax({type: "post",url: url,success: function(data){//post请求成功回调方法resolve(data)},error: fucntion(e){//post请求错误回调方法reject(e)}})})
}Promise.race([loadData(url1), loadData(url2), loadData(url3)])
.then(data => {console.log(data)
}).catch(error => {console.log(error);
})
复制代码

用Promise来写异步可以避免回调地狱,也可以轻松的来实现callback需要引入控制代码才能实现的多个异步请求动作的需求。

当然Promise也有自己的缺点:

  1. promise一旦新建,就会立即执行,无法取消
  2. 如果不设置回掉函数,promise内部抛出的错误就不会反应到外部
  3. 处于pending状态时,是不能知道目前进展到哪个阶段的 ( 刚开始?,即将结束?)

带着这些缺点,继续往下学习别的异步编程方案。

**关于Promise的详细文章可以阅读这篇你真的会用 Promise 吗

5 Generator

ES6新增Generator异步解决方案,语法行为与传统方法完全不一样。

Generator函数是一个状态机,封装了多个内部状态,也是一个遍历器对象生成函数,生成的遍历器对象可以一次遍历内部的每一个状态。

Generator用function*来声明,除了正常的return返回数据之外,还可以用yeild来返回多次。

调用一个Generator对象生成一个generator对象,但是还并没有去执行他,执行generator对象有两种方法:

  • next()方法,next方法回去执行generator方法,遇到yeild会返回一个{value:xx, done: true/fasle}的对象,done为true说明generator执行完毕
  • 第二个方法是用for....of循环迭代generator对象

Generator的用处很多,本文只讨论利用它暂停函数执行,返回任意表达式的值的这个特性来使异步代码同步化表达。从死路上来讲我们想达到这样的效果:

function loadData(url, data){//异步请求获取数据return new Promise((resolve, reject)=>{$.ajax({type: "post",url: url,success: function(data){//post请求成功回调方法resolve(data)},error: fucntion(e){//post请求错误回调方法reject(e)}})})
}
function*  gen() {yeild loadData(url1, data1);yeild loadData(url2, data2);yeild loadData(url3, data3);
}for(let data of gen()){//分别输出每次加载数据的返回值console.log(data)
}
复制代码

但仅仅是这样来实现是不行的,因为异步函数没有返回值,必须通过重新包装的方式来传递参数值。co.js就是一个这种generator的执行库。使用它是我们只需要将我们的 gen 传递给它像这样 co(gen) 是的就这样。

function*  gen() {let data1 = yeild loadData(url1, data1);console.log(data1);let data2 = yeild loadData(url2, data2);console.log(data2);let data3 = yeild loadData(url3, data3);console.log(data3);
}
co(gen())
.then(data => {//gen执行完成
}).catch(err => {//code
})
复制代码

因为ES7中新增了对async/await的支持,所以异步开发有了更好的选择,基本上可以放弃用原生generator来写异步开发,所以我们只是有个简单的概念,下面我们着重介绍一下异步编程的最终方案 async/await。

6 async/await

asycn/await方案可以说是目前解决JS异步编程的最终方案了,async/await是generator/co的语法糖,同时也需要结合Promise来使用。该方案的主要特点如下:

  • 普通函数,即所有的原子型异步接口都返回Promise,Promise对象中可以进行任意异步操作,必须要有resolve();
  • async函数,函数声明前必须要有async关键字,函数中执行定义的普通函数,并且每个执行前都加上await关键字,标识该操作需要等待结果。
  • 执行async函数。asynch函数的返回值是Promise对象,可以用Promise对象的then方法来指定下一步操作。

还用用代码来说明问题,用async/await方案来实现最初的需求

//普通函数
function loadData(url){//异步请求获取数据return new Promise((resolve, reject)=>{$.ajax({type: "post",url: url,success: function(data){//post请求成功回调方法resolve(data)},error: fucntion(e){//post请求错误回调方法reject(e)}})})
}//async函数
async function asyncFun(){//普通函数的调用let data1 = await loadData(url1);let data2 = await loadData(url2);let data3 = await loadData(url3)
}asyncFun()
.then(data => {//async函数执行完成后操作
})
.catch(err => {//异常抓取
});
复制代码

loadData()函数虽然返回的是Promise,但是await返回的是普通函数resole(data)时传递的data值。

通过和generator方式来的实现对比来看,更加理解了async/await是generator/co方法的语法糖,从函数结构上来说完全一样。但是省略了一些外库的引入,一些通用方法的封装,使异步开发的逻辑更加清晰,更加接近同步开发。

处理完有先后顺序的请求处理,下面来个多个请求同时发起的例子

//普通函数
function loadData(url){//异步请求获取数据return new Promise((resolve, reject)=>{$.ajax({type: "post",url: url,success: function(data){//post请求成功回调方法resolve(data)},error: fucntion(e){//post请求错误回调方法reject(e)}})})
}//async函数
async function asyncFun(){await Promise.all([loadData('url1'), loadData('url2')]).then(data => {console.log(data); //['data1', 'data2']})
}asyncFun();//配合Promise的race方法同样可以实现任意请求完成或异常后执行操作的需求//async函数
async function asyncFun(){await Promise.race([loadData('url1'), loadData('url2')]).then(data => {console.log(data);})
}
复制代码

最佳实践

通过上面四种不同的异步实现方式的对比可以发现,async/await模式最接近于同步开发,即没有连续回调,也没有连续调用then函数的情况,也没有引入第三方库函数,所以就目前来说async/await+promise的方案为最佳实践方案。

社区以及公众号发布的文章,100%保证是我们的原创文章,如果有错误,欢迎大家指正。

文章首发在WebJ2EE公众号上,欢迎大家关注一波,让我们大家一起学前端~~~

再来一波号外,我们成立WebJ2EE公众号前端吹水群,大家不管是看文章还是在工作中前端方面有任何问题,我们都可以在群内互相探讨,希望能够用我们的经验帮更多的小伙伴解决工作和学习上的困惑,欢迎加入。

转载于:https://juejin.im/post/5cff4f3df265da1b86087dd7

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/536398.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

迷宫问题【广搜】

迷宫问题 POJ - 3984 定义一个二维数组: int maze[5][5] {0, 1, 0, 0, 0,0, 1, 0, 1, 0,0, 0, 0, 0, 0,0, 1, 1, 1, 0,0, 0, 0, 1, 0,}; 它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能…

大虾对51单片机入门的经验总结

回想起当初学习AT89S52的日子还近在眼前:毕业后的第一年呆在亲戚公司做了10个月设备管理.乏味的工作和繁杂的琐事让我郁闷不已.思考很久后终于辞职.投奔我的同学去了,开始并不曾想到要进入工控行业,知识想找一份电子类技术职业,至于什么职业我根本没有目标可言.经过两个多月的挫…

mac安装cnpm

1.先安装node node的下载地址:http://nodejs.cn/download/ 这个没什么好说的,安装完成后测试一下,在终端输入:node -v 这时候就可以看到安装的node版本号,再输入:npm -v 这时候就会看到npm的版本号了 2.用n…

A计划【广搜】

A计划 HDU - 2102 可怜的公主在一次次被魔王掳走一次次被骑士们救回来之后,而今,不幸的她再一次面临生命的考验。魔王已经发出消息说将在T时刻吃掉公主,因为他听信谣言说吃公主的肉也能长生不老。年迈的国王正是心急如焚,告招天…

WordPress忘记密码的5种解决方法

为什么80%的码农都做不了架构师?>>> 无意中忘记wordpress的密码了,恰巧在后台又没来得及设置邮件,只好四处苦寻解决办法,还好总算找到了…… 1. WordPress内置的找加密码方法 如果你的admin帐户的电子邮件地址是正确的…

记录一次,事务遇到消息发送,疏忽给自己挖坑

场景:一个异步重算功能(任务新建后发送消息到RocketMq),每次重算单条记录的时候,可以计算正确,但是当多条记录批量重算时,结果总是莫名其妙的不对。排查了很久,终于找到原因 原因&am…

在linux上执行.net Console apps

为什么80%的码农都做不了架构师?>>> 有个程序,在.net下写了半天,总算跑起来了,发现有个问题,在windows上不好弄,而同事前一段时间已经有Linux下的解决方法了,于是想直接将.net程序放…

Android4.0设置界面修改总结

为什么80%的码农都做不了架构师?>>> 笔者前段时间完成设置的圆角item风格的修改,但最近,客户新增需求,想把设置做成Tab风格的,没办法,顾客就是上帝,咱得改啊。今天算是初步改完了&a…

敌兵布阵【线段树】

敌兵布阵 HDU - 1166 C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了。A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的活动情况。由于采取了某种先进的监测手段,所以每个工兵…

Android之仿网易V3.5新特性

为什么80%的码农都做不了架构师?>>> 最近,网易新闻更新到V3.5了,给我印象最深的是第一次进应用时显示新特性的ViewPager变成垂直滑动了。于是,小小的模仿了一下,我们来看看效果: 本文源码下载地…

Android_内存泄露

2019独角兽企业重金招聘Python工程师标准>>> 1.资源对象没关闭造成的内存泄漏 描述: 资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以…

CYQ.Data 轻量数据层之路 使用篇三曲 MAction 取值赋值(十四)

2019独角兽企业重金招聘Python工程师标准>>> 上一篇:CYQ.Data 轻量数据层之路 使用篇二曲 MAction 数据查询(十三) 内容概要 本篇继续上一篇内容,本节介绍所有取值与赋值的相关操作。1:原生:像操作Row一样…

CYQ.Data 数据框架 发放V1.5版本源码

2019独角兽企业重金招聘Python工程师标准>>> 本篇的内容很简单,就发放V1.5版本源码,同时补充了所有发布版本的API文档。 具体相关下载地址见: 秋色园下载中心:http://www.cyqdata.com/download/article-detail-426 如何…

New Bus Route

New Bus Route CodeForces - 792A There are n cities situated along the main road of Berland. Cities are represented by their coordinates — integer numbers a1, a2, ..., an. All coordinates are pairwise distinct. It is possible to get from one city to …

爱说说技术原理:与TXT交互及MDataTable对Json的功能扩展(二)

2019独角兽企业重金招聘Python工程师标准>>> 关于爱说说在技术选型的文章见:"爱说说"技术原理方案的定选思考过程 本篇将讲述“爱说说”比较重大的技术问题点及解决手段: 爱说说:http://speak.cyqdata.com/ 杂说几句&am…

ActiveXObject 安装

将后缀名为ocx的文件拷贝至目录 c:\Windows\SysWOW64\。执行如下命令,进行注册:regsvr32 c:\Windows\SysWOW64\x.ocx转载于:https://www.cnblogs.com/Currention/p/11024354.html

如何制作VSPackage的安装程序

2019独角兽企业重金招聘Python工程师标准>>> 第一步,生成一个REG文件: 收钱进入目录: C:\Program Files\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\Tools\Bin 这是SDK的目录,使用regpkg.exe 命令 命令格式为: …

MyBatis学习总结(1)——MyBatis快速入门

2019独角兽企业重金招聘Python工程师标准>>> 一、Mybatis介绍 MyBatis是一个支持普通SQL查询,存储过程和高级映射的优秀持久层框架。MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及对结果集的检索封装。MyBatis可以使用简单的XML或注解用于配置和…

MyEclipse+Tomcat+MAVEN+SVN项目完整环境搭建

2019独角兽企业重金招聘Python工程师标准>>> 这次换了台电脑,所以需要重新配置一次项目开发环境,过程中的种种,记录下来,便于以后再次安装,同时给大家一个参考。 1.JDK的安装 首先下载JDK,这个从…

Java基础学习总结(10)——static关键字

2019独角兽企业重金招聘Python工程师标准>>> 一、static关键字 原来一个类里面的成员变量,每new一个对象,这个对象就有一份自己的成员变量,因为这些成员变量都不是静态成员变量。对于static成员变量来说,这个成员变量只…