zoukankan      html  css  js  c++  java
  • 异步操作

    单线程模型

             单线程模型指的是,JavaScript 只在一个线程上运行。JavaScript只能执行一个任务,其他任务都必须在后面排队等待。JavaScript 在一个线程上运行,并不是 JavaScript 引擎只有一个线程。JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。之所以采用单线程,而不是多线程,JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,就太复杂。

    好处:是实现起来比较简单,执行环境相对单纯;

    坏处:是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。

    同步任务和异步任务

    程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。

    同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。

    异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。

    任务队列和事件循环

    JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。

    异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。

    JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)

     

    异步操作的模式

    回调函数

    回调函数是异步操作最基本的方法。

    比如两个函数f1和f2,编程的意图是f2必须等到f1执行完成,才能执行。

    事件监听

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

    还是以f1和f2为例。首先,为f1绑定一个事件

    f1.on('done', f2);

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

    发布/订阅

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

    异步操作的流程控制

    如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序。

    串行执行

    我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。

    并行执行

    流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行final函数。

    并行与串行的结合

    所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务,这样就避免了过分占用系统资源。

    定时器

    JavaScript 提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()setInterval()这两个函数来完成。它们向任务队列添加定时任务。

    setTimeout()

    setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。注意的是,函数指定的代码必须是字符串形式,如果是个函数可以直接使用函数名,如果第二个参数没有写,默认为0。

    var timerId = setTimeout(func|code, delay);

    console.log(1);

    setTimeout('console.log(2)',1000);

    console.log(3);

    // 1

    // 3

    // 2

    setInterval()

    setInterval函数的用法与setTimeout完全一致,区别仅仅在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行,直到关闭当前窗口,还有一点就是时间他的时间间隔包括数据处理的时间,所以往往实际的时间间隔小于设置的时间间隔,而setTimeout就不会这样。

    var i = 1

    var timer = setInterval(function() {

      console.log(2);

    }, 1000)

     

    clearTimeout()clearInterval()

    setTimeoutsetInterval函数,都返回一个整数值,表示计数器编号。将该整数传入clearTimeoutclearInterval函数,就可以取消对应的定时器。setTimeout和setInterval返回的整数值是连续的,因此也可以设置条件来取消定时器。

    var id1 = setTimeout(f, 1000);

    var id2 = setInterval(f, 1000);

    clearTimeout(id1);

    clearInterval(id2);

    上面代码中,回调函数f不会再执行了,因为两个定时器都被取消了。

     

    实例:debounce 函数

    有时,我们不希望回调函数被频繁调用。比如用户连续击键,就会连续触发点击事件,造成大量的 Ajax 通信。这是不必要的,而且很可能产生性能问题。可以设置一个门槛值,表示两次 Ajax 通信的最小间隔时间。如果在间隔时间内,发生新的点击事件,则不触发 Ajax 通信,并且重新开始计时。如果过了指定时间,没有发生新的keydown事件,再将数据发送出去。这种做法叫做 debounce(防抖动)。假定两次 Ajax 通信的间隔不得小于2500毫秒,上面的代码可以改写成下面这样。

    $('textarea').on('keydown', debounce(ajaxAction, 2500));

    function debounce(fn, delay){

      var timer = null; // 声明计时器

      return function() {

        var context = this;

        var args = arguments;

        clearTimeout(timer);

        timer = setTimeout(function () {

          fn.apply(context, args);

        }, delay);

      };

    }

     

    运行机制

    setTimeoutsetInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。这意味着,setTimeoutsetInterval指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeoutsetInterval指定的任务,一定会按照预定时间执行。

    setTimeout(someTask, 100);

    veryLongTask();

    上面代码的setTimeout,指定100毫秒以后运行一个任务。但是,如果后面的veryLongTask函数(同步任务)运行时间非常长,过了100毫秒还无法结束,那么被推迟运行的someTask就只有等着,等到veryLongTask运行结束,才轮到它执行。

    Promise 对象

    Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。

    Promise 是一个对象,也是一个构造函数。

    function f1(resolve, reject) {

      // 异步代码...

    }

    var p1 = new Promise(f1);

    Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数。

    var p1 = new Promise(f1);

    p1.then(f2);

    上面代码中,f1的异步操作执行完成,就会执行f2。

    传统的写法可能需要把f2作为回调函数传入f1,比如写成f1(f2),异步操作完成后,在f1内部调用f2。Promise 使得f1和f2变成了链式写法。不仅改善了可读性,而且对于多层嵌套的回调函数尤其方便。

    Promise 对象的状态

    Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。

    • 异步操作未完成(pending)
    • 异步操作成功(fulfilled)
    • 异步操作失败(rejected)

    上面三种状态里面,fulfilled和rejected合在一起称为resolved(已定型)。

    这三种的状态的变化途径只有两种。

    从“未完成”到“成功”

    从“未完成”到“失败”

    一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。因此,Promise 的最终结果只有两种。

    • 异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled。
    • 异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected。

    Promise 构造函数

    JavaScript 提供原生的Promise构造函数,用来生成 Promise 实例。

    var promise = new Promise(function (resolve, reject) {

      // ...

      if (/* 异步操作成功 */){

        resolve(value);

      } else { /* 异步操作失败 */

        reject(new Error());

      }

    });

    resolve函数的作用是,将Promise实例的状态从“未完成”变为“成功”(即从pending变为fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。reject函数的作用是,将Promise实例的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

    Promise.prototype.then()

    Promise 实例的then方法,用来添加回调函数。

    then方法可以接受两个回调函数,第一个是异步操作成功时(变为fulfilled状态)的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。

    var p1 = new Promise(function (resolve, reject) {

      resolve('成功');

    });

    p1.then(console.log, console.error);

    // "成功"

    var p2 = new Promise(function (resolve, reject) {

      reject(new Error('失败'));

    });

    p2.then(console.log, console.error);

    // Error: 失败

    实例:图片加载

    下面是使用 Promise 完成图片的加载。

    var preloadImage = function (path) {

      return new Promise(function (resolve, reject) {

        var image = new Image();

        image.onload  = resolve;

        image.onerror = reject;

        image.src = path;

      });

    };

    上面代码中,image是一个图片对象的实例。它有两个事件监听属性,onload属性在图片加载成功后调用,onerror属性在加载失败调用。

    promise小结

    Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。

    而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。

    Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。

     
  • 相关阅读:
    J
    I
    uva122 二叉树的实现和层次遍历(bfs)
    A
    HDU 波峰
    2239: 童年的圣诞树
    1734: 堆(DFS)
    1731: 矩阵(前缀和)
    1733: 旋转图像(模拟)
    1728: 社交网络(概率问题 组合数/排列数)
  • 原文地址:https://www.cnblogs.com/hjy-21/p/12324568.html
Copyright © 2011-2022 走看看