关闭

异步编程优势难点及解决方案

701人阅读 评论(0) 收藏 举报
分类:

异步:简单说为一个任务分成两段,先执行第一段,然后执行其他任务,等做好了准备,再转过头执行第二段

异步和非阻塞是两个不同的概念

异步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》






2
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:45919次
    • 积分:1211
    • 等级:
    • 排名:千里之外
    • 原创:75篇
    • 转载:34篇
    • 译文:0篇
    • 评论:5条
    文章分类
    最新评论