整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JavaScript中的async/await

文同步本人掘金平台的原创翻译文章:https://juejin.cn/post/6844903834456702984

异步的JavaScript从未如何简单!过去段时间,我们使用回调。然后,我们使用promises。现在,我们有了异步功能函数。

异步函数能够使得(我们)编写异步JavaScript更加容易,但是,它自带一套陷阱,对初学者很不友好。

在这个由两部分组成的文章中,我想分享下你需要了解的有关异步函数的内容。【PS:另一部分暂不打算翻译】

异步功能

异步功能函数包含async关键词。你可以在正常的函数声明中使用它:

async function functionName (arguments) {
  // Do something asynchronous
}
复制代码

你也可以使用箭头函数。

const functionName = async (arguments) => {
  // Do something asynchronous
}
复制代码

异步函数总是返回promises

(异步函数)它不管你返回什么。其返回值都是promise。

const getOne = async _ => { 
  return 1 
} 

const promise = getOne()
console.log(promise) // Promise 
复制代码

笔记:在接着往前读之前,你应该知道什么是JavaScript Promises知识点,以及如何使用它们。否则,它会开始变得混乱。这篇文章会帮助你熟悉JavaScript Promise。

await关键字

当你调用promise时,你会在then中处理下一步,如下:

const getOne = async _ => { 
  return 1 
} 

getOne()
  .then(value => {
    console.log(value) // 1
  })
复制代码

await关键字允许你等待promise去解析。一旦解析完promise,它就会返回参数传递给then调用。

const test = async _ => {
  const one = await getOne()
  console.log(one) // 1
}

test()
复制代码

返回await

在返回承诺(promise)之前没有必要等待(await)。你可以直接退回承诺。

如果你return await些内容,则你首先是解决了原先promise。然后,你从已经解析的内容(resolved value)创建新的promise。return await真的没做什么有效的东西。无需额外的步骤。

// Don't need to do this 
const test = async _ => {
  return await getOne()
}

test()
  .then(value => {
    console.log(value) // 1
  })
复制代码
// Do this instead
const test = async _ => {
  return getOne()
}

test()
  .then(value => {
    console.log(value) // 1
  })
复制代码

注意:如果你不需要await,则不需要使用异步功能(async function)。上面的例子可以改写如下:

// Do this instead
const test = _ => {
  return getOne()
}

test()
  .then(value => {
    console.log(value) // 1
  })
复制代码

处理错误

如果一个promise出错了,你可以使用catch调用来处理它,如下所示:

const getOne = async (success = true) => { 
  if (success) return 1
  throw new Error('Failure!')
} 

getOne(false)
  .catch(error => console.log(error)) // Failure!
复制代码

如果你想在一个异步函数中处理错误,你需要调用try/catch。

const test = async _ => {
  try {
    const one = await getOne(false)
  } catch (error) {
    console.log(error) // Failure!
  }
}

test()
复制代码

如果你有多个await关键字,错误处理可能变得很难看...

const test = async _ => {
  try {
    const one = await getOne(false)
  } catch (error) {
    console.log(error) // Failure!
  }

  try {
    const two = await getTwo(false)
  } catch (error) {
    console.log(error) // Failure!
  }

  try {
    const three = await getThree(false)
  } catch (error) {
    console.log(error) // Failure!
  }
}

test()
复制代码

还有更好的方法。

我们知道异步函数总是返回一个promise。当我们调用promise时,我们可以在catch调用中处理错误。这意味着我们可以通过添加.catch来处理异步函数中的任何错误。

const test = async _ => {
  const one = await getOne(false)
  const two = await getTwo(false)
  const three = await getThree(false)
}

test()
  .catch(error => console.log(error)))
复制代码

注意:Promise的catch方法只允许你捕获一个错误。

多个awaits

await阻止JavaScript执行下一行代码,直到promise解析为止。这可能会导致代码执行速度减慢的意外效果。

为了实际演示这点,我们需要在解析promise之前创建一个延迟。我们可以使用sleep功能来创建延迟。

const sleep = ms => {
  return new Promise(resolve => setTimeout(resolve, ms))
}
复制代码

ms是解析前等待的毫秒数。如果你传入1000到sleep函数,JavaScript将等待一秒才能解析promise。

// Using Sleep
console.log('Now')
sleep(1000)
  .then(v => { 
    console.log('After one second') 
  })
复制代码

假设getOne需要一秒来解析。为了创建这个延迟,我们将1000(一秒)传入到sleep。一秒过后,sleeppromise解析后,我们返回值1。

const getOne = _ => {
  return sleep(1000).then(v => 1)
}
复制代码

如果你使用await getOne(),你会发现在getOne解析之前需要一秒钟。

const test = async _ => {
  console.log('Now')

  const one = await getOne()
  console.log(one)
}

test()
复制代码

现在,假设你需要处理三个promises。每个promise都有一秒钟的延迟。

const getOne = _ => {
  return sleep(1000).then(v => 1)
}

const getTwo = _ => {
  return sleep(1000).then(v => 2)
}

const getThree = _ => {
  return sleep(1000).then(v => 3)
}
复制代码

如果你连续await这三个promises,你将要等待三秒才能解析完所有promises。这并不好,因为我们强迫JavaScript在做我们需要做的事情之前等待了两秒钟。

const test = async _ => {
  const one = await getOne()
  console.log(one)

  const two = await getTwo()
  console.log(two)

  const three = await getThree()
  console.log(three)

  console.log('Done')
}

test()
复制代码

如果getOne,getTwo和getThree可以同时获取,你将节省两秒钟。你可以使用Promise.all同时获取这三个promises。

有三个步骤:

  1. 创建三个promises
  2. 将三个promises添加到一个数组中
  3. 使用Promise.all来awaitpromises数组

如下所示:

const test = async _ => {
  const promises = [getOne(), getTwo(), getThree()]
  console.log('Now')

  const [one, two, three] = await Promise.all(promises)
  console.log(one)
  console.log(two)
  console.log(three)

  console.log('Done')
}

test()
复制代码

这就是你需要了解的基本异步功能函数!我希望这篇文章为你扫除了些障碍。

笔记:这篇文章是Learn JavaScript的修改摘录。如果你发现本文有用,你可能需要去查看它。

ello~各位亲爱的看官老爷们大家好。估计大家都听过,尽量将 CSS 放头部, JS 放底部,这样可以提高页面的性能。然而,为什么呢?大家有考虑过么?很长一段时间,我都是知其然而不知其所以然,强行背下来应付考核当然可以,但实际应用中必然一塌糊涂。因此洗(wang)心(yang)革(bu)面(lao),小结一下最近玩出来的成果。

友情提示,本文也是小白向为主,如果直接想看结论可以拉到最下面看的~

推荐下我的前端群:524262608,不定期分享干货,包括我自己整理的一份2017最新的前端资料和零基础入门教程,欢迎初学和进阶中的小伙伴。

由于关系到文件的读取,那是肯定需要服务器的,我会把全部的文件放在 github 上,给我点个 star 我会开心!掘金上再给我点个 赞 我就更开心了~

node 端唯一需要解释一下的是这个函数:

function sleep(time) { return new Promise(function(res) {
setTimeout(() => {
res()
}, time);
})
}

嗯!其实就延时啦。如果 CSS 或者 JS 文件名有 sleep3000 之类的前缀时,意思就是延迟3000毫秒才会返回这文件。

下文使用的 HTML 文件是长这样的:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
 div { width: 100px; height: 100px; background: lightgreen;
 } </style>
</head>
<body>
<div></div>
</body>
</html>

我会在其中插入不同的 JS 和 CSS 。

而使用的 common.css ,不论有没有前缀,内容都是这样的:

div { background: red;
}

好了,话不多数,开始正文!

关于 CSS ,大家肯定都知道的是 <link> 标签放在头部性能会高一点,少一点人知道如果 <script> 与 <link> 同时在头部的话, <script> 在上可能会更好。这是为什么呢?下面我们一起来看一下 CSS 对 DOM 的影响是什么。

CSS 不会阻塞 DOM 的解析

注意哦!这里说的是 DOM 解析,证明的例子如下,首先在头部插入 <script defer src="/js/logDiv.js"></script> , JS 文件的内容是:

const div = document.querySelecot('div');
console.log(div);

defer 属性相信大家也很熟悉了,MDN对此的描述是用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。设置这个属性,能保证 DOM 解析后马上打印出 div 。

之后将 <link rel="stylesheet" href="/css/sleep3000-common.css"> 插入 HTML 文件的任一位置,打开浏览器,可以看到是首先打印出 div 这个 DOM 节点,过3s左右之后才渲染出一个浅蓝色的 div 。这就证明了 CSS 是不会阻塞 DOM 的解析的,尽管 CSS 下载需要3s,但这个过程中,浏览器不会傻等着 CSS 下载完,而是会解析 DOM 的。

这里简单说一下,浏览器是解析 DOM 生成 DOM Tree ,结合 CSS 生成的 CSS Tree ,最终组成 render tree ,再渲染页面。由此可见,在此过程中 CSS 完全无法影响 DOM Tree ,因而无需阻塞 DOM 解析。然而, DOM Tree 和 CSS Tree 会组合成 render tree ,那 CSS 会不会页面阻塞渲染呢?

CSS 阻塞页面渲染

其实这一点,刚才的例子已经说明了,如果 CSS 不会阻塞页面阻塞渲染,那么 CSS 文件下载之前,浏览器就会渲染出一个浅绿色的 div ,之后再变成浅蓝色。浏览器的这个策略其实很明智的,想象一下,如果没有这个策略,页面首先会呈现出一个原始的模样,待 CSS 下载完之后又突然变了一个模样。用户体验可谓极差,而且渲染是有成本的。

因此,基于性能与用户体验的考虑,浏览器会尽量减少渲染的次数, CSS 顺理成章地阻塞页面渲染。

然而,事情总有奇怪的,请看这例子, HTML 头部结构如下:

<header>
<link rel="stylesheet" href="/css/sleep3000-common.css">
<script src="/js/logDiv.js"></script></header>

但思考一下这会产生什么结果呢?

答案是浏览器会转圈圈三秒,但此过程中不会打印任何东西,之后呈现出一个浅蓝色的 div ,再打印出 null 。结果好像是 CSS 不单阻塞了页面渲染,还阻塞了 DOM 的解析啊!稍等,在你打算掀桌子疯狂吐槽我之前,请先思考一下是什么阻塞了 DOM 的解析,刚才已经证明了 CSS 是不会阻塞的,那么阻塞了页面解析其实是 JS !但明明 JS 的代码如此简单,肯定不会阻塞这么久,那就是 JS 在等待 CSS 的下载,这是为什么呢?

仔细思考一下,其实这样做是有道理的,如果脚本的内容是获取元素的样式,宽高等 CSS 控制的属性,浏览器是需要计算的,也就是依赖于 CSS 。浏览器也无法感知脚本内容到底是什么,为避免样式获取,因而只好等前面所有的样式下载完后,再执行 JS 。因而造成了之前例子的情况。

所以,看官大人明白为何 <script> 与 <link> 同时在头部的话, <script> 在上可能会更好了么?之所以是可能,是因为如果 <link> 的内容下载更快的话,是没影响的,但反过来的话, JS 就要等待了,然而这些等待的时间是完全不必要的。

JS

JS ,也就是 <script> 标签,估计大家都很熟悉了,不就是阻塞 DOM 解析和渲染么。然而,其中其实还是有一点细节可以考究一下的,我们一起来好好看看。

JS 阻塞 DOM 解析

首先我们需要一个新的 JS 文件名为 blok.js ,内容如下:

<body>
<div></div>
<script src="/js/sleep3000-logDiv.js"></script>
<style>
 div { background: lightgrey;
 } </style>
<script src="/js/sleep5000-logDiv.js"></script>
<link rel="stylesheet" href="/css/common.css"></body>

这个例子也是很极端的例子,但不妨碍它透露给我们很多重要的信息。想象一下,页面会怎样呢?

答案是先浅绿色,再浅灰色,最后浅蓝色。由此可见,每次碰到 <script> 标签时,浏览器都会渲染一次页面。这是基于同样的理由,浏览器不知道脚本的内容,因而碰到脚本时,只好先渲染页面,确保脚本能获取到最新的 DOM 元素信息,尽管脚本可能不需要这些信息。

小结

综上所述,我们得出这样的结论:

CSS 不会阻塞 DOM 的解析,但会阻塞 DOM 渲染。

JS 阻塞 DOM 解析,但浏览器会"偷看" DOM ,预先下载相关资源。

最后还是要推荐下我的前端群:524262608,不定期分享干货,包括我自己整理的一份2017最新的前端资料和零基础入门教程,欢迎初学和进阶中的小伙伴。

浏览器遇到 <script> 且没有 defer 或 async 属性的 标签时,会触发页面渲染,因而如果前面 CSS 资源尚未加载完毕时,浏览器会等待它加载完毕在执行脚本。

所以,你现在明白为何 <script> 最好放底部, <link> 最好放头部,如果头部同时有 <script> 与 <link> 的情况下,最好将 <script> 放在 <link> 上面了吗?

web前端学习方法经验可以关注我的微信公众号:‘学习web前端’,每天更新案例源码或经验分享,关注后回复‘给我资料’可以领取一套完整的学习视频