js异步请求

目前async / await特性并没有被添加到ES2016标准中,但不代表这些特性将来不会被加入到Javascript中。在我写这篇文章时,它已经到达第三版草案,并且正迅速的发展中。这些特性已经被IE Edge支持了,而且它将会到达第四版,届时该特性将会登陆其他浏览器 -- 为加入该语言的下一版本而铺路(也可以看看:TC39进程)。

我们听说特性已经有一段时间了,现在让我们深入它,并了解它是如何工作的。为了能够了解这篇文章的内容,你需要对promise和生成器对象有深厚的理解。这些资源或许可以帮到你。

使用Promise

让我们假设我们有像下面这样的代码。在这里我将一个HTTP请求包装在一个Promise对象中。这个Promise在成功时会返回body对象,被拒绝时会将原因err返回。它每次都会在本博客(原作者博客)中为一篇随机文章拉取html内容。

var request = require(‘request‘);

function getRandomPonyFooArticle () {
  return new Promise((resolve, reject) => {
    request(‘https://ponyfoo.com/articles/random‘, (err, res, body) => {
      if (err) {
        reject(err); return;
      }
      resolve(body);
    });
  });
}

上述的promise代码的典型用法是像下面写的这样。 在那里,我们新建了一个promise链来将HTML页面中的DOM对象的一个子集转换成Markdown,然后再转换成对终端友好的输出, 最终再使用console.log输出它。 永远要记得为你的promise添加.catch处理器。

var hget = require(‘hget‘);
var marked = require(‘marked‘);
var Term = require(‘marked-terminal‘);

printRandomArticle();

function printRandomArticle () {
  getRandomPonyFooArticle()
    .then(html => hget(html, {
      markdown: true,
      root: ‘main‘,
      ignore: ‘.at-subscribe,.mm-comments,.de-sidebar‘
    }))
    .then(md => marked(md, {
      renderer: new Term()
    }))
    .then(txt => console.log(txt))
    .catch(reason => console.error(reason));
}

当代码运行后,这段代码将产生像以下截图所示的输出。

上面那段代码就是“比用回调函数更好”的写法,它能让你感觉像在按顺序的阅读代码。

使用生成器(generator)

过去,通过探索,我们发现生成器可以用一种“同步”合成的方法来获得html。即使现在的代码有一些同步写法,其中还是涉及相当多的包装,而且生成器可能不是最直截了当的达到我们期望结果的方法,最终可能无论如何我们都会坚持改为使用promise。

function getRandomPonyFooArticle (gen) {
  var g = gen();
  request(‘https://ponyfoo.com/articles/random‘, (err, res, body) => {
    if (err) {
      g.throw(err); return;
    }
    g.next(body);
  });
}

getRandomPonyFooArticle(function* printRandomArticle () {
  var html = yield;
  var md = hget(html, {
    markdown: true,
    root: ‘main‘,
    ignore: ‘.at-subscribe,.mm-comments,.de-sidebar‘
  });
  var txt = marked(md, {
    renderer: new Term()
  });
  console.log(txt);
});

“请记住,在使用promise时,你应该将yield调用包装在try/catch块中来保留我们添加的错误处理器”

不说你也知道,像这样使用生成器并不容易扩展。除了涉及直观的语法的混入,你的迭代代码会高度耦合到生成器函数中,这将会降低扩展性。这表示你在添加新的await表达式到生成器中时需要经常修改它。一个更好的替代方案是使用即将到来的Async函数

使用async/await

当Async函数终于落地时,我们将可以采取基于Promise的实现方法并使用它的优点,即像写同步生成器一样写异步。这种做法的另一个好处是你完全不需要再去修改getRandomPonyFooArticle方法,在它返回一个承诺前,它会一直等待。

要注意的是,await只能在函数中用async关键字标记后才能使用 它的工作方式和生成器很相似,直到promise完成之前,会在你的上下文中暂停处理。如果等待表达式不是一个promise,它也会被改造成一个promise。

read();

async function read () {
  var html = await getRandomPonyFooArticle();
  var md = hget(html, {
    markdown: true,
    root: ‘main‘,
    ignore: ‘.at-subscribe,.mm-comments,.de-sidebar‘
  });
  var txt = marked(md, {
    renderer: new Term()
  });
  console.log(txt);
}

“再次, -- 跟生成器一样 -- 记住,你最好把`await`包装到`try/catch`中,这样你就可以在异步函数中对返回后的promise进行错误捕获和处理。”

此外,一个Async函数总是会返回一个Promise对象。 这个promise在出现无法捕获的异常时会被拒绝,否则它会处理async函数的返回值。这就允许我们调用一个async函数并混入常规的基于promise的扩展。以下例子展示了两个方法的结合(看看Babel的交互式解释器)。

async function asyncFun () {
  var value = await Promise
    .resolve(1)
    .then(x => x * 3)
    .then(x => x + 5)
    .then(x => x / 2);
  return value;
}
asyncFun().then(x => console.log(`x: ${x}`));
// <- ‘x: 4‘

回到前一个例子中,那表示我们可以从异步读取函数中返回文本,并且允许调用者使用promise或另一个Async函数进行扩展。 那样,你的读取函数将只需关注从Pony Foo上的随机文章中拉取终端可读的Markdown即可。

async function read () {
  var html = await getRandomPonyFooArticle();
  var md = hget(html, {
    markdown: true,
    root: ‘main‘,
    ignore: ‘.at-subscribe,.mm-comments,.de-sidebar‘
  });
  var txt = marked(md, {
    renderer: new Term()
  });
  return txt;
}

然后,你可以进一步在另一个Async函数中调用await read()

async function write () {
  var txt = await read();
  console.log(txt);
}

或者你可以只使用promise对象来进一步扩展。

read().then(txt => console.log(txt));

岔路

在异步代码流中,总是能遇到同时执行两个或更多任务的情况。当Async函数更容易编写异步代码后,它们也将自己依次传递给代码。 这就是说:代码在一个时刻只执行一个操作。一个包含多个await表达式的函数在promise对象执行完之前,在恢复执行和移动到下一个await表达式之前,会在每个await表达式处暂停一次, -- 就跟我们在生成器和yield关键字处观察到的情况一样。

你可以使用Promise.all来解决创建单个promise对象并进行等待的功能。 当然,最大的问题是从习惯于让所有东西都串行运行改成使用Promise.all, 否则这将给你的代码带来性能瓶颈。

下面的例子展示了你如何同时完成对三个不同的promise对象进行等待操作。特定的await操作符会暂停你的Async函数,和等待 Promise.all表达式一起,最终会被解析到一个结果数组中,我们可以使用析构函数逐个拉取该数组中的单个结果。

async function concurrent () {
  var [r1, r2, r3] = await Promise.all([p1, p2, p3]);
}

在某些情况下, 可以用 await *来改动上述代码片段,让你不必用Promise.all来包装你的promise对象。Babel 5依然支持这种特性,但它已经从规格说明中移除(也已经从Babel 6中移除) -- 因为这些原因

async function concurrent () {
  var [r1, r2, r3] = await* [p1, p2, p3];
}

你依然可以用类似all = Promise.all.bind(Promise)的代码来做些事情,来获得一个简洁的替代Promise.all的方法。在这之上的是,你可以对Promise.race做相同的事情,而这跟使用await*并不等价。

const all = Promise.all.bind(Promise);
async function concurrent () {
  var [r1, r2, r3] = `await all([p1, p2, p3])`;
}

错误处理

要注意的是,在异步函数中,错误会被“默默的”吞噬 -- 就像在普通的Promise对象中一样。 除非我们围绕await表达式添加try/catch块 -- 而不管在暂停时,它们会在你的异步函数体中发生还是在它暂停时发生 -- promise对象会被拒绝并通过Async函数返回错误。

自然,这可以看作是一个能力: 你可以利用try/catch代码块,有些东西你无法用回调函数实现-- 但可以用Promise对象实现。 在这种情况下,Async函数就类似生成器,得益于函数执行暂停特性,你可以利用try/catch将异步流代码写成同步代码的样子。

此外, 你可以在Async函数外捕获这些异常, 只需要简单的对它们返回的promise对象添加.catch()方法调用。在promise对象中尝试用.catch方法来将try/catch错误处理组合起来是一种比较灵活的方法,但该方法也可能导致混乱并最终导致错误无法处理。

read()
  .then(txt => console.log(txt))
  .catch(reason => console.error(reason));

我们要小心谨慎并时刻提醒自己用不同的方法来让我们可以发现错误、处理错误或预防错误。

如今如何使用async/await

如今,有一种在你的代码中使用Async函数的方法是通过Babel。这涉及一系列模块,但只要你愿意,你总是可以拿出一个模块来将全部这些代码包装进去。我包含npm-run作为一个有用的方法,用于保持本地的所有东西都用包进行安装。

npm i -g npm-run
npm i -D   browserify   babelify   babel-preset-es2015   babel-preset-stage-3   babel-runtime   babel-plugin-transform-runtime

echo ‘{
  "presets": ["es2015", "stage-3"],
  "plugins": ["transform-runtime"]
}‘ > .babelrc

在使用babelifyAsync函数提供支持时,以下命令会将example.js通过browserify进行编译。然后你就可以用管道将脚本传输给node执行,或将脚本保存到硬盘中。

npm-run browserify -t babelify example.js | node

深入阅读

Async函数规格草案出奇的短,并且应该能成为一个有趣的读物, 如果你热衷于学习更多这些即将到来的功能。

我已经粘贴了一段代码在下面, 它是为了帮助你理解async函数的内部是如何工作的。即使我们不可以填充新的关键字,它也可以帮助你理解在async/await的帷幕后面发生了什么事情。

“换句话说,它应该对学习异步函数内部原理非常有帮助,无论是对生成器还是promise。”

首先,下面的一小段代码展示了一个async函数如何通过常规的function关键字来简化声明过程,这将返回一个生成spawn 生成器函数的结果 -- 我们会认为await在语法上是和yield等价的。

async function example (a, b, c) {
  example function body
}

function example (a, b, c) {
  return spawn(function* () {
    example function body
  }, this);
}

spawn中,promise会被代码包装起来并传入生成器函数中,通过用户代码串行的执行,并将值传递到你的“生成器”代码中(async函数的函数体中)。 在这个意义上,我们可以注意Async函数真的是生成器和primose对象之上的语法糖,这对于让你理解其中每一个环节是如何工作来说非常重要,这是为了让你对于混合、匹配、合并不同的异步代码流的写法有一个更好的理解。

function spawn (genF, self) {
  return new Promise(function (resolve, reject) {
    var gen = genF.call(self);
    step(() => gen.next(undefined));
    function step (nextF) {
      var next;
      try {
        next = nextF();
      } catch(e) {
        // 执行失败,并拒绝promise对象
        reject(e);
        return;
      }
      if (next.done) {
        // 执行成功,处理promise对象
        resolve(next.value);
        return;
      }
      // 未完成,以yield标记的promise对象呗中断,并在此执行step方法
      Promise.resolve(next.value).then(
        v => step(() => gen.next(v)),
        e => step(() => gen.throw(e))
      );
    }
  });
}
时间: 2024-11-07 20:35:35

js异步请求的相关文章

js异步请求发展史和yield

万恶的回调 对前端工程师来说,异步回调是再熟悉不过了,浏览器中的各种交互逻辑都是通过事件回调实现的,前端逻辑越来越复杂,导致回调函数越来越多,同时 nodejs 的流行也让 javascript 在后端的复杂场景中得到应用,在 nodejs 代码中更是经常看到层层嵌套. 以下是一个典型的异步场景:先通过异步请求获取页面数据,然后根据页面数据请求用户信息,最后根据用户信息请求用户的产品列表.过多的回调函数嵌套,使得程序难以维护,发展成万恶的回调. $.get('/api/data', functi

原生js 异步请求,responseXML解析

异步更新原理:用XMLHTTP发送请求得到服务器端应答数据,在不重新载入整个页面的情况下,用js操作Dom最终更新页面1.创建XMLHttp请求协议 1 function createXMLHttpRequest(){ 2 var xmlHttp; 3 if(window.ActiveXObject) { //IE浏览器 4 //IE浏览器(将XMLHttpRequest对象作为ActiveX对象来创建) 5 try{ 6 xmlHttp = new ActiveXObject("Msxml2.

js 异步请求封装

1. function ajax(url, onsuccess) { var xmlhttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); //创建XMLHTTP对象,考虑兼容性.XHR xmlhttp.open("POST", url, true); //“准备”向服务器的GetDate1.ashx发出Post请求(GET可能会有缓存问题).这里还没有发

js 异步请求

<p id="check"> <label>验证码:</label> <input class="vid" id="ValidateCode" name="ValidateCode" type="text" value="" onblur="checkValidateCode()"/> <img id="

手动封装js原生XMLHttprequest异步请求

Code Object.extend =function(targetObj,fnJson){ //扩展方法,类似于jQuery的$.extend,可以扩展类的方法,也可以合并对象 for(var fnName in fnJson){ targetObj[fnName]=fnJson[fnName]; } return targetObj; }; HttpAjax = (function(){ function HttpAjax(options){ var settings={ type:'po

[js开源组件开发]network异步请求ajax的扩展

network异步请求ajax的扩展 在日常的应用中,你可能直接调用$.ajax是会有些问题的,比如说用户的重复点击,比如说我只希望它成功提交一次后就不能再提交,比如说我希望有个正在提交的loading效果.所以我做network这个组件来扩展$.ajax,希望全中国的人民们喜欢. 这里使用到了上篇[js开源组件开发]loading加载效果 一个loading效果,但为了让它独立运行,所以没有进行引用,而是直接做了一个loading方法在里面.它的具体效果图如下: 它的实例DEMO地址请点击这里

Ajax_原生ajax写法、理解异步请求、js单线程+事件队列、封装原生ajax

1.原生Ajax 一定要理解Ajax出现的背景 Ajax通过url查询后端接口的数据,在前端做数据的解析和局部更新 1.隐藏帧iframe方式实现页面局部更新---只是为了比较好的用户体验 访问后台接口数据显示在iframe页面中显示,没有做主页面的刷新,但页面实际上也刷新了  看左上角的转圈圈了 2.Ajax异步请求,真正实现页面局部刷新,没有跳转,坐上角小圈圈没转 原生Ajax写法---注意ajax的缩写 3.服务器放回了xml数据格式 解析过程还是很麻烦的,所以这种数据格式很少用了. 4.

Ajax:实现后台验证js实现get方式的异步请求,判断用户名是否重复

<%@ page contentType="text/html;charset=UTF-8" language="java" %><html> <head> <title>ajax</title> <script src="/ajax/js/jquery-3.3.1.min.js"></script> </head> <body> <

js实现post方式的异步请求

<%@ page contentType="text/html;charset=UTF-8" language="java" %><html> <head> <title>ajax</title> <script src="/ajax/js/jquery-3.3.1.min.js"></script> </head> <body> <