学习js开发,不管是前端开发还是node。该设计是单线程异步模型。正是因为js本身就与众不同,才具有独特的魅力,也给学习者带来了很多探索的路径。本文从js的初始设计入手,梳理js异步编程的开发过程。什么是异步在研究js异步之前,先搞清楚什么是异步。异步是与同步相反的概念。同步是指发起调用后等待结果返回,返回时必须获取返回结果。异步调用发起后直接返回,返回时没有结果,无需等待结果,但调用结果产生后由被调用者传递通知调用者。比如A想找C,但是不知道C的电话号码,但是他有B的电话号码,所以A打电话给B问C的电话号码,B需要搜索知道C的电话号码,然后就会出现See下面两种场景分别是A不挂电话,B找到号码后告诉AA直接挂断,B找到号码后再打电话告诉A。是不是感觉两种情况不一样,前者是同步的,后者是异步的。为什么是异步的?先来看看js的诞生。JavaScript诞生于1995年,由BrendanEich设计。至于后来提交给ECMA形成规范,各种来龙去脉不是本文的重点。说这些只是想说js最初的设计是为了浏览器的GUI交互。对于图形界面的处理,多线程的引入必然会带来各种同步问题,所以浏览器中的js被设计成单线程也就很容易理解了。但是单线程有一个问题:一旦唯一的线程被阻塞,就没有办法工作了——这肯定不行。既然异步编程可以达到“非阻塞”的调用效果,那么自然要介绍异步编程。现在,js的运行环境不局限于浏览器,也有node.js。node.js设计的初衷是设计一个完全事件驱动、非阻塞IO的服务器运行环境,因为网络IO请求是一个非常大的性能瓶颈,其他编程语言的使用早期失败,因为人们固有的同步编程思想,人们更倾向于使用同步设计的API。由于js最初的设计是完全异步的,人们不会有很多不适,而V8高性能引擎的出现,造就了node.js技术。Node.js擅长处理IO密集型业务,得益于事件驱动、非阻塞IO设计,这一切都离不开异步编程。js异步原理这是浏览器js执行的简化流程图。nodejs跟它不一样,但是有一个队列。这个队列是一个异步队列,是处理异步事件的核心。整个js调用的时候,同步任务和其他编程语言一样,是在栈上调用的。一旦遇到异步任务,不会立即执行,直接放入异步队列,这样就形成了两个不同的任务。由于主线程中没有阻塞,所以很快就完成了。栈中的任务为空后,会有事件循环将队列中的任务逐一取出执行。只要主线程空闲,异步队列中有任务,事件循环就会从队列中取出任务并执行。说起来比较简单,js的执行引擎设计要比这复杂的多,但是在js的异步实现原理中,事件循环和异步队列是核心内容。异步编程实现了异步编程的代码实现,并且随着时间的推移逐渐完善。不仅仅是js,很多编程语言的用户都在寻找一种优雅的方式来编写异步编程代码。我们看一下js中的代码,有几个重要的实现。最经典的异步编程方法——回调说到异步编程,就不得不提回调(callback)方法,它是最传统的异步编程解决方案。首先你要知道回调可以解决异步问题,但是并不意味着使用回调就是异步任务。下面以最常见的网络请求为例,演示一下回调是如何处理异步任务的。先来看一个错误的例子:functiongetData(url){constdata=$.get(url);返回数据;}constdata=getData('/api/data');//错误,数据未定义由于函数getData需要在内部执行网络请求,所以无法预测结果的返回时间。直接以同步的方式返回结果是不可行的。正确的写法是这样的:functiongetData(url,callback){$.get(url,data=>{if(data.status===200){callback(null,data);}else{callback(数据);}});}getData('/api/data',(err,data)=>{if(err){console.log(err);}else{console.log(data);}});callback利用functions传统编程的特点,将要执行的函数作为参数传入,由被调用者控制执行的时机,保证能够得到正确的结果。这种方法乍一看可能有点难懂,但是熟悉函数式编程其实很简单,很好的解决了最基本的异步问题。早期的异步编程只能通过这种方式来完成。但是,这种方法有一个致命的问题。在实际开发中,模型并不总是那么简单。以下场景是常见的:fun1(data=>{//...fun2(data,result=>{//...fun3(result,()=>{//...});});});随着系统越来越复杂,整个回调函数的层级会逐渐加深,加入复杂的逻辑,代码编写和维护都会变得非常困难,几乎没有可读性。这叫做毁天灭地,曾经困扰开发者,甚至是异步编程中最受诟病的地方。滚出地狱——promise使用回调函数编程很简单,但是回调地狱真的很可怕。嵌套层次足够深之后,绝对是维护的噩梦,而promise的出现就是为了解决这个问题。承诺是根据规范实现的对象。ES6提供了原生实现,早期的三方实现也有很多。我们这里不讨论promise的规范和实现原理,而是重点关注promise是如何解决异步编程问题的。Promise对象表示一个尚未完成但预计在未来完成的操作。它有三种状态:pending:初值,notfulfilled,也不rejectedresolved(也叫fulfilled):表示操作成功rejected:表示操作失败只支持整个promise的状态两种转换:从pending到已解决,或从待处理变为拒绝。一旦发生转换,它将保持此状态,无法再次更改。状态改变后,then方法会被触发。这个比较抽象,我们直接修改上面的例子:functiongetData(url){returnnewPromise((resolve,reject)=>{$.get(url,data=>{if(data.status===200){reject(data);}else{resolve(data);}});});}getData('/api/data').then(data=>{console.log(data);}).catch(错误=>{console.log(错误);});Promise是一个构造函数,创建一个promise对象并接收一个回调函数作为参数,回调函数接收两个函数作为参数,代表两种类型的promise状态转换。resolve回调会将promise从pending更改为resolved,reject回调会将promise从pending更改为rejected。当promise变为resolved时,then方法会被触发,在then方法中可以获取resolved的内容。一旦承诺被拒绝,就会产生错误。无论是resolve还是reject,都会返回一个新的Promise实例,并将返回值作为参数传递给新Promise的resolve函数,从而实现链式调用。对于错误处理,系统提供了一个catch方法,错误总是会向后传递,总能被下一个catch捕获。使用promise可以有效避免回调嵌套的问题,代码会是这样:fun1().then(data=>{//...returnfun2(data);}).then(result=>{//...returnfun3(result);}).then(()=>{//...});整个调用过程变得非常清晰,可维护性和可扩展性都会大大增强,promise是一种非常重要的异步编程方法,它改变了以前的思维方式,也是后来出现新方法的重要基础。换个思路——generatorpromise是最好的写法吗?和回调函数相比,链式调用增加了很多可维护性,但是和同步编程相比,异步就显得不那么和谐了,生成器的出现带来了另一种思路。Generator是ES对协程的实现。协程是指函数不是作为一个整体来执行的。一个函数可以在执行到一半后交出执行权,在有可能的情况下再获取执行权。这种方法最大的特点是同步。想了想,除了控制执行的yield命令,整体看起来和同步编程差不多。我们来看看这个方法的写法:functiongetDataPromise(url){returnnewPromise((resolve,reject)=>{$.get(url,data=>{if(data.status===200){reject(data);}else{resolve(data);}});});}function*getDataGen(url){yieldgetDataPromise(url);}constg=getDataGen('/api/data');g。下一个();生成器和普通函数的区别在于前面多了一个*,但这不是重点,重点是生成器中可以使用yield关键字来表示暂停,它接收一个promise对象,返回promise的结果并在此处停止等待,而不是一次全部执行。生成器执行后,会返回一个迭代器。迭代器中有一个next方法。每次调用next方法时,生成器都会向下执行,直到遇到yield。返回的结果是一个对象,里面有一个value属性,也就是当前yield返回的结果,done属性代表整个generator是否执行完毕。生成器的出现使得像同步一样编写异步代码成为可能。以下是使用生成器转换后的结果:*fun(){constdata=yieldfun1();//...constresult=yieldfun2(data);//...yieldfun3(result);//...}constg=fun();g.next();g.next();g.next();g.next();在generator编写的过程中,我们仍然需要手动控制执行过程,但实际上这是可以自动实现的。下一代新语法使异步编程真的和同步编程一样简单。新时代的写法——async,await异步编程的最高境界就是你不需要关心它是不是异步的。在最新的ES中,终于有了这种令人兴奋的语法。async函数的写法和generator几乎一样,只是把*换成async关键字,把yield换成await。async函数带有一个内置的生成器执行器。我们不再需要手动控制执行。现在让我们看看最后的写法:functiongetDataPromise(url){returnnewPromise((resolve,reject)=>{$.get(url,data=>{if(data.status===200){reject(data);}else{resolve(data);}});});}asyncfunctiongetData(url){returnawaitgetDataPromise(url);}constdata=awaitgetData(url);除了增加了关键字,其余和同步编码方式完全一样,同步try-catch方式也可以用于异常捕获,对于复杂的场景逻辑不会混乱:*fun(){const数据=等待fun1();//...constresult=awaitfun2(data);//...returnawaitfun3(result);//...}fun()now回过头来看回调函数的写法,感觉就像是另一个世界。这个语法比较新,不支持的环境应该使用babel翻译。用js写在最后,异步编程是一个长期的话题。很高兴有这么好用的async和await,但是promise和callback函数的原理一定要搞懂。搞清楚异步编程的模式很重要。也算是扫清了学习js,尤其是node.js路上最大的障碍。尊重原创,转载分享前请知晓作者,欢迎指出错误,共同交流。更多内容请关注作者博客。点击这里
