Javascript的异步编程知多少?

·  阅读 148
Javascript的异步编程知多少?

写在前面

Javascript的异步编程可以说在日常的前端业务开发中举足轻重,常见的异步编程有:回调函数、事件监听、Promise、Generator、Async/await。 ​

那么:

  • 同步编程和异步编程有啥区别呢?
  • 回调地狱有哪些方法可以解决呢?
  • Promise内部究竟有几种状态?
  • Promise是如何解决回调地狱的问题?
  • Generator执行后返回什么?
  • Async/await的方式比Promise和Generatir好在哪里?

同步和异步

同步:就是在执行某段代码时,在该代码没有得到返回结果前,其它代码是阻塞的无法执行,但是一旦执行完成拿到返回值后,就可以执行其它代码了。

异步:就是当某段代码执行异步过程调用发出后,这段代码不会立即得到返回结果,而是挂起在后台执行。在异步调用发出后,一般通过回调函数处理这个调用后才能拿到结果。

前面知道Javascript是单线程的,如果JS都是同步代码执行可能会造成阻塞。如果使用就不会造成阻塞,就不需要等待异步代码执行的返回结果,可以继续执行该异步任务之后的代码逻辑。

那么JS异步编程的实现方式是如何发展的呢?

早些年为了实现JS的异步编程,一般采用回调函数的方式,如:比较典型的事件回调,但是使用回调函数来实现存在一个很常见的问题,就是回调地狱。看下面的代码像不像俄罗斯套娃。

fs.readFile(a,"utf-8",(err,data)=>{
    fs.readFile(b,"utf-8",(err,data)=>{
        fs.readFile(c,"utf-8",(err,data)=>{
            fs.readFile(d,"utf-8",(err,data)=>{
                    ....
            })
        })
    })
})
复制代码

常见的异步编程的场景有:

  • ajax请求的回调
  • 定时器中的回调
  • 事件回调
  • Node.js中的一些方法回调

异步回调如果层级很少,可读性和代码的维护性暂时还是可以接受的,但是当层级变多后就会陷入回调地狱。

Promise

为了解决回调地狱的问题,社区提出了Promise的解决方案,ES6又将其写入语言标准,采用Promise的实现方式在一定程度上解决了回调地狱的问题。

Promise简单理解就是一个容器,里面保存了某个未来才会结束的事件的结果。从语法而言,Promise是一个可以获取异步操作消息的对象。Promise具有三个状态:

  • 待定状态pending:初始状态,既没有被完成,也没有被拒绝
  • 已完成fulfilled:操作成功完成
  • 已拒绝rejected:操作失败

关于Promise的状态切换,如果想深入研究,可以学习『有限状态机』知识点。

待定状态的Promise对象执行的话,最后要么通过一个值完成,要么就是通过一个原因拒绝。当待定状态改成为完成或拒绝状态时,我们可以使用Promise.then的形式进行链式调用。因为最后Promise.prototype.then和Promise.prototype.catch方法返回的是一个Promise,所以它们可以继续被链式调用。 未命名文件 (19).png Promise是如何结局回调地狱问题的?

  • 解决多层嵌套问题
  • 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性

Promise主要利用三大技术来解决回调地狱:回调函数延迟绑定、返回值穿透、错误冒泡

Promise.all

Promise.all(iterable)可以传递一个可迭代对象作为参数,此方法对于汇总多个Promise的结果很有用,在es6中可以将多个Promise.all异步请求并行操作。当所有结果成功返回时按照顺序返回成功,当其中一个方法失败则进入失败方法。

Promise.all(iterable);
复制代码

使用Promise.all解决上面的异步编程问题。

function read(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,"utf-8",(err,data)=>{
      if(err) return err;
      resolve(data);
    })
  })
}

read(A).then(data=>{
    return read(B);
}).then(data=>{
    return read(C);
}).then(data=>{
    return read(D);
}).catch(reason=>{
    console.log(reason);
})
复制代码

我们看到上面使用Promise的使用对回调地狱的解决有所提升,但是依旧不是很好维护,对此有了新的方法。

function read(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,"utf-8",(err,data)=>{
      if(err) return err;
      resolve(data);
    })
  })
}
//通过Promise.all可以实现多个异步并行执行,同一时刻获取最终解决的问题
Promise.all([read(A),read(B),read(C)]).(data=>{
    console.log(data)
}).catch(reason=>{
    console.log(reason);
})
复制代码

Promise.allSettled

Promise.allSettled的语法和Promise.all类似,都是接受一个可迭代对象作为参数,返回一个新的Promise。当Promise.allSettled全部处理完毕后,我们可以拿到每个Promise的状态,而不管其是否处理成功。

Promise.allSettled(iterable);
复制代码

Promise.any

Promise.any也是接收一个可迭代对象作为参数,any方法返回一个Promise。只要参数Promise实例有一个变成fulfilled状态,最后any返回的实例就会变成fullfiled状态;如果所有参数Promise实例都变成rejected状态,最后any返回的实例就会变成rejected状态。

Promise.race

Promise.race接收一个可迭代对象作为参数,race方法返回一个Promise,只要参数之中有一个实例率先改变状态,则race方法的返回状态就跟着改变。

Promise方法作用
all参数所有返回结果都为成功才返回
allSettled参数无论返回结果是否成功,都返回每个参数执行状态
any参数中只要有一个成功,就返回该成功的执行结果
race返回最先执行成功的参数的执行结果

Generator

Generator生成器是es6的新关键词,Generator是一个带星号的函数,可以配合yield关键字来暂停或执行函数。

Generator最大的特点就是可以交出函数的执行权,Generator函数可以看作是异步任务的容器,需要暂停的地方使用yield语法进行标注。

function* gen(){
  let a = yield 111;
  console.log(a);
  let b = yield 222;
  console.log(b);
  let c = yield 333;
  console.log(c);
  let d = yield 444;
  console.log(d);
}

let t = gen();
t.next(1);//第一调用next函数时,传递的参数无效,因此无法打印结果
t.next(2);//2
t.next(3);//3
t.next(4);//4
t.next(5);//5
复制代码

上面代码中,调用gen()后程序会被阻塞住,不会执行任何语句;而调用g.next()后程序会继续执行,直到遇到yield关键词时执行暂停;一直执行next方法,最后返回一个对象,其存在两个属性:value和done。 ​

yield也是es6的关键词,配合Generator执行以及暂停,yield关键词最后返回一个迭代器对象,该对象有value和done两个属性,value表示返回的值,done便是当前是否完成。

function* gen(){
  yield 1;
  yield* gen2();
  yield 4;
}

function* gen2(){
  yield 2;
  yield 3;
}

const g = gen();
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
复制代码

运行结果:

image.png

那么,Generator和异步编程有着什么联系呢?泽呢么才能将Generator函数按照顺序一次执行完毕呢?

thunk函数

thunk函数的基本思路就是接收一定的参数,会产生触定制化的函数,最后使用定制化的函数去完成想要实现的功能。

const isType = type => {
  return obj => {
    return Object.prototype.toString.call(obj) === `[object ${type}]`;
  }
}

const isString = isType("string");
const isArray = isType("Array");

isString("yichuan");//true
isArray(["red","green","blue"]);//true
复制代码
const readFileThunk = filename=>{
  return callback=>{
    fs.readFile(filename,callback);
  }
}

const gen = function* (){
  const data1 = yield readFileThunk("a.txt");
  console.log(data1.toString());
  const data2 = yield readFileThunk("b.txt");
  console.log(data2.toString());
}

const g = gen();
g.next().value((err,data1)=>{ 
  g.next(data1).value((err,data2)=>{
     g.next(data2);
  })
})
复制代码

我们可以看到上面的代码还是像俄罗斯套娃,理解费劲,我们进行优化以下:

function fun(get){
  const next = (err,data)=>{
    const res = gen.next(data);
    if(res.done) return;
    res.value(next);
  }
  next();
}

run(g);
复制代码

co函数库是用于处理Generator函数的自动执行,核心原理是前面讲到的通过和thunk函数以及Promise对象进行配合,包装成一个库。

Generator函数就是一个异步操作的容器,co函数接收Generator函数作为参数,并最后返回一个Promise对象。在返回的Promise对象中,co先检查参数gen是否为Generator函数。如果是就执行函数,如果不是就直接返回,并将Promise对象的状态改为resolved。co将Generator函数的内部指针对象的next方法包装成onFulfilled函数,主要是为了能够捕获到抛出的错误。关键在于next,他会反复调用自身。

const co = require("co");
const g = gen();
co(g).then(res=>{
  console.log(res);
})
复制代码

Async/await

JS异步编程从最开始的回调函数的方式演化到使用Promise对象,再到Generator+co函数的方式,每次都有一些改变但是都不彻底。async/await被称为JS中异步终极解决方案,既能够像Generator+co函数一样用同步方式阿里写异步代码,又能够得到底层的语法支持,无需借助任何第三方库。

async是Generator函数的语法糖,async/await的优点是代码清晰,可以处理回调的问题。

function testWait(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log("testWait");
      resolve();
    },1000);
  })
}

async function testAwaitUse(){
  await testWait();
  console.log("hello");
  return "yichuan";
}
//输出顺序依次是:testWait hello yichuan
console.log(testAwaitUse());
复制代码

异步编程方式小结

JS异步编程方式简单总结
回调函数最拉胯的异步编程方式
Promisees6新增语法,解决回调地狱问题
Generator和yield配合使用,返回的是迭代器
async/await二者配合使用,async返回的是Promise对象,await控制执行顺序

参考文章

写在最后

本文主要介绍了Javascript的最重要的知识点之一,也是之后开发工作中经常要接触的概念,常用的异步编程方式有:回调函数、Promise、Generator和async/await。频繁使用回调函数会造成回调地狱,Promise的出现就是解决回调地狱的,但是Promise的链式函数也有长,对于出现了async/await的终极解决方案。

分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改