JavaScript异步编程

本文主要探讨JavaScript异步编程。介绍了同步与异步的概念,解释了JavaScript采用异步编程的原因是避免单线程执行时阻塞UI更新和事件处理。阐述了异步运行机制,还详细讲解了4种异步编程方式,包括回调函数、事件监听、发布订阅和Promises对象。

参考:
浅析JavaScript异步
Javascript异步编程的4种方法
promise详解

一、同步&异步

同步——同步会逐行执行代码,会对后续代码造成阻塞,直至代码接收到预期的结果之后,才会继续向下执行。
异步——如果在函数返回的时候,调用者还不能够得到预期结果,而是将来通过一定的手段得到结果(例如回调函数),这就是异步。

二、JavaScript为什么要采用异步编程?

JavaScript是一门单线程执行脚本语言,它的单线程体现在任何一个函数都需要从头到尾执行完毕后,才会执行另外一个函数。假如有一段 JavaScript从头到尾执行时间比较长,那么在执行期间任何 UI更新都会被阻塞,界面事件处理也会停止响应。这种情况下就需要异步编程模式,目的就是把代码的运行打散或者让 IO调用(例如 AJAX)在后台运行,让界面更新和事件处理能够及时地运行。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Javascript异步</title>
</head>
<body>
<button onclick="updateSync()">同步</button>
<button onclick="updateAsync()">异步</button>
<div id="animate">animate</div>
<div id="output"></div>
<script>
  function updateSync() {
    for (var i = 0; i < 1000000; i++) {
      document.getElementById('output').innerHTML = i;
    }
  }

  function updateAsync() {
    var i = 0;

    function updateLater() {
      document.getElementById('output').innerHTML = (i++);
      if (i < 1000000) {
        setTimeout(updateLater, 0);
      }
    }

    updateLater();
  }
</script>
</body>
</html>

点击 同步按钮会调用 updateSync的同步函数,逻辑非常简单,循环体内每次更新 output结点的内容为 i。如果在其他多线程模型下的语言,你可能会看到界面上以非常快的速度显示从 0到 999999后停止。但是在 JavaScript中,你会感觉按钮按下去的时候卡了一下,然后看到一个最终结果 999999,而没有中间过程,这就是因为在 updateSync函数运行过程中 UI更新被阻塞,只有当它结束退出后才会更新 UI。反之,当点击 异步的时候,会明显的看到 Dom在逐步更新的过程。

三、异步运行机制

  1. 所有同步任务都在主线程上执行,形成一个执行栈。

  2. 主线程之外,还存在一个 任务队列。只要异步任务有了运行结果,就在 任务队列之中放置一个事件。

  3. 一旦 执行栈中的所有同步任务执行完毕,系统就会读取 任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

    主线程不断重复上面的第三步。

四、异步编程方式

  • 回调函数
    假设有两个函数,f1()和f2(), f2需要等待f1()的执行结果

    f1()
    f2()

    如果f1()是一个耗时的任务,则可以考虑将f1()改写,f2()写成f1()的回调函数

    function f1(cb){
        setTimeout(function () {
          // f1的任务代码
          cb();
        }, 1000);
     } 
     
    f1(f2)

    采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
    回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。

  • 事件监听
    另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

    还是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的写法)。

    f1.on(‘done’, f2);

    上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:

    function f1(){
        setTimeout(function () {
          // f1的任务代码
          f1.trigger(‘done’);
        }, 1000);
      }

    f1.trigger(‘done’)表示,执行完成后,立即触发done事件,从而开始执行f2。

    这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

  • 发布订阅
    上一节的"事件",完全可以理解成"信号"。

    我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

    这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。

    首先,f2向"信号中心"jQuery订阅"done"信号。

    jQuery.subscribe(“done”, f2);

    然后,f1进行如下改写:

    function f1(){
        setTimeout(function () {
          // f1的任务代码
          jQuery.publish(“done”);
        }, 1000);
      }

    jQuery.publish(“done”)的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
    此外,f2完成执行后,也可以取消订阅(unsubscribe)。

    jQuery.unsubscribe(“done”, f2);

    这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

  • Promises对象
    Promise 实际就是一个对象, 从它可以获得异步操作的消息,Promise 对象有三种状态,pending(进行中)、fulfilled(已成功)和rejected(已失败)。Promise 的状态一旦改变之后,就不会在发生任何变化,将回调函数变成了链式调用。它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。

    比如,f1的回调函数f2,可以写成:

    f1().then(f2);

    Promise 封装异步请求demo:

function f1(URL) {
    return new Promise((resolve, reject) => {
      axios.get(URL).then(res => {
        resolve(res)
      }).catch(err => {
        reject(err)
      })
    })
  }
  f1('/api/').then(res => {
    f2(res)
  }).catch(err => {
    console.log(err);
  })
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值