JavaScript异步调用的发展历程

简介: 通常,代码是由上而下依次执行的。如果有多个任务,就必须排队,前一个任务完成,后一个任务才能执行。这种连续的执行模式就叫做同步。

同步与异步

  • 通常,代码是由上而下依次执行的。如果有多个任务,就必须排队,前一个任务完成,后一个任务才能执行。这种连续的执行模式就叫做同步。
a();
b();
c();
复制代码

上面代码中,a、b、c是三个不同的函数,每个函数都是一个不相关的任务。在同步模式会先执行 a 任务,再执行 b 任务,最后执行 c 任务。当b任务是一个耗时很长的请求时,而c任务是展现新页面时,就会导致网页卡顿。

  • 所谓异步,就是一个任务不是连续完成的。比如,有一个读取文件处理的任务,任务的第一段的向操作系统发出请求,要求读取文件,然后程序执行其他任务,等到操作系统返回文件,再去处理文件。这种不连续的执行模式就叫做异步。
a();
//立即发送请求
ajax('url',(b)=>{
    //请求回来执行
});
c();
复制代码

上面代码中,就是将b任务分成了两部分。一部分立即执行,另一部分再请求回来后执行。也就解决了上面的问题。

总结: 同步就是大家排队工作,异步就是大家同时工作。

异步的解决方案

1、CallBack

CallBack,即回调函数,回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。即异步操作执行完成后触发执行的函数。

//当请求完成时就会触发回调函数
$.get('url',callback);
复制代码

回调可以完成异步操作,但用过 jquery 的 PE 都对下面的代码折磨过。

$.ajax({
    url:"type",
    data:1,
    success:function(a){
        url:"list",
        data:a,
        success:function(b){
            $.ajax({
                url:"content",
                data:b,
                success:function(c){
                    console.log(c);
                }
            })
        }
    }
})
复制代码

上面代码就是传说中的回调金字塔,又叫回调地狱。这里还只是单纯的嵌套代码,如若再加上业务代码,代码可读性可想而知。自己开发起来还好,如果这是别人的代码,你要改其中一部分足以让人疯掉。

2、事件发布订阅

我们想读取两个文件时,希望这两个文件都被读取完后,拿到结果。我们可以通过 node 中的 EventEmitter 类来实现,它有两个核心方法,一个是 on(表示注册监听事件),一个是emit(表示发射事件)。

let fs=require('fs');
let EventEmitter=require('event');
let eve=new EventEmitter();
let arr=[];//存储读取内容
//监听数据获取成功事件,然后调用回调函数
eve.on('ready',function(d){
    arr.push(d);
    if(arr.length==2){
        //两个文件的数据
        console.log(arr);
    }
});
fs.readFile('./a.txt','utf8',function(err,data){
    eve.emit('ready',data);
});
fs.readFile('./b.txt','utf8',function(err,data){
    eve.emit('ready',data);
});
复制代码

请求 a.txt 和 b.txt 文件数据,当成功后发布ready事件。on 订阅了 ready 事件,当监听到触发的时候,on 方法执行。

哨兵变量

let fs=require('fs');
function after(times,callback){
    let arr=[];
    return function(d){
        arr.push(d);
        if(arr.length==times){
            callback(arr);
        }
    }
}
//2是一个哨兵变量,将读取文件数据成功后执行的方法作为回调函数传给after方法
let out=after(2,function(data){
    console.log(data);
})
fs.readFile('./a.txt','utf8',function(err,data){
    out(data);
});
fs.readFile('./b.txt','utf8',function(err,data){
    out(data);
});
复制代码

上面代码 after 方法执行时传入的 2 相当于一个哨兵变量,需要读取几个文件的数据就传几。将需要读取的文件数量,和读取全部文件成功后的方法作为回调函数传入 after。out 为 after 执行后返回的函数,每次获取文件成功后执行 out 方法可以后去到最终全部文件的数据。

不使用回调地狱遇到的问题是:不知道读取文件的函数什么时候执行完。只有当全部读取完成后才能执行需要文件数据的方法。

3、Generator函数

Generator 函数要用*号来标识,yield 关键字表示暂停执行的标记。Generator 函数是一个状态机,封装了多个内部状态。调用一次 next 就会继续向下执行,返回的结果是一个迭代器,所以 Generator 函数还是一个遍历器对象生成函数。

function* read(){
    let a=yield '123';
    console.log(a);
    let b=yield 4;
    console.log(b);
}
let it = read();
console.log(it.next('321')); // {value:'123',done:false}
console.log(it.next(1)); // 1 {value:4,done:false}
console.log(it.next(2)); // 2 {value:2,done:true}
console.log(it.next(3)); // {value:undefined,done:true}
复制代码

上面代码可以看出,调用 Generator 函数后,返回的不是函数运行的结果,而是一个指向内部状态的指针对象,也就是遍历器对象。必须调用遍历器对象的 next 方法,让指针移动的下一个状态。内部指针就会从函数开始或上次定下来的地方开始执行,直到遇到下一个 yield 语句或 return 语句为止。value 属性表示当前的内部状态值,是 yield 语句后面那个表达值;done 属性是一个布尔值,表示是否遍历结束。

4、Promise

在 JavaScript 的异步发展史中,出现了一系列解决 callback 弊端的库,而 promise 成为了胜者,并成功地被加入了ES6标准。Promise 函数接受一个函数作为参数,该函数有两个参数 resolve 和 reject。promise 就像一个中介,而它只返回可信的信息给 callback,所以 callback 一定会被异步调用,且只会被调用一次。

let p=new Promise((resolve,reject)=>{
    //to do...
    if(/*异步操作成功*/){
        resolve(value);
    }else{
        reject(err);
    }
});

p.then((value)=>{
    //success
},(err)=>{
    //failure
})

复制代码

这样 Promise 就解决了回调地狱的问题,比如我们连续读取多个文件时,写法如下:

let fs=require('fs');
function read(url){
    return new Promise((resolve,reject)=>{
        fs.readFile(url,'utf8',function(err,data){
            if(err) reject(err);
            resolve(data);
        })
    }
}

read('a.txt').then((data)=>{
    console.log(data);
}).then(()=>{
    return read('b.txt');
}).then((data)=>{
    console.log(data);
}).catch((err)=>{
    console.log(err);
})
复制代码

如此不断的返回一个新的 Promise,这种不断的链式调用,就摆脱了callback回调地狱的问题和异步代码非线性执行的问题。

Promise 还解决了 callback 只能捕获当前错误异常。Promise 和 callback 不同,Promise 代理着所有的 callback 的报错,可以由 Promise 统一处理。所以,可以通过catch来捕获之前未捕获的异常。

Promise解决了callback的回调地狱问题,但Promise并没有摆脱callback。所以,有没有更好的写法呢?

5、Async Await

async函数是ES7中的一个新特性,它结合了Promise,让我们摆脱callback的束缚,直接用类同步的线性方式写异步函数,使得异步操作变得更加方便。

let fs=require('fs');
function read(url){
    return new Promise((resolve,reject)=>{
        fs.readFile(url,'utf8',function(err,data){
            if(err) reject(err);
            resolve(data);
        })
    }
}

async function r(){
    let a=await read('a.txt');
    let b=await read('b.txt');
    return a+b;
}
r().then((data)=>{
    console.log(data);
});

复制代码

至此,异步的 await 函数已经可以让我们满意。目前使用 Babel 已经支持 ES7 异步函数的转码了,大家可以在自己的项目中开始尝试。以后会不会出现更优秀的方案?以我们广大程序群体的创造力,相信一定会有的。

JavaScript异步调用的发展历程就到这里了,如果您觉得文章有用,可以打赏个咖啡钱(⊙﹏⊙)!

原文发布时间为:2018年06月27日

原文作者:afan

本文来源: 掘金  如需转载请联系原作者
相关文章
|
1月前
|
前端开发 JavaScript
如何处理 JavaScript 中的异步操作和 Promise?
如何处理 JavaScript 中的异步操作和 Promise?
15 1
|
1月前
|
前端开发 JavaScript 数据处理
在JavaScript中,什么是异步函数执行的例子
在JavaScript中,什么是异步函数执行的例子
10 0
|
1月前
|
前端开发 JavaScript
JavaScript的异步操作
JavaScript的异步操作
|
10天前
|
Web App开发 缓存 JavaScript
|
19天前
|
JavaScript 前端开发
JS 单线程还是多线程,如何显示异步操作
JS 单线程还是多线程,如何显示异步操作
22 2
|
2月前
|
前端开发 JavaScript
前端JavaScript中异步的终极解决方案:async/await
在深入讨论 async/await 之前,我们需要了解一下 JavaScript 的单线程和非阻塞的特性。JavaScript 是单线程的,也就是说在任何给定的时间点,只能执行一个操作。然而,对于需要大量时间的操作(例如从服务器获取数据),如果没有适当的管理机制,这种单线程特性可能会导致应用程序的阻塞。为了解决这个问题,JavaScript 引入了回调函数和后来的 Promise,用来管理这些异步操作。
|
2月前
|
前端开发 JavaScript
关于 JavaScript 的异步操作
关于 JavaScript 的异步操作
23 1
|
3月前
|
JavaScript
JS中同步和异步的区别
JS中同步和异步的区别
24 0
|
3月前
|
监控 前端开发 JavaScript
【面试题】聊聊 js 异步解决方案
【面试题】聊聊 js 异步解决方案
|
3月前
|
前端开发 JavaScript
【JavaScript】异步解决方案的发展历程
JavaScript是一种广泛使用的编程语言,用于开发Web应用程序。在Web开发中,异步编程是一种重要的技术,它允许在执行长时间运行的操作时不阻塞用户界面。随着JavaScript的发展,异步编程解决方案也在不断演进。本文将探讨JavaScript异步解决方案的发展历程、优缺点以及代码示例。
27 0