异步:简单说为一个任务分成两段,先执行第一段,然后执行其他任务,等做好了准备,再转过头执行第二段
异步和非阻塞是两个不同的概念
异步I/O和非阻塞I/O的区别:
阻塞造成CPU等待I/O,浪费等待时间,非阻塞I/O跟阻塞I/O的差别为调用之后会立即返回。
非阻塞的问题是由于完整的I/O并没有完成,立即返回的并不是业务期望的数据,而仅仅是当前调用状态。为了获得完整数据需要重复调用I/O操作确认是否完成,异步I/O可实现不等待数据读取完成。
优势:
特性是基于事件驱动的非阻塞I/O模型,非阻塞I/O可以使CPU与I/O并不互相依赖等待
难点:
1.异常处理
处理异常约定,将异常作为回掉函数的第一个参数传回,如果为空则说明没有异常
2.函数嵌套过深
3.阻塞代码
没有sleep的函数,只能使用setTimeout()
4.多线程编程
浏览器多线程Web Workers,nodejs的工作线程child_process是其基础API
5.异步转同步
异常编程解决方案
0.回调函数Callback
Javascript语言对异步编程的实现就是回调函数。回掉函数就是把任务的第二段单独写在一个函数里面,等重新执行这个任务的时候直接调用这个函数。
fs.readFile('/etc/passwd', function (err, data) { if (err) throw err; console.log(data); });readFile函数就是回调函数,回掉函数的第一个参数必须是错误对象err,原因是程序分成两段,在两段之间抛出的异常程序无法捕捉,只能当作参数传入第二段。
如果多层回调就会造成恶魔金字塔问题
1.Promise/Deferred模式
Promise是为了解决多重嵌套回调问题提出来的。不是新的语法功能,而是一种新的写法
Deferred主要是用于内部用于维护异步模型的状态;Promise则作用于外部,通过then()方法暴露给外部添加自定义逻辑
Promise操作只有三种状态:未完成态 --完成态 |--- 失败态
var readFile = require('fs-readfile-promise'); readFile(fileA) .then(function(data){ console.log(data.toString()); }) .then(function(){ return readFile(fileB); }) .then(function(data){ console.log(data.toString()); }) .catch(function(err) { console.log(err); });每个.then()方法都会返回一个新的promise
调用fs-readfile-promise模块,它的作用是返回一个Promise版本的readFile函数。提供then方法加载回掉函数,catch方法捕捉执行过程中抛出的错误。
promise的最大问题是代码冗余,原来任务被promise包装了一下,不管什么操作都是一堆then,原来的语义变得不清楚
2.事件发布/订阅模式
是回调函数的事件化。Node自身提供的events模块是发布/订阅模式的简单实现。
可利用事件队列解决雪崩问题例如:
var proxy=new events.EventEmitter(); var status="ready"; var select=function(callback){ proxy.once("selected",callback); if(status==="ready"){ status="pending"; db.select("SQL",function(results){ proxy.emit("selected",results); status="ready"; }); } }
3.generator函数
协程(coroutine):多个线程互相协作,完成异步任务。大致流程如下
-第一步,协程A开始执行
-第二步,协程A执行到一半,进入暂停,执行权转移到协程B
-第三步,(一段时间后)协程B交还执行权
-第四步,协程A恢复执行
协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。
整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。
异步任务的封装
var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); }Generator函数封装了一个异步操作。这段代码非常像同步操作,除了加上了yield命令
var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); });首先执行Generator函数,获取遍历器对象,然后执行next方法,执行一部任务的第一阶段。由于Fetch模块返回的是一个Promise对象,因此要用then方法调用下一个next方法
Thunk函数
JavaScript语言是传值调用,它的Thunk函数替换的是多参数函数,将其替换成单参数的版本,且只接受回掉函数作为参数。
通过高阶函数实现
//正常版本的readfile(多参数版本) fs.readFile(fileName,callback); //Thunk版本的readFile(单参数版本) var readFileThunk=Thunk(fileName); readFileThunk(callback); var Thunk=function(fileName){ return function(callback){ return fs.readFile(fileName,callback); } }简单的Thunk函数转化器
var Thunk = function(fn){ return function (){ var args = Array.prototype.slice.call(arguments); return function (callback){ args.push(callback); return fn.apply(this, args); } }; };转换器的使用
var readFileThunk = Thunk(fs.readFile); readFileThunk(fileA)(callback);但是在生产环境应使用 Thunkify模块
Generator函数的流程管理
var fs = require('fs'); var thunkify = require('thunkify'); var readFile = thunkify(fs.readFile); var gen = function* (){ var r1 = yield readFile('/etc/fstab'); console.log(r1.toString()); var r2 = yield readFile('/etc/shells'); console.log(r2.toString()); }yield命令用于将程序的执行权移出Generator函数,那么就需要一种方法将执行权交还给Generator函数
这种方法就是Thunk函数,因为它可以在回调函数里,将执行权交给Generator函数。自己先手动执行上面这个Generator函数
var g = gen(); var r1=g.next(); //值是一个函数,需要传入回调函数 r1.value(function(err,data){ if(err) throw err; var r2=g.next(data); r2.value(function(err,data){ if(err) throw err; g.next(data); }); });原理是将同一个回调函数,反复传入next方法的value属性。使我们可以用递归来自动完成这个过程。
以下函数是一个简单的Generator执行器
//执行器函数 function run(fn) { //获得遍历器 var gen = fn(); //执行方法 递归 function next(err, data) { //获得yield结果 value是一个函数 data参数给Generator函数上一个yield的返回值 var result = gen.next(data); if (result.done) return; //递归调用 执行方法 result.value(next); } //执行递归 next(); } //开始执行Generator函数 run(gen);不管有多少个异步操作,直接传入run函数即可。当然,前提是每一个异步操作,都要是Thunk函数,也就是说, 跟在yield命令后面的必须是Thunk函数
co模块的原理
Generator函数自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
两种方法可以做到这一点:
-1 回调函数。将异步操作包装近Thunk函数,在回掉函数里面交回执行权
-2 Promise对象。将异步操作包装成Promise对象,用then方法交回执行权
回调函数的方法上文已说,下文介绍封装成promise对象的方法
var fs = require('fs'); //调用方法封装为promise函数 var readFile = function(fileName){ return new Promise(function(resolve,reject){ fs.readFile(fileName,function(error,data){ if(error) reject(error); resolve(data); }); }); }; var gen = function* (){ var r1 = yield readFile('/etc/fstab'); var r2 = yield readFile('/etc/shells'); console.log(r1.toString()); console.log(r2.toString()); }手动执行上面的Generator函数
var g = gen(); g.next().value.then(function(data){ g.next().value.then(function(data){ g.next(data); }); });手动执行用的是then方法,层层回掉函数。可写一个自动执行器
//执行器函数 function run(gen){ //获得遍历器 var g = gen(); //执行方法 递归 function next(data){ //获得yield结果 value是一个函数 data参数给Generator函数上一个yield的返回值 var result=g.next(data); if(result.done) return result.value(); //递归调用 执行方法 result.value.then(function(data){ next(data); }); } //执行递归 next(); } //开始执行Generator函数 run(gen);
4.流程控制库
1.尾触发与Next
2.async
3.Step
4.wind
参考:http://es6.ruanyifeng.com/#docs/async 《深入浅出nodejs》