JavaScript异步编程操作实现介绍

 更新时间:2022年09月09日 08:31:31   作者:ExMaterial  
异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。在我们学习的传统单线程编程中,程序的运行是同步的,同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行,而异步的概念则是不保证同步的概念

异步编程

目前主流的JavaScript执行环境都是以单线程执行JavaScript的。

JavaScript早期只是一门负责在浏览器端执行的脚本语言,主要用来操作DOM,如果其添加的同时又删除了DOM,浏览器就不知道该如何是好,所以其就被设计成为单线程模型。而随着JavaScript能做的事情越来越多,如果一直维持同步编程的话,就会导致浏览器卡在某个耗时操作无法进行下一步,造成浏览器假死的现象,影响用户体验。因此,异步编程应运而生。

同步模式与异步模式

同步模式(Synchronous)

同步模式是指代码是同步执行的,下一步的代码执行必须要等到上一步的代码完成之后才能执行,执行顺序就为代码的编写顺序。

console.log('global begin')
function bar() {
    console.log('bar task')
}
function foo() {
    console.log('foo task')
    bar()
}
foo()
console.log('global end')

异步模式(Asynchronous)

不会去等待这个任务的执行完成才去执行下一个任务,开启过后就立即开始下一个任务,后续逻辑一般会通过回调函数来进行定义。

console.log('global begin')
setTimeout(function timer1 () {
  console.log('timer1 invoke')
}, 1800)
setTimeout(function timer2 () {
  console.log('timer2 invoke')

  setTimeout(function inner () {
    console.log('inner invoke')
  }, 1000)
}, 1000)
console.log('global end')

回调函数

回调函数——所有异步编程方案的根基

其实回调函数就是封装你想要对某些数据进行的操作,等到你想要进行的操作结束后,再调用这个函数。

一讲起回调函数,面试中一般都会被问到,什么是回调地狱?如何解决回调地狱。以下面代码为例:

// 回调地狱,只是示例,不能运行
$.get('/url1', function (data1) {
  $.get('/url2', data1, function (data2) {
    $.get('/url3', data2, function (data3) {
      $.get('/url4', data3, function (data4) {
        $.get('/url5', data4, function (data5) {
          $.get('/url6', data5, function (data6) {
            $.get('/url7', data6, function (data7) {
              // 略微夸张了一点点
            })
          })
        })
      })
    })
  })
})

一大串的回调不仅难以阅读,当代码出现错误时,找出代码错误更是一种折磨。幸运的是,JavaScript是在不断发展的,在ES2015(ES6)中,出现了一种解决方法,妈妈再也不用担心我写代码碰到回调地狱了。

Promise

Promise——一种更优的异步编程统一方案

你可以把Promise理解成“承诺”或者“期约”(js高程作者的翻译,想了解的话,可以去看看红宝书第四版),你已经声明了这个东西,它在未来的时间一定会执行,你可以相信它。

首先,你要了解Promise是有三种状态,即pending(等待)、onFulfilled(完成)、onRejected(失败),完成或失败状态一旦确定,就是无法更改的。

Promise基本用法

const promise = new Promise(function (resolve, reject) {
  // 注意,要得到reject的结果时要先把resolved的代码注释掉,原因上面已经解释了
  resolve(100)    // 兑现承诺
  reject(new Error('promise rejected'))   // 承诺失败
})
promise.then(function(value) {
  console.log('resolved', value)
}, function(error) {
  console.log('rejected', error)
})
console.log('end')

每个new Promise都接受两个参数,第一个为兑现承诺的函数,会将函数中的值传递给promise实例,reject等同。而返回的promise又自带一个then方法,也接受两个参数,一个代表成功的回调,一个代表失败的回调。

Promise使用案例

在当前文件夹下新建一个文件夹,其中随便放两个json文件,用来模拟ajax请求。

// Promise 方式的 AJAX
function ajax(url) {
  return new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json'
    xhr.onload = function() {
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}
ajax('./api/posts.json').then(function (res) {
  console.log(res)
}, function(err) {
  console.log(err)
})

注意,上述代码应该在浏览器端运行。

Promise常见误区

有人学了promise之后,可能还是会写出这样的代码:

ajax('/api/urls.json').then(function (urls) {
  ajax(urls.users).then(function (users) {
    ajax(urls.users).then(function (users) {
      ajax(urls.users).then(function (users) {
        ajax(urls.users).then(function (users) {
        })
      })
    })
  })
})

说实话,这样写还不如不写。正经写法是链式调用,学过jQuery的同学应该不会陌生吧。

ajax('/api/users.json')
  .then(function (value) {
    console.log(1111)
    return ajax('/api/urls.json')
  }) // => Promise
  .then(function (value) {
    console.log(2222)
    console.log(value)
    return ajax('/api/urls.json')
  }) // => Promise
  .then(function (value) {
    console.log(3333)
    return ajax('/api/urls.json')
  }) // => Promise
  .then(function (value) {
    console.log(4444)
    return 'foo'
  }) // => Promise
  .then(function (value) {
    console.log(5555)
    console.log(value)
  })

Promise异常处理

通过.catch方法来捕获异常。

ajax('/api/users.json')
  .then(function onFulfilled (value) {
    console.log('onFulfilled', value)
    return ajax('/error-url')
  })
  .catch(function onRejected (error) {
    console.log('onRejected', error)
  })

其实catch方法和then方法实现差不太多,不过是then方法第一个参数传入undefine,一个语法糖而已。还有一个有意思的现象是,当中间的then出现错误时,会直接穿透到最后的catch方法,有兴趣了解怎么实现的可以去看看源码。相信你一定会有所收获。

Promise静态方法

  • Promise.resolve
Promise.resolve('foo')
  .then(function (value) {
    console.log(value)
  })

该方法会直接将传入的内容当作一个onFulfilled对象返回;其也可以传入一个Promise对象,直接返回Promise.resolve方法;传入一个带有then方法的函数也同理。

  • Promise.reject

该方法和上述一样调用,不过后面是接一个catch。

  • Promise.all

该方法接受一个数组,会等待数组内的方法全部调用完后再返回一个数组对象。

  • Promise.race

该方法会返回最先完成的promise。

宏任务与微任务

// 微任务
console.log('global start')
// setTimeout 的回调是 宏任务,进入回调队列排队
setTimeout(() => {
  console.log('setTimeout')
}, 0)
// Promise 的回调是 微任务,本轮调用末尾直接执行
Promise.resolve()
  .then(() => {
    console.log('promise')
  })
  .then(() => {
    console.log('promise 2')
  })
  .then(() => {
    console.log('promise 3')
  })
console.log('global end')

每次调用宏任务之前,都得确保微任务队列清空,所以也就能理解上面为什么会按照那样的顺序进行输出。

常见的宏任务有

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

常见的微任务有

  • promise
  • nextTick
  • mutationObserver

Generator 异步方案

ES6也推出了Generator异步解决方案。首先来看下生成器函数如何使用。

生成器函数回顾

function *foo() {
  console.log('satrt')
  try {
    const res = yield 'foo'
    console.log(res)
  } catch (e) {
    console.log(e)
  }
}
const generator = foo()
const result = generator.next()
console.log(result)
generator.throw(new Error('Generato error'))

生成器函数比普通函数多了个 * ,其放左放右都无所谓,我个人倾向于放右边。

其内部有一个next方法,返回一个对象,大概就是这个样子

{ value: 'foo', done: false }

value为yield返回的值,当然,如果你调用yield时传入了值,返回的值就是你传入的值。当执行完毕时,done就变成了true。

function * main () {
  try {
    const users = yield ajax('/api/users.json')
    console.log(users)
    const posts = yield ajax('/api/posts.json')
    console.log(posts)
    const urls = yield ajax('/api/urls11.json')
    console.log(urls)
  } catch (e) {
    console.log(e)
  }
}
function co (generator) {
  const g = generator()
  function handleResult (result) {
    if (result.done) return // 生成器函数结束
    result.value.then(data => {
      handleResult(g.next(data))
    }, error => {
      g.throw(error)
    })
  }
  handleResult(g.next())
}
co(main)

你只要理解了这个,即要通过你调用next方法才会进行到下一步,否则代码就会停在yield那里。不过,你这样每次享受优美代码时都还是需要自己编写一个co函数,未免有点太过麻烦。不过不用担心,async就要出场了。

async与await

async、await——可能是异步的终极解决方案

async function main () {
  try {
    const users = await ajax('/api/users.json')
    console.log(users)
    const posts = await ajax('/api/posts.json')
    console.log(posts)
    const urls = await ajax('/api/urls.json')
    console.log(urls)
  } catch (e) {
    console.log(e)
  }
}

你只需要在函数定义前加一个async,并在你想要等待的完成操作后的函数前加一个await,就可以实现同步的书写代码而异步调用,怎么样?这是不是更加优雅方便了呢?鉴于目前ECMAScript的发展趋势,保不准哪一天不需要async,直接用await就能实现异步编程了。

到此这篇关于JavaScript异步编程操作实现介绍的文章就介绍到这了,更多相关JS异步编程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • javascript 自定义常用方法

    javascript 自定义常用方法

    在实际的js开发过程中,我们常常会有相似或相同的需求。这时候如果没有很好的封装(通用功能),代码的重复将不可避免。
    2009-08-08
  • js与applet相互调用的方法

    js与applet相互调用的方法

    这篇文章主要介绍了js与applet相互调用的方法,结合实例形式分析了js调用java的applet以及java调用js的实现技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2016-06-06
  • js实现简易点击切换显示或隐藏

    js实现简易点击切换显示或隐藏

    这篇文章主要为大家详细介绍了js实现简易点击切换显示或隐藏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-11-11
  • js实现将json数组显示前台table中

    js实现将json数组显示前台table中

    本文主要介绍了把JSON数组显示在前台的table中的方法。具有一定的参考价值,下面跟着小编一起来看下吧
    2017-01-01
  • 微信小程序项目实践之九宫格实现及item跳转功能

    微信小程序项目实践之九宫格实现及item跳转功能

    这篇文章主要介绍了微信小程序项目实践之九宫格实现及item跳转功能,需要的朋友可以参考下
    2018-07-07
  • js实现多个标题吸顶效果

    js实现多个标题吸顶效果

    这篇文章主要为大家详细介绍了js实现多个标题吸顶效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-01-01
  • JS实现横向轮播图(初级版)

    JS实现横向轮播图(初级版)

    这篇文章主要为大家详细介绍了JS实现横向轮播图的初级版,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-01-01
  • 小程序实现左滑删除功能

    小程序实现左滑删除功能

    这篇文章主要为大家详细介绍了小程序实现左滑删除功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-10-10
  • 基于pdf.js实现文本划词高亮效果

    基于pdf.js实现文本划词高亮效果

    最近有一个需求,需要对于pdf文本进行操作,对接ai大模型对pdf文档进行高效解读,其中一个功能就是对于pdf的文本进行划词高亮,用户可进行阅读标记,本文给大家介绍了如何基于pdf.js实现文本划词高亮效果,需要的朋友可以参考下
    2024-05-05
  • javascript获取元素文本内容的通用函数

    javascript获取元素文本内容的通用函数

    获取元素文本内容的通用函数,思路很好值得参考。
    2009-12-12

最新评论