现在的位置: 首页 > web前端 > 正文

JavaScript异步是什么?

2020年02月12日 web前端 ⁄ 共 4198字 ⁄ 字号 评论关闭

  我们知道,JavaScript 语言的一大特点是单线程,这是由它最初的应用场景决定的。它最初作为浏览器的脚本语言,用来与用户进行交互,并且可以用来操作 DOM。如果它是多线程的,可能会带来复杂的冲突,因此 JavaScript 最初被设计时即为单线程的。

  虽然在 HTML5 标准中新增了 Web Worker 的概念,它允许 JavaScript 创建多个线程,但这些子线程完全受主线程的控制,且不能操作 DOM,因此本质上 JavaScript 还是单线程的。在 JavaScript 中,除主线程外,还存在一个任务队列,主线程循环不断地从任务队列中读取事件,这整个运行机制被称为事件循环,事件循环的过程在这里就不展开讨论了。

  在主线程上的任务是排队执行的,只有前一个任务完成了才会执行后一个任务,这些任务是“同步”的;而任务队列中的任务(如定时器、网络请求、Promise 等)只有在满足条件时才会被加入到主线程中执行,在满足条件之前不会阻塞主线程中的任务,这些任务是“异步”的。从执行顺序来说,同步和异步的特点是:

  同步:从上到下执行,便于理解,写起来方便,但下一条语句需要等待上一条完成后才能执行;

  异步:遇到异步任务可以继续往下执行,等到异步任务完成了再执行特定的语句,但代码写起来稍微复杂一些。

  因此我们有个小小的愿望——如果能用同步的写法来实现异步就好了。下面开始介绍 JavaScript 异步编程方法的发展之路。

  一、回调函数

  1.回调函数的简单用法

  const fn = _ => {

  console.log('JavaScript yes!')

  }

  console.log('start')

  setTimeout(fn, 500)

  console.log('end')

  // start

  // end

  // JavaScript yes! (about 500ms later)

  其中 fn 即为 回调函数。从该例子中可以看到,执行了 setTimeout 后,线程并未阻塞在其中,而是继续往下执行,打印出了“end”后经过约 500ms,回调函数执行,打印出 "JavaScript yes!"。

  2.异步网络请求

  举一个异步网络请求的例子,假设有一个 score.json 数据,我们通过 XMLHttpRequest 发起异步请求,并在成功返回数据时,以返回数据为参数调用传入的回调函数。

  // score.json

  {

  "name": "Daniel",

  "score": 95

  }

  // loadData.js

  // 参数 callback 即为回调函数

  const loadData = (item, callback) => {// line: 9

  if (item === 'score') {

  let xhr = new XMLHttpRequest()

  xhr.open('GET', './score.json') xhr.onreadystatechange = function () {

  // 待到结果返回时,调回调函数

  if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {

  callback(xhr.responseText)// line: 16

  }

  }

  xhr.open()

  }

  }

  const displayData = data => {// line: 23

  console.log(`data: ${data}`)

  }

  console.log('start')

  loadData('score', displayData)// line: 28

  console.log('end')

  /*startenddata: { "name": "Daniel", "score": 95}*/

  第 9 行处, loadData 函数的第二个参数为 callback ,即回调函数。第 28 行处,调用 loadData 函数时,传入的第二个参数为 displayData ,此函数(第 23 行)接收一个参数并打印输出。在 loadData 函数体内,第 16 行处,待到结果返回时,以 xhr.responseText 为参数调用了callback 函数,即 displayData 函数。于是打印出了:

  data: { "name": "Daniel", "score": 95}

  3.比较麻烦的情况

  当连续出现“后一个异步操作依赖上一个异步操作的返回结果”时,回调函数会变得难以使用。

  load('score', data => { console.log(`score: ${data.score}`) if (data.score < 60) {sendToMon(data.score, res => { console.log(`message: ${res}`) sendToTeacher(res, comment => { console.log(`comment: ${comment}`) showComment(comment, state => { if (state === 'success') { console.log('complete') } }) }) }) }})

  4.小结

  回调函数能够实现异步处理,但存在一些问题(“回调地狱”):

  一层层回调函数堆叠起来,不利于代码的维护;

  结构混乱,逻辑耦合强,不利于错误处理;

  代码横向发展,不利于阅读。

  二、Promise

  1.Promise 的简单用法

  let p = new Promise((resolve, reject) => { console.log('start') setTimeout(_ => { reject(2333) }, 500) console.log('end')})p.then(data => { console.log(`data: ${data}`)}, err => { console.log(`error: ${err}`)})// start// end// error: 2333

  p 是我们定义的 Promise 实例,Promise 接收一个函数作为参数,该函数有两个参数,分别为 resolve 和 reject ,他们也都是函数,由 JS 内部实现,在不考虑内部原理、仅作使用时无需考虑具体实现方法。 resolve 函数可以将 Promise 实例的状态由 pending 变为 resolved ,其参数为异步操作成功时的值 value ; reject 函数可以将 Promise 实例的状态由 pending 变为rejected ,其参数为异步操作失败时的原因 reason 。

  作为 Promise 的实例, p 拥有 then 方法,该方法接收两个函数作为参数,分别为 onResolved 和 onRejected ,当 p 的状态由 pending 变为 resolved 或 rejected 时,会调用相应的 onResolved 或 onRejected ,调用时的参数为上一段中的 value 或 reason 。

  在这个例子中,在 500ms 后 p 以 2333 为原因将状态由 pending 变为 rejected ,并以 2333 为参数调用 then 的第二个参数中的函数,即:

  err => { console.log(`error: ${err}`)}

  于是打印出了 error: 2333 (注意,定义 p 时的代码是同步执行的,因此会先输出 start 和end )。

  2.Promise/A+规范

  Promise 的实例有三种状态: pending 、 fulfilled 和 rejected 。初始状态为 pending ,该状态可以变为 fulfilled 或 rejected ,状态一旦变化便不可再次改变;且 fulfilled 的 value 和 rejected 的 reason 不可再改变。( fulfilled 即为 resolved )

  Promise 的实例会有一个 then 方法,该方法接收两个参数,分别为成功或失败时的回调函数: promise.then(onFullfilled, onRejected) 。promise 的 then 方法会返回一个新的 Promise 实例(因此可以继续使用 then 等方法进行链式调用)。

  当一个 promise 成功时,会调用其 then 方法中的成功回调,参数 value 为 resolve 的值

  当一个 promise 失败时,会调用其 then 方法中的失败回调,参数 reason 为 reject 的值

  3.ES6 Promise

  在 ES6 中,JavaScript 对 Promise/A+ 规范进行了实现,还增加了一些 额外的方法 ,如 Promise.prototype.catch 、 Promise.prototype.finally 、 Promise.resolve 、 Promise.reject 、Promise.all 、 Promise.any 和 Promise.race 等等。

  4.一个小小的思考题

  上面提到, then 方法会返回一个新的 Promise 实例,其实 catch 方法也会返回一个新的 Promise 实例。假设我们有:

  let p1 = Promise.reject(1).catch(err => { console.log(err) })

  那么 p1 的状态是什么呢? resolved ? rejected ?思考并尝试一下吧。

  5.Promise 版的 load

  回调函数一节中 load 的例子如果用 Promise 实现,则会简洁很多:

  // 此例子中省略了失败回调函数 onRejectedload('score').then(data => { console.log(`score: ${data.score}`) if (data.score < 60) { return sendToMon(data.score) }}).then(res => { console.log(`message: ${res}`) return sendToTeacher(res)}).then(comment => { console.log(`comment: ${comment}`) return showComment(comment)}).then(state => { if (state === 'success') { console.log('complete') }})

  不再有多层的嵌套,不再有数不过来的括号,逻辑更清晰,代码不再像回调函数那样横向发展。

抱歉!评论已关闭.