nodejs异步处理

我们先来看一个例子:

 

const fs = require('fs')
fs.readFile('a.txt', 'utf8', function(err, data) {
  console.log(data)
})

这是一个从文件中读取文件的代码,fs.readFile的第三个参数是个回调函数,当文件读取成功后,会调用改回调。

为什么会有回调函数

主要原因是因为js是单线程的,之所以是单线程,与它的用途有关。

js创立之初是作为浏览器脚本语言,主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定js同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,js就是单线程,这已经成了这门语言的核心特征,目前来看,这个特征将不会改变。

事件队列

既然是单线程,就会遇到一个问题,来看下一般的编程语言处理文件读取的方式(伪代码):

 

// 声明文件名
const filename = 'a.txt'
// 获取文件内容
const content = readFile(filename)
// 把文件内容按照空格分割成数组
const arr = content.split(' ')
// ......

当程序读取文件的时候,会进行一个漫长的IO操作,此时改线程处于等待状态,等待文件内容的返回,这导致我们程序的执行效率不高(因为有个等的过程)。

js采用了更加有效率的方式,使用了事件队列:

js的主线程在执行的时候,一旦发生了异步处理(文件读写、网络请求、定时任务等),一方面,js会请求操作系统让相关的部件(比如磁盘或者网卡等)处理这些异步事件,同时把这些异步处理包装成一个事件对象,放置到一个队列中(事件队列),然后继续执行后面的代码。
当主线程中的所有同步代码执行完毕后,js就会对事件队列进行循环检测,一旦某个事件被触发(比如网络请求返回数据了),js就会调用相应的事件对象里的处理函数,这个处理函数,就是回调函数。

注意,事件队列当中的事件,即使已经收到了事件完成的通知,也必须在js的主程序完成之后,才有机会被执行。

回到最开始的回调函数问题上,这里的回调函数,指的就是当事件完成时,需要执行的处理函数。

回调地狱

我们来考虑这样的一个例子,依次从a.txtb.txtc.txt读取内容,并拼接在一起,要求串行:

 

const fs = require('fs')
fs.readFile('a.txt', 'utf8', function(err, dataa) {
  fs.readFile('b.txt', 'utf8', function(err, datab) {
    fs.readFile('c.txt', 'utf8', functioin(err, datac){
      console.log(`${dataa}${datab}${datac}`)
    })
  })
})

可以看到,js的这种回调方式,对于处理异步的串行操作是很不优雅的,串行的异步越多,调用嵌套的越深,这种情况,我们称之为:回调地狱。

promise

Promise是异步编程的一种解决方案,可以帮助我们摆脱回调地狱的问题,Promise最早是由技术社区里面的一些散户提出并实现,后来官方觉得这个东西很不错,就把它纳入es6的标准中。

promise的代码风格如:

 

const fs = require('fs')
const readfilePromise = new Promise((resolve, reject) => {
    fs.readFile('a.txt', 'utf8', function(err, data){
        if (err) return reject(err)
        resolve(data)
    })
})

readfilePromise.then(data => console.log(data), err => console.log(err))

Promise可以认为是一个状态管理器,它有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)状态切换的时机需要我们在构造的时候指定。

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject,它们是两个函数,由js引擎内部提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从pending 变为resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数,并且回调函数分别可以拿到resolverejected传入的参数。

了解了promise之后,我们来写一个Promise版本的文件串行读取的实现:

 

// 依次从`a.txt`、`b.txt`、`c.txt`中读取文件
const fs = require('fs')
const aPromise = new Promise((resolve, reject) => {
    fs.readFile('a.txt', 'utf8', function(err, data){
        if (err) return reject(err)
        resolve(data)
    })
})

const bPromise = new Promise((resolve, reject) => {
    fs.readFile('b.txt', 'utf8', function(err, data){
        if (err) return reject(err)
        resolve(data)
    })
})

const cPromise = new Promise((resolve, reject) => {
    fs.readFile('c.txt', 'utf8', function(err, data){
        if (err) return reject(err)
        resolve(data)
    })
})

let content = ''
aPromise
    .then(aContent => {
        content += aContent
        return bPromise
    })
    .then(bContent => {
        content += bContent
        return cPromise
    })
    .then(cContent => {
        console.log(content + cContent)
    })

再来看一个关于Promise的面试题目

 

// 请写出下面这段代码打印出来的内容
console.log(1)
const p = new Promise((resolve, reject) => {
    console.log(2)
    setTimeout(() => console.log(3), 0)
    resolve(4)
})
p.then(v => console.log(v))
console.log(5)

promise实现原理

promise的实现原理是:

promise的then会被同步执行,then中会将参数(onFulfilledonRejected)包装成一个handler,放到一个队列当中,当promise构造中的resolvereject被调用的时候,会使用参数将队列里面的所有方法依次调用。

因此,我们来考虑一下下面的代码实际调用的流程:

 

var p = new Promise((resolve, reject) => {
    resolve(3) // 这里是一定会异步操作
})

p.then(v => console.log(v))
p.then(v => console.log(v))
p.then(v => console.log(v))
console.log(4)

协程

在操作系统当中有两种概念,一个是进程,一个是线程,进程拥有独立的资源空间,也就是各个进程之间不共享资源,操作系统为了能够让多个进程“同时”运行,采用了时间片轮换机制,CPU某一时刻属于一个进程,下一时刻就切换到另外一个进程,当时间片足够短的时候,我们就可以感觉到,多个进程同时运行。

因为进程之间不共享资源,切换的代价就会比较的高,所以后来就设计了线程,一个进程可以含有多个线程,各个线程共享一个进程中的资源,线程的切换只是切换CPU中的执行代码,运行时所需要的资源几乎可以不用切换,大大提高了切换效率。

其实线程也有自己独立的资源,比如运行时创建的堆栈信息等,一般线程初始化的时候,会预分配1M左右的空间,用于存放这些资源,所以切换依然是有成本的。

目前比较流行的一种更加高效的方式是协程,协程是以多占用内存为代价,实现多任务的并行的,协程有多线程版本的,也有单线程版本的,以nodejs为例来说(js是单线程的),nodjes中的协程其实是多个可以并行执行的函数的协作。

怎么来理解这句话呢?传统的程序执行,采用堆栈式的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数,执行信息保存在一个堆栈当中。

协程其实是突破了堆栈数的限制,主函数一个堆栈,每个异步的回调也有自己的堆栈,当程序执行发生异步的时候,执行权限可以切换给其他函数,因为堆栈信息一直保留在运行环境中(没有被切换出去),所以切换成本非常小,一般是直接修改执行函数的引用就可以完成切换。

nodejs协程的缺点

线程是基于时间片的,也就是说一个线程卡死,不会影响其他线程的执行,但是nodejs协程不同,一个协程卡死,将导致执行权限无法释放,导致其他的协程也无法执行。

go语言的协程

nodejs因为是单线程的,协程无法使用多核,也就是说,无法真正的并发执行多个函数。

go语言设计了多核版本的协程,也就是说,同一时间,多个协程可以被多个CPU真正的并发执行,或者你可以开多个线程,让多个线程去调度协程序,避免程序假死的问题。

因为go的优秀的协程处理机制,区块链优先采用了go作为开发语言。

async

es6中,为了实现协程,提供了Generator函数和yield关键字,但是语法比较繁琐,后来对其进行了包装,变成了async函数和await关键字,使用async函数来实现串行文件读取操作:

 

const fs = require('fs')

const readFile = (fileName, encoding) => {
    return new Promise((resolve, reject) => {
        fs.readFile('a.txt', 'utf8', function(err, data){
            if (err) return reject(err)
            resolve(data)
        })
    })
}

async function readABC(encoding = 'utf8') {
    const contenta = await readFile('a.txt', encoding)
    const contentb = await readFile('b.txt', encoding)
    const contentc = await readFile('c.txt', encoding)
    return `${contenta}${contentb}${contentc}`
}



作者:张柳哥
链接:https://www.jianshu.com/p/600e34931f2a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值