玩转js异步编程

js异步编程

一般知道,js脚步语言的执行环境是单线程的,就是它会等一个任务完成,才会进行第二个任务,然后一直向下进行,这样的执行环境简单,但是处理不了复杂的运用,当一个请求需要非常久的时候,下一个流程就会被搁浅,如果长时间得不到反馈,进程就这样的奔溃了。

为了解决这个硬性需求,Javascript语言提出了二种语言模式: 同步(Synchronous)和 异步 (Asynchronous)。

异步的几种常用方法

  • 回调函数
  • 订阅和发布模式
  • Promise
  • generator
  • async/await

回调函数方法

通过把一个函数(callback)作为参数传入另一个函数,当满足一定条件的时候,就执行callback函数。

用法:

// 这里只是一个简单的条件
function fn1(a, fn) {
  if(a > 10 && fn instanceof Function) {
    fn.call()
  }
}
function fn2() {
  console.log(' --- fn2 ----')
}
// 通过简单的异步调用
function fn3(fn) {
  setTimeout(() => {
    console.log('--- fn3  ---')
    fn.call()
  },1000)
}
// 通过回调函数调用
fn1(12, fn2)
fn3(fn2)

通过回调函数的方式处理异步,是在异步早期的情况,其中jquery中的很多都是通过callback来实现回调的。但是这种模式代码编写比较耦合,不利于代码维护。

发布订阅模式

pub/sub模式是js设计模式中的一种,本身是借鉴于java的模式,但是在处理异步处理的时候非常有作用。通过一个信息中心EventCenter 来处理的监听(on)和触发(triggle)。可以参考楼主之前手写的设计模式的文章最后一个设计模式。

function fn1() {
  setTimeout(() => {
    // 异步操作后得到数据data
    let data = fetch(.....)
    // 触发信息中心的waterFull,并传出data
    Event.triggle('waterFull', data)
  },2000)
}
fn1()
Event.on('waterFull', (data) => {
  // 对得到的值进行进一步加工处理
  console.log(data)
})

通过pub/sub模式,我们可以在信息中心清楚的看到有多少信号来源,方便的集中管理,更加方便于模块化的管理,但是如果整个项目都使用pub/sub模式的话,流程就变得不太清晰了,数据的得到和数据的处理分开,对于后期的维护也是一个很大的问题。

Promise

对于现在一个基本的前端人员,没有说没有听过Promise的,如果你实在没有看多promise, 可以查看阮老师的es6文档Promise。下面主要是通过具体的要求来实现promise,不会仔细的讲解。

Promise构造函数成为承诺,它分为三种状态resolve, reject, pending ,一旦状态从pending改为其它2个状态之后,就不能修改了,就一个承诺一样。

Promise接收2个参数resolve , reject,分别表示成功后执行和失败后执行,可以通过实例的then()方法传递对于的函数。

// 返回一个Promise实例
const promise = new Promise((resolve, reject) => {
  // some code  这里函数会立马执行
  if(success) resolve(value)
  else reject(err)
})
promise.then(/*成功*/(data) => { console.log(data) }).catch(/*失败*/(err) => { console.log(err) })

这里看了之后,你可能会说,这个和异步处理有什么联系吗?你思考一下,当一个异步操作后,我们可以不去管它什么时候结束,什么时候出错,就像一个人承诺了,我只需要按照他的承诺去当这个事情已经被处理好了,是不是方便很多,下面直接上手一个例子。

let promise = new Promise((resolve, reject) => {
  let data = fetch('url') // 得到接口返回的数据
  resolve(data)
})
promise.then(data => console.log(data));

// fetch自动返回一个promise
fetch('http://ons.me/tools/dropload/json.php?page=0&size=4').then(response => response.json()).then(data => console.log(data)) // 可以直接到控制台看结果
//

我完全不用担心它里面怎么实现了,反正它已经承诺了会给我结果,我只需要通过then()方法去接受,我需要得到的值就可以了。

Promise.resolve(value) value可以是三种值
  1. 单个值
  2. 一个promsie实例
  3. 一个thenable对象
Promise.resolve(value).then((value) => {})

处理一个请求依赖另一个请求的情况

如果一个请求的结果是下一个请求的参数,如果我们使用原始的请求方法,就是出现一个像右的箭头的回调地狱。

一层层嵌套,非常的恐怖,不利于维护。那么通过prmise怎么处理回调地狱呢?

function send(url) {
  return new Promise((resolve, reject => {
    ajax(data);
    if('成功') resolve(data)
    else reject
  }))
}
send('url1').then(data => send('url2'))
            .then(data => send('url3'))
            .then(data => send('url4'))
            .then(data => console.log(data)) //输出最终的值

// 还有一个简单的例子
Promise.resolve(1).then(val1 => val1+2).then(val2 => val2+3).then(val3 => console.log(val3)) //6

上面处理回调地狱是不是看着方便很多,代码也简单命令,依赖性也很强,后面我们会继续通过async/await继续简化。

处理多个请求并发的情况(不需要管服务器的返回顺序)

Promise.all(arr) 接受一个promise实例的数组,可以并发多个请求给服务器,但是并不能保证接受到的先后顺序,这个取决于服务器的处理速度。

// 现在有一个包含url的数组,需要并发请求给服务器  setPromise是一个包装成promise的函数,返回一个promsie实例
let urlArr = [url1, url2, url3]
Promise.all(urlArr.map(url => setPromise(url))).then(data => console.log(data))
// 会得到一个数组,包含了三个请求数据的数组。

处理多个请求并发,并且需要保证返回数据的顺序(运用场景比较少)

上面一个方法并不会保证请求返回的结果,按照你发送的顺序返回,如果我想把完整的响应的结果按照我
希望的顺序返回给我,那应该怎么办呢?

let urlArr = [url1, url2, url3];
let totalData = []
// 遍历一个数组,并对每一项都执行对应的函数,返回一个Promise.
urlArr.reduce((promise, url) => {
  return promise.then(() => setPromise(url)).then(data => { totalData.push(data) })
}, Promise.resolve()) 

这样,会等待每一个请求完成后,并把得到的数据pushtotalData中,就可以按照顺序得到我们想要的值了。当然使用async/await会更加的方便。之后我们会讲解。

generator构造器

generator是一个构造器,generator函数执行并不会执行函数体内部部分,而是返回一个构造器对象,通过构造器对象的next()方法调用函数主体,并且每当遇到yield都会暂停执行,并返回一个对象。

function* gen() {
  console.log(`---- start ---`)
  yield 1
  yield 2
  return 3
}
let g = gen() // 这里执行了generator函数,但是并没有执行下面
g.next()  // console---- start --- return { value: 1; done: false }
g.next() // {value: 2; done : false}
g.next() // {value: 3; done: true}
g.next() // {value: undefined; done: true}

注意yield本身是不会反悔内容的,只是给构造器对象返回了内容,如果想yield表达式也返回内容,可以通过给下一个next()传递参数。

function* gen() {
  let a = yield 1
  console.log(a)
  yield 2
  return 3
}
let g = gen();
// 这里先执行yield 1 然后暂停函数
g.next() // {value: 1, done: false}  
// 继续执行赋值表达式,并yield 1得到的值为 ggg
g.next('ggg') // console  ggg  {value: 2, done: false} 

通过next()传递参数,我们可以做到值向内部传递,对于后面的异步处理很有帮助。

generator异步运用

利用构造器的暂停和继续的功能,我们可以很好的处理异步请求,得到数据后再进行其他内容。主要是运用yield表达式返回一个promise对象的原理。

function* send() {
  let data = yield fetch('https://suggest.taobao.com/sug?code=utf-8&q=%E6%89%8B%E6%9C%BA');
}
let objData;
// 调用
send().next().value.then( response => response.json()).then(data => objData = data)

这样我们就得到了接口请求的数据,相比于之前的promise函数的书写是不是要简单很多。和同步是一样的操作。

如果我们想内部对得到的数据进行进一步的处理呢?

// 这里可以像写同步代码的一样,除掉这个yield关键字
function* send() {
  let data = yield fetch('https://suggest.taobao.com/sug?code=utf-8&q=%E6%89%8B%E6%9C%BA');
  data.result.map(item => {
    return item.push(11)
  })
  return data
}
let objData;
let gen = send()
// 调用  和promise一样的调用。
gen.next().value.then( response => response.json()).then(data => gen.next(data)).then(data => objData=data)

// 多个请求
var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
// 首先手动执行
const g = gen()
g.next().value.then(data => {
  // 将第一个接口的值传入
  g.next(data).value.then(data => {
    // 将第二个接口的值传入
    g.next(data);
  })
})

简单的co模块处理generator多个函数请求

从上面我的调用方法就可以看出,利用Promise + generator的异步处理不断地通过then()方法处理数据。有没有一个方式是我可以直接运行一个函数,然后就可以得到我想要的值。 例如:

function* send() {
  let data = yield fetch('https://suggest.taobao.com/sug?code=utf-8&q=%E6%89%8B%E6%9C%BA');
  return data
}

run(send) // 这样调用就可以直接返回一个data数据
// TODO
function run(gen) {
  const g = gen();
  function next(data) {
    let result = g.next(data);
    // 如果执行完了,就直接返回value
    if(result.done) return result.value
    result.value.then(data => {
      // 回调执行
      next(data)
    })
  } 
  next()
}

网上已经封装了很多的方法,例如常见的run库,co函数就是来处理这样的处理方式。但是当我们发送多个请求的时候,可能你会这样写:

function* send() {
  var p1 =yield  request( "http://some.url.1" );
  var p2 =yield  request( "http://some.url.2" );
  var r3 = yield request(
        "http://some.url.3/?v=" + r1 + "," + r2
  );
  console.log(r3)       
}
// 运行已经实现好的run函数
run(send)

这样写是会发送请求,但是并不是并发多个请求,而是等第一个请求p1之后,再进行第二个请求p2,在性能优化方面是不利的,也不符合我们的要求,怎么做到2个请求是独立的,并且我们还可以通过得到2个请求的结果后,进行其他请求。或许我们可以这样:

function* send() {
  // 先并发进行请求
  var p1 = request( "http://some.url.1" );
  var p2 = request( "http://some.url.2" );
  // 请求已经发送了,我们可以让得到的数据进行yield处理
  const d1 = yield p1;
  const d2 = yield p2;
  var r3 = yield request(
    "http://some.url.3/?v=" + d1 + "," + d2
  );
}

这样写是不是和我们之前写的Promise.all()很像?所以还可以改成这样的:

function* send() {
  // 先并发进行请求,然后等待解析数据
  const [d1, d2] = yield Promise.all([
    request( "http://some.url.1" ),
    request( "http://some.url.2" )
  ])
  var r3 = yield request(
    "http://some.url.3/?v=" + d1 + "," + d2
  );
}

async/await异步处理

ES7出现了async/await进行异步的处理,使得异步操作就像同步代码一样简单,方便了使用,由于async/await内部封装了generator的 处理,所有就很少有人用generator来处理异步了,但是在异步的推动中generator起到了很大的作用。

await: 后面接受一个promise实例

**async: 返回一个promise对象 **

一个简单的异步请求

async function f() {
    // 直接得到了接口返回的数据,在这里会等待接口返回数据。
    let data = await fetch('').then(res => res.json()) 
    console.log(data) // 接口数据
    return data  // 返回一个promise实例
}

async function h() {
  let data = await Promise.resolve(22);
  console.log(data);  // 22
  return data  // Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 22}
}

async function c() {
  try {
    let data = await Promise.reject(22);
    console.log(11) // 不会执行
  } catch(e){
     console.log(222)  //  输出 222
  } 
  return 333  // Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 333} 
}

上面的例子是不是和generator中的异步请求很像?可以像同步一样的编写代码,但是相比generator,await后面加上promise后直接返回相应的数据,不像yield还需要从外部传入。

处理多个请求并发的情况(不需要管服务器的返回顺序)

用async/await处理多个请求并发,由于await后面需要添加Promise实例,是不是脑袋里面一下子就想到了一个Promise.all()

// request返回一个promise对象
async function send() {
  // 先并发进行请求,然后等待解析数据
  const [d1, d2] = await Promise.all([
    request( "http://some.url.1" ),
    request( "http://some.url.2" )
  ])
}

你可能会很好奇,为什么不需要像generator那样通过额外的函数来调用,因为async已经帮你想好了,内部已经调用了,是不是很爽?

处理多个请求并发,并且需要保证返回数据的顺序(运用场景比较少)

如果数据中没有相互的联系,但是又想一个个发送,可以这样。

let patharr = [url1, url2, url3]
async function main2() {
  let arrData = [];
  // 利用for循环一次次的执行
  for(const url of pathArr) {
    arrData.push(await request(url));
  }
  return arrData
} 

异步好文

图解async/awati

推荐阅读更多精彩内容

  • 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...
    呼呼哥阅读 5,900评论 5 22
  • 官方中文版原文链接 感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大...
    HetfieldJoe阅读 5,670评论 9 20
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,000评论 0 4
  • 本文首发在个人博客:http://muyunyun.cn/posts/7b9fdc87/ 提到 Node.js, ...
    牧云云阅读 1,455评论 0 3
  • 一.非阻塞和异步 借用知乎用户严肃的回答在此总结下,同步和异步是针对消息通信机制,同步代表一个client发出一个...
    Daniel_adu阅读 1,430评论 0 8