JS异步处理三强争霸赛

1,272 阅读5分钟

前言

众所周知 Javascript 是单线程模型语言,同时只能执行一个任务,其他任务都必须在后面排队等待。

因此,异步处理就成为 Javascript 处理多任务时提升效率最重要的方式之一,这也是 Javascript 区别于其他语言的重要特征。

本文将通过对比三种目前最流行的异步处理方式,让读者深刻体会不同的异步处理方式的优缺点,进一步感受 Javascript 异步处理的独特魅力。

三强选手

1. callback

首先登场的是 callback(回调函数),它是异步操作最基本的方法。

callback 的具体介绍如下。

下面是两个函数f1f2,编程的意图是f2必须等到f1执行完成,才能执行。

function f1() {
  // ...
}

function f2() {
  // ...
}

f1();
f2();

上面代码的问题在于,如果f1是异步操作,f2会立即执行,不会等到f1结束再执行。

这时,可以考虑改写f1,把f2写成f1的回调函数。

function f1(callback) {
  // ...
  callback();
}

function f2() {
  // ...
}

f1(f2);

回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。

2. Promise

二号选手是 Promise,它是异步编程的一种新的解决方案。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

下面是一个Promise对象的简单例子。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});

Promise解决了callbak回调地狱的问题,异步处理的表达流程也更加清晰,但是多层嵌套带来的繁琐写法,也是困扰Promise的难点。

3. async

最后一位选手是asyncasync 函数是什么?一句话,它就是 Generator 函数的语法糖。

具体代码展示如下。

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

async函数的最大特点,就是可以将异步处理编写成同步处理,可读性是最强的。相应的,它对开发者的理解难度和技能要求也是最高的。

比赛规则

1. 比赛内容

通过js代码依次读取files文件夹中的三个文件——a.jsonb.jsonc.json的内容。

a.json的内容如下:

{
    "next":"b.json",
    "msg":"this is a"
}

b.json的内容如下:

{
    "next":"c.json",
    "msg":"this is b"
}

c.json的内容如下:

{
    "next": null,
    "msg":"this is c"
}

首先读取a.json的内容,然后获取next再读取b.json,最后读取b.jsonnext获取c.json,这是最典型的js异步处理的问题之一。

2. 比赛环境

Node.js是服务器端运行Javascript的重要环境,可以为js提供读取文件的接口,因此,本次比赛也将在Node.js(8.0以上)的环境下进行。

Node.js提供的比赛工具如下:

const fs = require('fs') // 文件读写接口,获取json文件的内容
const path = require('path') // 文件路径接口,获取files文件夹里面三个文件的文件路径

比赛结果

1. callback

首先是callback登场,它给出的解决方式如下:

// callback 方式获取一个文件的内容
function getFileContentByCallback(fileName, callback) {
    // path 获取文件路径
    const fullFileName = path.resolve(__dirname, 'files', fileName)
    // fs 读取文件
    fs.readFile(fullFileName, (err, data) => {
        if (err) {
            console.error(err)
            return
        }
        callback(
            JSON.parse(data.toString())
        )
    })
}

// 读取文件
getFileContentByCallback('a.json', aData => {
    console.log('a data callback: ', aData)
    getFileContentByCallback(aData.next, bData => {
        console.log('b data callback: ', bData)
        getFileContentByCallback(bData.next, cData => {
            console.log('c data callback: ', cData)
        })
    })
})

// 输出结果

a data callback:  { next: 'b.json', msg: 'this is a' }
b data callback:  { next: 'c.json', msg: 'this is b' }
c data callback:  { next: null, msg: 'this is c' }

2. promise

接下来是promise登场,它的解决方式如下:

// promise 方式获取一个文件的内容
function getFileContentByPromise(fileName) {
    const promise = new Promise((resolve, reject) => {
        // path 获取文件路径
        const fullFileName = path.resolve(__dirname, 'files', fileName)
        // fs 读取文件
        fs.readFile(fullFileName, (err, data) => {
            if (err) {
                reject(err)
                return
            }
            resolve(
                JSON.parse(data.toString())
            )
        })
    })
    return promise
}

// 读取文件
getFileContentByPromise('a.json').then(aData => {
    console.log('a data promise: ', aData)
    return getFileContentByPromise(aData.next)
}).then(bData => {
    console.log('b data promise: ', bData)
    return getFileContentByPromise(bData.next)
}).then(cData => {
    console.log('c data promise: ', cData)
})


// 输出结果

a data promise:  { next: 'b.json', msg: 'this is a' }
b data promise:  { next: 'c.json', msg: 'this is b' }
c data promise:  { next: null, msg: 'this is c' }

3. async

最后登场的是async,它的解决方式如下:

// async 方式获取一个文件的内容
async function getFileContentByAsync(fileName) {
    try {
        // getFileContentByPromise 是上面promise获取文件内容的处理函数,aysnc直接调用
        const aData = await getFileContentByPromise(fileName) 
        const bData = await getFileContentByPromise(aData.next)
        const cData = await getFileContentByPromise(bData.next)
        
        console.log('a data async: ', aData)
        console.log('b data async: ', bData)
        console.log('c data async: ', cData)
    } catch (error) {
        console.error(error)
    }
}

// 读取文件
getFileContentByAsync('a.json')

// 输出结果
a data async:  { next: 'b.json', msg: 'this is a' }
b data async:  { next: 'c.json', msg: 'this is b' }
c data async:  { next: null, msg: 'this is c' }

比赛总结

从比赛结果来看,它们都可以顺利完成任务,从实现方式来看,却各不相同。

本次比赛并非比较三者到底谁更强,而是通过最直观的对比方式,让大家感受js异步处理的多样性,以及每种异步处理的特点和差异性。

callback利于理解、学习成本低,Promise承上启下、中流砥柱,async集大成者、代表未来。

最后附上比赛的GitHub地址:github.com/jiangjiahen…

祝工作顺利,生活幸福。