nodejs中的事件发布和promise

异步编程解决方案

上篇中涉及了异步编程的问题。异常处理 函数嵌套深 阻塞代码 多线程编程 异步转同步
存在如下的解决方案

事件发布/订阅模式[发布/订阅模式]

事件监听器模式是一个应用于异步编程的模式。是回调函数的事件化,称为发布订阅模式
node自身提供的events模块是发布订阅的一个简单实现。node中的部分模块都继承于它。比前端的大量dom事件简单。不存在事件冒泡,也不存在preventDefault stopPropagation stopImmediatePropagation等事件处理方法。具有监听addListener/on() once() removeListener removeAllListeners emit等基本的事件监听模式方法的实现。事件发布订阅实例代码如下

emitter.on("event1", function(message) {
    console.log(message)
})
emitter.emit("event1", "message")

事件订阅为高阶函数的应用[高阶函数是把函数作为参数,将函数作为返回值的函数],通过发布订阅模式实现一个事件和多个回调函数的非关联。这些回调函数又叫做事件侦听器,侦听器可以灵活的添加和删除。使得事件和具体处理逻辑之间很轻松的关联和解耦
emit()多半是伴随事件循环而异步触发的。事件发布订阅广泛应用于异步编程
事件发布者不许关注订阅的侦听器如何实现业务逻辑,不需要关注有多少个侦听器存在,数据通过消息的方式可以很灵活的传递。在一些典型场景中,可以通过事件发布订阅模式进行组件封装。
事件侦听是一个钩子机制,利用钩子导出内部数据或者状态给外部的调用者。

var options = {
    host: 'www',
    prot: 80,
    path: '/',
    method: 'POST'
}
var req = http.request(options, function(res) {
    console.log(res.statusCode)
    res.setEncoding('utf8')
    res.on('data', function(chunk) {
        console.log(chunk)
    }) 
})

在http的请求代码中,只需要将视线放在error, data, end这些业务上。

  • 如果一个事件添加了10个侦听器,会收到警告。侦听器太多会造成内存泄露。调用emitter.setMaxListeners(0)可以将这个限制去掉。
  • 为了处理异常,EventEmitter对象对error事件进行了特殊处理。如果在运行期间触发了error事件,eventEmitter会检查是否对error事件添加过监听器,如果添加了,这个错误会有侦听器进行处理。这个错误称为异常抛出。没有捕获这个异常。会引起线程退出。

EventEmitter

继承events模块

实现一个继承EventEmitter的类是十分简单的。

var events = require('events')
function Stream() {
    events.EventEmitter.call(this)
}
util.inherits(Stream, events.EventEmitter)

node在util模块中实现了继承的方式。可以很方便的调用,开发者可以通过这样的方式继承EventEmitter类。利用事件机制解决业务问题。

利用事件队列解决雪崩问题

在事件订阅发布模式中,通常有一个once方法,通过这个api添加的侦听器只能执行一次。在执行之后,就将它和事件关联接触。可以过滤一些重复性的事件响应。

var status = "ready"
var select = function(callback) {
    if (status === "ready") {
        status = 'pending'
        db.select("SQL", function(results) {
            status = "ready"
            callback(results)
        })
    }
}

但是在这种情况下,多次调用select,只有第一次调用是起作用的,后续的select是没有数据服务的。

var proxy = new events.EventEmitter()
var status = "ready"
varr select = function(callback) {
    proxy.once("selected", callback)
    if (status === "ready") {
        status = "pending"
        db.select("SQL", function (result) {
            proxy.emit("selected",results)
            status = "ready"
        })
    } 
}

使用once,所有的请求被压入事件队列中,利用执行一次就会将监视器移除的特点,每次回调只会被执行一次。对于相同的sql语句,保证在每次查询从开始到结束的过程只发生一次。不同的sql,只会在队列中等待数据库操作,可以很节省重复的数据库的开销。使用node单线程,无需担心状态同步。

多异步之间的协作方案

事件发布和订阅模式利用高阶函数的有限,侦听器作为回调函数可以随意添加和删除。帮助开发者处理可以添加的业务逻辑。 一般而言,事件和侦听器的关系是一对多。也会出现事件和侦听器是多对一的情况。

var count = 0
var results = {}
var done = function(key, value) {
    results[key] = value
    count++
    if(count === 3) {
        render(results)
    }
} 
fs.readFile(template_path, 'utf-8', function(err, template) {
    done("template", template)
})
db.query(sql, function(err, data) {
    done("data", data)
})

多个异步场景不能保证顺序,而且回调函数之间没有交集,需要借助一个第三方函数和第三方变量来处理异步写作的结果。

var after = function(times, callback) {
    var count = 0, results = {}
    return funtion(key, value) {
        results[key] = value
        count++
        if(count === times) {
            callback(results)
        }
    }
}
var done = after(times, render)
var emitter = new events.Emitter();
var done = after(times, render);
emitter.on("done", done);
emitter.on("done", other);
fs.readFile(template_path, "utf8", function (err, template) {
emitter.emit("done", "template", template);
});
db.query(sql, function (err, data) {
emitter.emit("done", "data", data);
});
l10n.get(function (err, resources) {
emitter.emit("done", "resources", resources);
});

这种方案结合了前者用简单的偏函数完成多对一的收敛和事件订阅发布模式中的一对多的发散

EventProxy模块
var proxy = new EventProxy()
proxy.all('template','data','resources',function(template, data, resources) {
    // todo
})
fs.readFile(template_path, 'utf8', function(err, template) {
    proxy.emit('template', template)
})
db.query(sql, function(err,data) {
    proxy.emit('data', data)
})
lion.get(function(err, resources) {
    proxy.emit("resources", resources)
})

EventProxy提供了一个all方法来订阅这个事件,当每个事件都被出发后,侦听器才会执行。tail()方法, 和all()方法的区别就是,all()在满足条件之后会触发一次,tail()方法在满足条件执行一次后,如果组合事件再次被触发,侦听器会继续执行。
all()方法带来的改进: 在侦听器中返回的数据的参数列表和订阅组合事件列表是一致对应的。

EventProxy原理

EventProxy 来自于Backbone的事件模块[backbone的事件模块是model,view的基础功能,在前端中有广泛的引用]。每个非all事件触发一次,都会触发一次all事件。EventProxy是将all作为一个事件流的拦截层,在其中注入业务来完成单一事件无法解决的异步问题,

EventProxy异常处理

在eventProxy中的异常处理模块

exports.getContent = function(callback) {
    var ep = new EventProxy()
    ep.all('tpl','data',function(tpl, data) {
        callback(null, {
            template: tpl,
            data: data
        })
    })
eq.fail(callback)
fs.readFile('template.tpl', 'utf-8', ep.done('tpl'))
db.get('some sql', ep.done('data'))
}

在上述代码中,eventproxy提供了file和done()这两个实例方法来优化异常处理
// fail

// 1
ep.fail(function (err) {
    callback(err)
})
// 2
ep.fail(callback)
// 3
ep.bind('error', function(error) {
    ep.unbind()
    callback(err)
})

promise/deferred模式

使用事件的方式,执行流程需要被预先设定。这是由发布订阅的运行机制构成的。例如下面代码块

$.get('/', {
    success: onSuccess,
    error: onError,
    complete: onComplete
})

Promise/Deffered模式的应用逐渐增多,CommonJS草案出现了Promise/A, Promise/B, Promise/D这些典型的异步调用的Promise/Deferred

Promise/A

Promise/Deferred模式包含PromiseDeferred

  • Promise/A操作只会处在三种状态中的一种: 未完成态,完成态和失败态。
  • Promise的转换只会出现从未完成态到完成态的转换。完成态和失败态不能相互转换
  • Promise的转换一旦完成,不能被更改。
    在这里插入图片描述

对于API的定义。Promise/A的提议是简单的。一个Promise对象具有then方法即可。

  • 接受完成态,错误态的回调方法,在操作完成或者出错的时候,会调用相对应的方法
  • 可选的支持progress事件回调作为第三个方法
  • then()方法只能接受function对象,其他对象会被忽略
  • then()方法继续返回Promise对象,实现链式调用
    then(fulfilledHandler, errorHandler, progressHandler)
    代码演示
var Promise = function() {
    EventEmitter.call(this)
}
util.inherits(Promise, EventEmitter)
Promise.prototype.then = function(fulfilledHandler, errorHandler, progressHandler) {
    if (typeof fulfilledHandler === 'function') {
        this.once('success', fulfilledHander)
    }
    if (typeof errorHandler === 'function') {
        this.once('error', errorHandler )
    }
    if (typeof progressHandler === 'function') {
        this.on('progress', progressHandler)
    }
    return  this
}

这里的then的方法所作的事情是将回调函数存储起来,完成这些流程,还需要触发这些回调的地方,实现这些功能的功能叫做Deferred,就是延迟对象

var Deferred = function () {
    this.state = 'unfulfilled'
    this.promise = new Promise()
}
Deferred.prototype.resolve =  function (obj) {
    this.state = 'fulfilled'
    this.promise.emit('success', obj)
}
Deferred.prototype.reject = function(err) {
    this.state = 'failed'
    this.promise.emit('error', err)
}
Deferred.prototype.progress = function (data) {
    this.promise.emit('progress', data)
}

在这里插入图片描述

利用promise/A的模式,对响应对象进行封装。

res.setEncoding('utf-8')
res.on('data', function(chunk) {
    console.log('body', chunk)
})
res.on('end', function() {
    
})
res.on('error', function(err) {

})

上述代码等价于

var promiseify = function(res) {
    var deferred = new Deferred() 
    var result = ''
    res.on('data', function(chunk) {
        result += chunk
        deferred.resolve(result)
    })
    res.on('end', function() {
        deferred.resolve(result)
    })
    res.on('error',function() {
        deferred.reject(err)
    })
    return deferred.promise
}

调用

res.then(function () {

},function(err) {
}, function(chunk) {
})

Deferred主要是作用于内部,用于维护异步模型的状态,Promise用于外部,通过then方法暴露给外部增加自定义逻辑。
在这里插入图片描述

Q模块是promise/A规范的一个实现

// 高阶函数的使用。makeNodeResolve返回了一个Node风格的回调函数
defer.prototypee.makeNodeResolver = function() {
    var self = this
    return function(error, value) {
        if (error) {
            self.reject(error)
        } else if (arguments.length > 2) {
            self.resolve(array_slice(arguments, 1))
        } else {
            self.resolve(value)
        }
    }
}
var readFile = function(file, encoding) {
    var readFIle = Q.defer()
    fs.readFile(file, encoding, deferred.makeNodeResolver())
    return deferred.promise
} 

调用

readFile("foo.txt", "utf-8").then(function(data) {
    // success
}, function(err) {
    // failed
})

Promise通过封装异步调用,实现了正向用例和反向用例的分离以及逻辑处理延迟。使得回调函数相对优雅。

Promise中的多异步协作

在promise的介绍中说过。主要的解决的是单个异步操作中存在的问题。下面是处理多个异步调用的代码

Deferred.prototype.all = function(promises) {
    var count = promises.length;
    var that = this
    var results = []
    promises.forEach(function(promise, i) {
    promise.then(function (data) {
        count--
        results[i] = data
        if (count === 0) {
            that.resolve(results)
        }
    }, function(err) {
        that.reject(err)
    })
})
return this.promise
}

对于多次文件的读取场景,all()方法将两个单独的Promise重新抽象组合成一个新的Promise

var promise1 = readFile('foo.txt', 'utf-8')
var promise2 = readFile('bar.txt', 'utf-8')
var deferred = new Deferred()
deferred.all([promise1, promise2]).then(function(results) {
    // todo
}, function(err) {
    // todo 
})

这里的all()方法抽象多个异步操作。只有所有的异步操作成功。这个异步操作才算成功。一旦其中的一个异步操作失败。整个异步操作就会失败

Promise的进阶知识

在api的暴露上,Promise模式比原始的事件侦听和触发更加优美,缺点是需要为不同的场景封装不同的api。
回调函数的层层嵌套,就叫做回调地狱。回调地狱会造成代码可复用性不强,可阅读性差,可维护性(迭代性差),扩展性差等等问题。
为了解决回调地狱,可以使用支持序列执行的promise

promise()
    .then(obj.api1)
    .then(obj.api2)
    .then(obj.api3)
    .then(obj.api4)
var Deferred = function() {
    this.promise =new Promise()
}
Deferred.prototype.resolve = function(obj) {
    var promise = this.promise
    var handler
    while((handler = promise.queue.shift())) {
        if (handler && handler.fulfilled) {
            var ret = handler.fulfilled(obj)
            if (ret && ret.isPromise) {
                ret.queue = promise.queue
                this.promise = ret
                return 
            }
        }
    }
}
Deferred.prototype.reject = function(err) {
    var promise = this.promise 
    var handler 
    while((handler = promise.queue.shift())) {
        var ret = handler.error(err)
        if (ret && ret.isPromsie) {
            ret.queue = promise.queue;
            this.promise = ret
            return
        }
    }
}
Deferred.prototype.callback = function() {
    var that = this
    return function(err, file) {
        if (err) {
            reteurn that.reject(err)
        }
        that.resolve(file)
    }
}
var Promise = function() {
    this.queue = []
    this.isPromise = true
}
Promise.prototype.then = function(fulfilledHander, errorHandler, progressHandler) {
    var handler = {}
    if (typeof fulfilledHandler === 'function') {
        handler.fulfilled = fulfilledHandler
    }
    if (typeof errorHandler === 'function') {
        handler.error = errorHandler
    }
    this.queue.push(handler)
    return this
}

这里以文件读取作为例子

var readFile1 = function(file, encoding) {
    var deferred = new Deferred()
    fs.readFile(file, encodeing ,deferred.callback())
    return deferred.promise
}
var readFile2 = function(life, encoding) {
    var deferred = new Deferred()
    fs.readFile(file, encoding, deferred.callback())
    return deferred.promise
}
readFile('file1.txt', 'utf-8').then(function(file1) {
    return readFile2(file1.trim(), 'utf-8')
}).then(function (file2){
    console.log(file2)
})

promise链式执行。主要通过下面两个步骤

  1. 将所有的回调存储到队列中
  2. promise完成的时候,每个都进行回调,一旦检测到了返回的新的promise对象,停止执行,然后将当前的deferred对象的promise引用改变为新的promise对象。并在队列中余下的回调转交给它
    批量将API Promise化
var smooth = function(method) {
    return function (method) {
        var deferred = new Deferred()
        var args = Array.prototype.slice.call(arguments,1)
        args.push(deferred.callback())
        method.apply(null, args)
        return deferred.promise
    }
}

文件读取操作代码可以改变为

var readFile1 = function(file, encoding) {
    var deffered = new Deffered()
    fs.readFile(file, encoding, deferred.callback())
    return deferred.promise
}
var readFile = smooth(fs.readFile)

就是将上面的代码封装了一下

  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值