资讯专栏INFORMATION COLUMN

js学习之异步处理

VioletJack / 461人阅读

摘要:学习开发,无论是前端开发还是都避免不了要接触异步编程这个问题就和其它大多数以多线程同步为主的编程语言不同的主要设计是单线程异步模型。由于异步编程可以实现非阻塞的调用效果,引入异步编程自然就是顺理成章的事情了。

学习js开发,无论是前端开发还是node.js,都避免不了要接触异步编程这个问题,就和其它大多数以多线程同步为主的编程语言不同,js的主要设计是单线程异步模型。正因为js天生的与众不同,才使得它拥有一种独特的魅力,也给学习者带来了很多探索的道路。本文就从js的最初设计开始,整理一下js异步编程的发展历程。

什么是异步

在研究js异步之前,先弄清楚异步是什么。异步是和同步相对的概念,同步,指的是一个调用发起后要等待结果返回,返回时候必须拿到返回结果。而异步的调用,发起之后直接返回,返回的时候还没有结果,也不用等待结果,而调用结果是产生结果后通过被调用者通知调用者来传递的。

举个例子,A想找C,但是不知道C的电话号码,但是他有B的电话号码,于是A给B打电话询问C的电话号码,B需要查找才能知道C的电话号码,之后会出现两种场景看下面两个场景:

A不挂电话,等到B找到号码之后直接告诉A

A挂电话,B找到后再给A打电话告诉A

能感受到这两种情况是不同的吧,前一种就是同步,后一种就是异步。

为什么是异步的

先来看js的诞生,JavaScript诞生于1995年,由Brendan Eich设计,最早是在Netscape公司的浏览器上实现,用来实现在浏览器中处理简单的表单验证等用户交互。至于后来提交到ECMA,形成规范,种种历史不是这篇文章的重点,提到这些就是想说一点,js的最初设计就是为了浏览器的GUI交互。对于图形化界面处理,引入多线程势必会带来各种各样的同步问题,因此浏览器中的js被设计成单线程,还是很容易理解的。但是单线程有一个问题:一旦这个唯一的线程被阻塞就没办法工作了--这肯定是不行的。由于异步编程可以实现“非阻塞”的调用效果,引入异步编程自然就是顺理成章的事情了。

现在,js的运行环境不限于浏览器,还有node.js,node.js设计的最初想法就是设计一个完全由事件驱动,非阻塞式IO实现的服务器运行环境,因为网络IO请求是一个非常大的性能瓶颈,前期使用其他编程语言都失败了,就是因为人们固有的同步编程思想,人们更倾向于使用同步设计的API。而js由于最初设计就是全异步的,人们不会有很多不适应,加上V8高性能引擎的出现,才造就了node.js技术的产生。node.js擅长处理IO密集型业务,就得益于事件驱动,非阻塞IO的设计,而这一切都与异步编程密不可分。

js异步原理

这是一张简化的浏览器js执行流程图,nodejs和它不太一样,但是都有一个队列

这个队列就是异步队列,它是处理异步事件的核心,整个js调用时候,同步任务和其他编程语言一样,在栈中调用,一旦遇上异步任务,不立刻执行,直接把它放到异步队列里面,这样就形成了两种不同的任务。由于主线程中没有阻塞,很快就完成,栈中任务边空之后,就会有一个事件循环,把队列里面的任务一个一个取出来执行。只要主线程空闲,异步队列有任务,事件循环就会从队列中取出任务执行。

说的比较简单,js执行引擎设计比这复杂的多得多,但是在js的异步实现原理中,事件循环和异步队列是核心的内容。

异步编程实现

异步编程的代码实现,随着时间的推移也在逐渐完善,不止是在js中,许多编程语言的使用者都在寻找一种优雅的异步编程代码书写方式,下面来看js中的曾出现的几种重要的实现方式。

最经典的异步编程方式--callback

提起异步编程,不能不提的就是回调(callback)的方式了,回调方式是最传统的异步编程解决方案。首先要知道回调能解决异步问题,但是不代表使用回调就是异步任务了。下面以最常见的网络请求为例来演示callback是如何处理异步任务的,首先来看一个错误的例子:

function getData(url) {
    const data = $.get(url);
    return data;
}

const data = getData("/api/data"); // 错误,data为undefined

由于函数getData内部需要执行网络请求,无法预知结果的返回时机,直接通过同步的方式返回结果是行不通的,正确的写法是像下面这样:

function getData(url, callback) {
    $.get(url, data => {
        if (data.status === 200) {
            callback(null, data);
        } else {
            callback(data);
        }
    });
}

getData("/api/data", (err, data) => {
    if (err) {
        console.log(err);
    } else {
        console.log(data);
    }
});

callback方式利用了函数式编程的特点,把要执行的函数作为参数传入,由被调用者控制执行时机,确保能够拿到正确的结果。这种方式初看可能会有点难懂,但是熟悉函数式编程其实很简单,很好地解决了最基本的异步问题,早期异步编程只能通过这种方式。

然而这种方式会有一个致命的问题,在实际开发中,模型总不会这样简单,下面的场景是常有的事:

fun1(data => {
    // ...
    fun2(data, result => {
        // ...
        fun3(result, () => {
            // ...
        });
    });
});

整个随着系统越来越复杂,整个回调函数的层次会逐渐加深,里面再加上复杂的逻辑,代码编写维护都将变得十分困难,可读性几乎没有。这被称为毁掉地狱,一度困扰着开发者,甚至是曾经异步编程最为人诟病的地方。

从地狱中走出来--promise

使用回调函数来编程很简单,但是回调地狱实在是太可怕了,嵌套层级足够深之后绝对是维护的噩梦,而promise的出现就是解决这一问题的。promise是按照规范实现的一个对象,ES6提供了原生的实现,早期的三方实现也有很多。在此不会去讨论promise规范和实现原理,重点来看promise是如何解决异步编程的问题的。

Promise对象代表一个未完成、但预计将来会完成的操作,有三种状态:

pending:初始值,不是fulfilled,也不是rejected

resolved(也叫fulfilled):代表操作成功

rejected:代表操作失败

整个promise的状态只支持两种转换:从pending转变为resolved,或从pending转变为rejected,一旦转化发生就会保持这种状态,不可以再发生变化,状态发生变化后会触发then方法。这里比较抽象,我们直接来改造上面的例子:

function getData(url) {
    return new Promise((resolve, reject) =>{
        $.get(url, data => {
            if (data.status === 200) {
                reject(data);
            } else {
                resolve(data);
            }
        });
    });
}

getData("/api/data").then(data => {
    console.log(data);
}).catch(err => {
    console.log(err);
});

Promise是一个构造函数,它创建一个promise对象,接收一个回调函数作为参数,而回调函数又接收两个函数做参数,分别代表promise的两种状态转化。resolve回调会使promise由pending转变为resolved,而reject 回调会使promise由pending转变为rejected。

当promise变为resolved时候,then方法就会被触发,在里面可以获取到resolve的内容,then方法。而一旦promise变为rejected,就会产生一个error。无论是resolve还是reject,都会返回一个新的Promise实例,返回值将作为参数传入这个新Promise的resolve函数,这样就可以实现链式调用,对于错误的处理,系统提供了catch方法,错误会一直向后传递,总是能被下一个catch捕获。用promise可以有效地避免回调嵌套的问题,代码会变成下面的样子:

fun1().then(data => {
    // ...
    return fun2(data);
}).then(result => {
    // ...
    return fun3(result);
}).then(() => {
    // ...
});

整个调用过程变的很清晰,可维护性可扩展性都会大大增强,promise是一种非常重要的异步编程方式,它改变了以往的思维方式,也是后面新方式产生的重要基础。

转换思维--generator

promise的写法是最好的吗,链式调用相比回调函数而言却是可维护性增加了不少,但是和同步编程相比,异步看起来不是那么和谐,而generator的出现带来了另一种思路。

generator是ES对协程的实现,协程指的是函数并不是整个执行下去的,一个函数执行到一半可以移交执行权,等到可以的时候再获得执行权,这种方式最大的特点就是同步的思维,除了控制执行的yield命令之外,整体看起来和同步编程感觉几乎一样,下面来看一下这种方式的写法:

function getDataPromise(url) {
    return new Promise((resolve, reject) =>{
        $.get(url, data => {
            if (data.status === 200) {
                reject(data);
            } else {
                resolve(data);
            }
        });
    });
}

function *getDataGen(url) {
    yield getDataPromise(url);
}

const g = getDataGen("/api/data");
g.next();

generator与普通函数的区别就是前面多一个*,不过这不是重点,重点是generator里面可以使用yield关键字来表示暂停,它接收一个promise对象,返回promise的结果并且停在此处等待,不是一次性执行完。generator执行后会返回一个iterator,iterator里面有一个next方法,每次调用next方法,generator都会向下执行,直到遇上yield,返回结果是一个对象,里面有一个value属性,值为当前yield返回结果,done属性代表整个generator是否执行完毕。generator的出现使得像同步一样编写异步代码成为可能,下面是使用generator改造后的结果:

* fun() {
    const data = yield fun1();
    // ...
    const result = yield fun2(data);
    // ...
    yield fun3(result);
    // ...
}

const g = fun();
g.next();
g.next();
g.next();
g.next();

在generator的编写过程中,我们还需要手动控制执行过程,而实际上这是可以自动实现的,接下来的一种新语法的产生使得异步编程真的和同步一样容易了。

新时代的写法--async,await

异步编程的最高境界,就是根本不用关心它是不是异步。在最新的ES中,终于有了这种激动人心的语法了。async函数的写法和generator几乎相同,把*换成async关键字,把yield换成await即可。async函数内部自带generator执行器,我们不再需要手动控制执行了,现在来看最终的写法:

function getDataPromise(url) {
    return new Promise((resolve, reject) =>{
        $.get(url, data => {
            if (data.status === 200) {
                reject(data);
            } else {
                resolve(data);
            }
        });
    });
}

async function getData(url) {
    return await getDataPromise(url);
}

const data = await getData(url);

除了多了关键字,剩下的和同步的编码方式完全相同,对于异常捕获也可以采取同步的try-catch方式,对于再复杂的场景也不会逻辑混乱了:

* fun() {
    const data = await fun1();
    // ...
    const result = await fun2(data);
    // ...
    return await fun3(result);
    // ...
}
fun()

现在回去看回调函数的写法,感觉好像换了一个世界。这种语法比较新,在不支持的环境要使用babel转译。

写在最后

在js中,异步编程是一个长久的话题,很庆幸现在有这么好用的async和await,不过promise原理,回调函数都是要懂的,很重要的内容,弄清楚异步编程模式,算是扫清了学习js尤其是node.js路上最大的障碍了。

尊重原创,转载分享前请先知悉作者,也欢迎指出错误不足共同交流,更多内容欢迎关注作者博客点击这里

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92607.html

相关文章

  • Java基础习之AJAX技术简单

    摘要:是与服务器交换数据并更新部分网页的艺术,在不重新加载整个页面的情况下。对象是的核心,所有现代浏览器均支持对象和使用。用于在后台与服务器交换数据。及时有效地帮助学员解决疑难问题,提高学员的学习积极性。   Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。  AJAX...

    番茄西红柿 评论0 收藏2637
  • 区块链习之以太坊(七)

    摘要:基于以太坊项目,以太坊团队目前运营了一个公开的区块链平台以太坊网络。主要特点以太坊区块链底层也是一个类似比特币网络的网络平台,智能合约运行在网络中的以太坊虚拟机里。以太坊采用交易作为执行操作的最小单位。 以太坊将比特币针对数字交易的功能进一步进行了拓展,面向更为复杂和灵活的应用场景,支持了智能合约这一重要特性。 以太坊项目简介 以太坊:项目最初的目标是打造以个智能合约的平台,该平台支持...

    xiongzenghui 评论0 收藏0
  • Zepto.js源码习之

    摘要:本次主要分享关于上一篇区域的学习。区域为的核心部分,它的结构如下为了便于梳理思路,以上代码省略了细节,只保留了轮廓脉络。最终暴露给开发者的如下图所示这里只分析了区域的结构,下一次会深入到函数语句粒度。 本次主要分享关于上一篇区域2的学习。区域2为Zepto的核心部分,它的结构如下 var Zepto = (function() { var $, zepto = {}; fu...

    kel 评论0 收藏0
  • JS数组习之splice方法

    摘要:确认需要移除的长度,最小为,确认原数组变化后的新长度按照索引位置,获取长度的新数组,用于返回对原数组进行替换新元素。 array.splice(start,deleteCount,item1,item2....) splice方法从array移除n个元素(大于或等于0),并且可以用新的item替换被移除的元素。参数start是从数组array中移除元素的最开始位置(数组的索引,正负数表...

    AbnerMing 评论0 收藏0

发表评论

0条评论

VioletJack

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<