jQuery异步编程 – deferred、promise、callback



jQuery异步编程 – deferred、promise、callback

0 2


jQuery-deferrred


On Github zodiac-xl / jQuery-deferrred

jQuery异步编程

deferred、promise、callback

Created by xiaolong / @zodiac-xl

  • 异步编程的背景
  • javascript异步编程的发展
  • jQuery异步编程
  • jQuery callbacks补充

背景

Javascript语言的执行环境是"单线程"(single thread)。一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

单线程

  • 好处:实现简单,执行环境单纯
  • 坏处:阻塞

同步和异步

为了解决阻塞,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

"异步模式"编程的发展

经常的我们用callback实现异步,你还知道多少异步方案呢?

回调函数callback 事件监听 publish/subscribe 发布/订阅模式 Promise对象

一、回调函数callback 回调黑洞 回调金字塔

doAsync1(function () {
  doAsync2(function () {
    doAsync3(function () {
      doAsync4(function () {
    })
  })
})
  • 优点:简单、容易理解和部署

  • 缺点:不利于代码阅读和维护,各部分高度耦合,流程混乱,且每个任务只能指定一个回调函数

二、事件监听

f1.on('done1', f2);
f1.on('done2', f3);

function f1(){
    // f1的任务代码
    f1.trigger('done1');
    f1.trigger('done2');
}

执行晚f1的代码后触发f1的done事件,调用f2

  • 优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。

  • 缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰。

三、publish/subscribe 发布/订阅模式

上一节的"事件"可以理解成"信号"。

假定,存在一个"信号中心" 某个任务执行完成,就向信号中心"发布"(publish)一个信号 其他任务可以向信号中心"订阅"(subscribe)这个信号 当"信号中心"接受一个"发布"信号时,通知相应“订阅者”开始执行任务

又称"观察者模式"。

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

 jQuery.subscribe("done", f2);

然后,f1发布信号:

 function f1(){
         jQuery.publish("done");
 }

 f1执行完成后,向"信号中心"jQuery发布"done"信号,
 从而引发f2的执行。

取消订阅。

 jQuery.unsubscribe("done", f2);

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

四、Promise对象

Promise对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。

  • 思想:每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。
  • 示例:f1的回调函数f2:

    f1().then(f2);

f1要进行如下改写:

function f1(){
    var dfd = $.Deferred();
        setTimeout(function () {
            // f1的任务代码
            dfd.resolve();
    }, 0);
    return dfd.promise;
}
  • 优点:

  • 回调函数变成了链式写法,程序的流程清楚,而且有整套的配套方法,可以实现许多强大的功能。

  • 指定多个回调函数:

    f1().then(f2).then(f3);

  • 多种类型的回调:

    f1().then(f2).fail(f3).always();

  • 如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。不用担心是否错过了某个事件或信号

  • 缺点:不易理解和编写

jQuery异步编程
  • jQuery deferred的诞生
  • deferred的思想
  • jQuery deferred主要功能
  • 发展趋势

一、jQuery deferred的诞生

场景:

我们需要依次:

ajax从服务器获取A loop遍历B ajax从服务器获取C loop遍历D

what we do ?

通常的做法是,为它们指定回调函数(callback)

ajax(loop(ajax(loop)))

进化版:

ajax(
    loop(
        ajax(
            loop
            )
        )
)

jQuery deferred的应需而生

deferred对象是jQuery的回调函数解决方案。

  • defer的翻译是"延迟",所以deferred对象的含义就是"延迟"到未来某个点再执行。

  • 它解决了如何处理耗时操作的问题,对那些操作提供了更好的控制,以及统一的编程接口。

jQuery deferred思想

jQuery规定,deferred对象有三种执行状态----未完成,已完成和已失败。

  • 如果执行状态是"已完成"(resolved),deferred对象立刻调用done()方法指定的回调函数;
  • 如果执行状态是"已失败",调用fail()方法指定的回调函数;
  • 如果执行状态是"未完成",则继续等待,或者调用progress()方法指定的回调函数(jQuery1.7版本添加)。

简易的Deferred

var Deferred = function () {
    var callbacks = [];
    return {
        done: function (fn) {
            callbacks.push(fn);
        },
        resolve: function (arg) {
            $.each(callbacks,function(index,callback){
                callback(arg)
            })

        }
    };
};

实例:

var fn1 = function(){    
        var dtd = $.Deferred(); // 新建一个deferred对象
        var tasks = function(){
          alert("执行完毕!");
          dtd.resolve(); // 改变deferred对象的执行状态
        };
        setTimeout(tasks,5000);
        return dtd;
};

$.ajax().done(fn);
fn1.done();

ajax和普通方法都必须返回一个deferred对象

分析

    var fn1 = function(){    
            var dtd = $.Deferred();
            var tasks = function(){
              dtd.resolve(); 
            };
            setTimeout(tasks,5000);
            return dtd;
    };

fn1.done();

steps:

fn1 return dtd dtd.done 注册done事件 执行fn1 fn1完成后dtd.resolve 触发done事件

jQuery deferred主要功能

回调的的链式写法
 $.ajax().done()
指定同一操作的多个回调函数
 $.ajax().done().fail()
为多个操作指定回调函数
 $.when($.ajax(),$.ajax).done()
普通操作的回调函数接口
 $.when(wait()).done()

一、ajax操作的链式写法

jQuery的ajax操作的传统写法:

$.ajax({
    url: "test.html",
    success: function(){
      alert("哈哈,成功了!");
       },
      error:function(){
        alert("出错啦!");
      }
});      

现在,新的写法是这样的:

$.ajax("test.html")
  .done(function(){ alert("哈哈,成功了!"); })
  .fail(function(){ alert("出错啦!"); });

二、指定同一操作的多个回调函数

deferred对象的一大好处,就是它允许你自由添加多个回调函数。

$.ajax("test.html")
  .done(function(){ alert("哈哈,成功了!");} )
  .fail(function(){ alert("出错啦!"); } )
  .done(function(){ alert("第二个回调函数!");} );

三、为多个操作指定回调函数

deferred对象的另一大好处,就是它允许你为多个事件指定一个回调函数,这是传统写法做不到的。

$.when($.ajax("test1.html"), $.ajax("test2.html"))
  .done(function(){ alert("哈哈,成功了!"); })
  .fail(function(){ alert("出错啦!"); });      

B:ajax是jQuery包装好的返回deferred对象的方法

Q:如何让普通的方法也能享受deferred的便易呢?

四、普通操作的回调函数接口

deferred对象的最大优点,就是它把这一套回调函数接口,从ajax操作扩展到了所有操作

我们来看一个具体的例子。假定有一个很耗时的操作wait:

var wait = function(){
    var tasks = function(){
      alert("执行完毕!");
    };
    setTimeout(tasks,5000);
  };  

   我们为它指定回调函数,应该怎么做呢?

很自然的,可以使用$.when():

$.when(wait())
  .done(function(){ alert("哈哈,成功了!"); })
  .fail(function(){ alert("出错啦!"); });

done()方法立即执行,没有起到回调函数的作用。

why?

原因在deferred思想中提到过 方法必须返回deferred对象,所以必须对wait()进行改写:

var dtd = $.Deferred(); // 新建一个deferred对象
var wait = function(){            
        var tasks = function(){
          alert("执行完毕!");
          dtd.resolve(); // 改变deferred对象的执行状态
        };
        setTimeout(tasks,5000);
        return dtd.promise();
};                

dtd.promise()在原来的deferred对象上返回另一个deferred对象, 只开放与改变执行状态无关的方法(比如done()方法和fail()方法 屏蔽与改变执行状态有关的方法(比如resolve()方法和reject()方法),从而使得执行状态不能被外部改变。

resolve()和reject()

  • ajax操作,deferred对象会根据返回结果,自动改变自身的执行状态;

  • 但是,在普通方法wait()函数中,这个执行状态必须由程序员手动指定。

  • dtd.resolve()的意思是,将dtd对象的执行状态从"未完成"改为"已完成",从而触发done()方法。

  • deferred.reject()方法,作用是将dtd对象的执行状态从"未完成"改为"已失败",从而触发fail()方法。

小结:deferred对象的方法

$.Deferred() deferred.done() deferred.fail() deferred.promise() deferred.resolve() deferred.reject() $.when() deferred.then(), deferred.always()

jqeury deferred和promise关系

JavaScript原生异步编程的Promise模式有着自己的规范Promises/A+

  • jQuery Deferred并没有按Promises/A+ 规范实现,但实现了类似的功能,
  • 为了兼容规范 jquery实现了deferred.promise()

promise规范有那些?

promise规范

  • 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)

  • 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换

  • promise必须实现then方法(可以说,then就是promise的核心)

  • then方法接受两个参数

思考

不管是deferer promise callbacks实质都是包装的回调的方法 只是更加友好 有没有跳出来的方法的

扩展 其他方法

ES6 generator ES7 async/await

基础

  • generator 生成器
  • 迭代器协议 iterator

generator 生成器 原理:把yield比作多个事物的原料 当所有yield原料都准备好了后 具体什么时候生产产品(执行)由我们决定 (顺序:依次生产)

    function* anotherGenerator(i) {
        yield i + 1;//index 2
        yield i + 2;//index 3
        yield i + 3;//index 4
    }

    function* generator(i){
        yield i; //index 1
        yield* anotherGenerator(i);//index 2-4
        yield i + 10;//index 5
    }

    var gen = generator(10);//返回一个这个生成器函数的迭代器(iterator)对象。
    console.log("--------------");
    console.log(gen.next().value); // 10
    console.log(gen.next().value); // 11
    console.log(gen.next().value); // 12
    console.log(gen.next().value); // 13
    console.log(gen.next().value); // 20
    console.log("--------------");

迭代器协议 iterator

ES6里的迭代器是一种协议 (protocol). 所有遵循了这个协议的对象都可以称之为迭代器对象.

核心:

next 的方法, 调用该方法后会返回一个拥有两个属性的对象:

  • value 可以是任意值
  • done 布尔值, 表示该迭代器是否已经被迭代完毕

      function iterator(){
          var index = 0;
    
          return {
              next: function(){
                  return {value: index++, done: false};
              }
          }
      }
    
      var it = iterator();
    
      console.log(it.next().value); // '0'
      console.log(it.next().value); // '1'
      console.log(it.next().value); // '2'
    

新一代异步嵌套方案

Generator 函数就是一个异步操作的容器,按照迭代器协议收集了所有待生产的原料 ? 如何控制生产流程

控制生产流程

回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。 Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。

现有方法:

使用co函数库

co 函数库其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个库。使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。

ES7 async/await

async/await

    var asyncReadFile = async function (){
          var f1 = await readFile('/etc/fstab');
          var f2 = await readFile('/etc/shells');
          console.log(f1.toString());
          console.log(f2.toString());
        };    

不同:async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await

优点:

内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器

更好的语义。 async 和 await,比起星号和 yield,语义更清楚了

更广的适用性。

  • co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,
  • async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,这时等同于同步操作)

前面deferred基本实现了异步编程的流程控制 为什么还需要callbacks呢?

是的 callbacks就是实现队列控制的

jQuery.Callback

jQuery.Callbacks()是在版本1.7中新加入的。 是一个多用途的回调函数列表对象,提供了一种强大的方法来管理回调函数队列。 如果需要控制一系列的函数顺序执行。那么一般就需要一个队列函数来处理这个问题

核心思想(可触发式队列)

var Callbacks = {
      callbacks: [],
      add: function(fn) {
        this.callbacks.push(fn);
      },
      fire: function() {
        this.callbacks.forEach(function(fn) {
          fn();
        })
      }
  }

WHAT Jquery DO?

添加懒人写法

添加扩展方法

jQuery.callbakcs可用参数

  • once: 保证callback list只能被执行一次
  • memory: 保存最后fire传入的值,并在list增加callback时使用该值执行callback
  • unique: 确保同一个callback只能被添加一次
  • stopOnFalse:当一个回调返回false时中止队列.

添加懒人写法

jQuery.Callbacks("once memory")

使用createOptions参数调整并缓存

jQuery.Callbacks({once:true,memory:true})

// String to Object options format cache
var optionsCache = {};  

// Convert String-formatted options into Object-formatted ones and store in cache
function createOptions( options ) {
    var object = optionsCache[ options ] = {};
    jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {
        object[ flag ] = true;
    });
    return object;
}

添加扩展方法

基础方法
  • add
  • fire
  • remove
追加:
  • has
  • empty
  • disable (Disable .fire and .add)
  • disabled
  • lock (Disable .fire)
  • locked
实用:
  • fireWith
总结
  • 异步编程的背景
    • javascript异步编程的发展
    • 回调函数callback
    • 事件监听
    • Pub/Sub发布/订阅
    • Promise对象
  • jQuery异步编程
  • deferred的诞生
  • deferred的思想
  • deferred的功能
  • 发展趋势
jQuery callbacks补充
思考

jquery的队列都是按序执行 为什么不用双向链表 方便实现队列的插入、删除等操作 改变执行顺序呢

jQuery异步编程 deferred、promise、callback Created by xiaolong / @zodiac-xl