首頁 web前端 js教程 JavaScript 非同步程式設計之 jsdeferred 原理解析

JavaScript 非同步程式設計之 jsdeferred 原理解析

Nov 07, 2016 pm 05:26 PM
javascript

1. 前言

最近在看司徒正美的《JavaScript框架設計》,看到異步編程的那一章介紹了jsdeferred這個庫,覺得很有意思,花了幾天的時間研究了一下代碼,在此做一下分享。

非同步程式設計是編寫js的一個很重要的理念,特別是在處理複雜應用的時候,非同步程式設計的技巧就至關重要。那麼下面就來看看這個被稱為里程碑式的非同步程式庫吧。

2. API原始碼解析

2.1 建構子

這裡使用了安全的建構函數,避免了在沒有使用new呼叫建構子時出錯的問題,提供了兩個形式倆取得Deferred物件實例。

function Deferred() {
    return (this instanceof Deferred) ? this.init() : new Deferred();
}

// 方式1 
var o1 = new Deferred();
// 方式2
var o2 = Deferred();
登入後複製

2.2 Deferred.define()

這個方法可以包裝一個對象,指定對象的方法,或者將Deferred對象的方法直接暴露在全局作用域下,這樣就可以直接使用。

Deferred.methods = ["parallel", "wait", "next", "call", "loop", "repeat", "chain"];
/*
    @Param obj 赋予该对象Deferred的属性方法
    @Param list 指定属性方法
*/
Deferred.define = function(obj, list){
    if(!list)list = Deferred.methods;
    // 获取全局作用域的技巧,利用立即执行函数的作用域为全局作用域的技巧
    if(!obj) obj = (function getGlobal(){return this})();
    // 将属性都挂载到obj上
    for(var i = 0; i < list.length; i++){
        var n = list[i];
        obj[n] = Deferred[n];
    }
    return Deferred;
}

this.Deferred = Deferred;
登入後複製

2.3 非同步的操作實作

在JSDeferred中有許多非同步操作的實作方式,也是作為這個框架最為出彩的地方,方法依序是:

script.onreadystatechange(針對IE5.55~8)

script.onreadystatechange(針對IE5.55~8)

.onerror/img.onload(針對現代瀏覽器的非同步操作方法)

針對node環境的,使用process.nextTick來實現異步調用(已經過時)

setTimeout(default)

它會視瀏覽器選擇最最喜歡瀏覽器快的API。

使用script的onreadystatechange事件來進行,需要注意的是由於瀏覽器對並發請求數有限制,(IE5.5~8為2~3,IE9+和現代瀏覽器為6),當並發請求數大於上限時,會讓請求的發起操作排隊執行,導致延遲更嚴重。程式碼的想法是以150ms為一個週期,每個週期以透過setTimeout發起的非同步執行為起始,週期內的其他非同步執行操作透過script請求實現,如果此方法被頻繁調用的話,說明達到並發請求數上限的可能性越高,因此可以下調週期時間,例如設為100ms,避免因排隊而導致的高延遲。

Deferred.next_faster_way_readystatechange = ((typeof window === "object") && 
(location.protocol == "http:") && 
!window.opera &&
/\bMSIE\b/.test(navigator.userAgent)) &&
function (fun) {
var d = new Deferred();
var t = new Date().getTime();
if(t - arguments.callee._prev_timeout_called < 150){
var cancel = false; // 因为readyState会一直变化,避免重复执行
var script = document.createElement("script");
script.type = "text/javascript";
// 发送一个错误的url,快速触发回调,实现异步操作
script.src = "data:text/javascript,";
script.onreadystatechange = function () {
    if(!cancel){
        d.canceller();
        d.call();
    }
};

d.canceller = function () {
    if(!cancel){
        cancel = true;
        script.onreadystatechange = null;
        document.body.removeChild(script);// 移除节点
    }
};

// 不同于img,需要添加到文档中才会发送请求
document.body.appendChild(script);
} else {
// 记录或重置起始时间
arguments.callee._prev_timeout_called = t; 
// 每个周期开始使用setTimeout
var id = setTimeout(function (){ d.call()}, 0);
d.canceller = function () {clearTimeout(id)};
}
if(fun)d.callback.ok = fun;
return d;
}
登入後複製

使用img的方式,利用src屬性報錯和綁定事件回呼的方式來進行非同步操作

Deferred.next_faster_way_Image = ((typeof window === "object") &&
(typeof Image != "undefined") && 
!window.opera && document.addEventListener) && 
function (fun){
var d = new Deffered();
var img = new Image();
var hander = function () {
d.canceller();
d.call();
}
img.addEventListener("load", handler, false);
img.addEventListener("error", handler, false);

d.canceller = function (){
img.removeEventListener("load", handler, false);
img.removeEventListener("error", handler, false);
}
// 赋值一个错误的URL
img.src = "data:imag/png," + Math.random();
if(fun) d.callback.ok = fun;
return d;
}
登入後複製

針對Node環境的,使用process.nextTick來實現非同步呼叫

Deferred.next_tick = (typeof process === &#39;object&#39; &&
typeof process.nextTick === &#39;function&#39;) && 
function (fun) {
var d = new Deferred();
process.nextTick(function() { d.call() });
if (fun) d.callback.ok = fun;
return d;
};
登入後複製

setTimeout的方式,這種方式有一個觸發最小的時間間隔,在舊的IE瀏覽器中,時間間隔可能會稍微長一點(15ms)。

Deferred.next_default = function (fun) {
var d = new Deferred();
var id = setTimeout(function(){
clearTimeout(id);
d.call(); // 唤起Deferred调用链
}, 0)
d.canceller = function () {
try{
    clearTimeout(id);
}catch(e){}
};
if(fun){
d.callback.ok = fun;
}
return d;
}
登入後複製

預設的順序為

Deferred.next = 
    Deferred.next_faster_way_readystatechange || // 处理IE
    Deferred.next_faster_way_Image || // 现代浏览器
    Deferred.next_tick || // node环境
    Deferred.next_default; // 默认行为
登入後複製

根據JSDeferred官方的數據,使用next_faster_way_readystatechange和next_faster_way_Image這兩個比原有的setTimeout異步的方式快上700%以上。

看了一下數據,其實對比的瀏覽器版本都相對比較舊,在現代的瀏覽器中效能提升應該就沒有那麼明顯了。

2.4 原型方法

🎜Deferred的原型方法中實現了🎜

_id 用来判断是否是Deferred的实例,原因好像是Mozilla有个插件也叫Deferred,因此不能通过instanceof来检测。cho45于是自定义标志位来作检测,并在github上提交fxxking Mozilla。

init 初始化,给每个实例附加一个_next和callback属性

next 用于注册调用函数,内部以链表的方式实现,节点为Deferred实例,调用的内部方法_post

error 用于注册函数调用失败时的错误信息,与next的内部实现一致。

call 唤起next调用链

fail 唤起error调用链

cancel 执行cancel回调,只有在唤起调用链之前调用才有效。(调用链是单向的,执行之后就不可返回)

Deferred.prototype = {
    _id : 0xe38286e381ae, // 用于判断是否是实例的标识位
    init : function () {
        this._next = null; // 一种链表的实现思路
        this.callback = {
            ok : Deferred.ok, // 默认的ok回调
            ng : Deferred.ng  // 出错时的回调
        };
        return this;
    },
    next : function (fun) {
        return this._post("ok", fun); // 调用_post建立链表
    },
    error : function (fun) {
        return this._post("ng", fun); // 调用_post建立链表
    },
    call : function(val) {
        return this._fire("ok", val); // 唤起next调用链
    },
    fail : function (err) {
        return this._fire("ng", err); // 唤起error调用链
    },
    cancel : function () {
        (this.canceller || function () {}).apply(this);
        return this.init(); // 进行重置
    },
    _post : function (okng, fun){ // 建立链表
        this._next = new Deferred();
        this._next.callback[okng] = fun;
        return this._next;
    },
    _fire : function (okng, fun){
        var next = "ok";
        try{
            // 注册的回调函数中,可能会抛出异常,用try-catch进行捕捉
            value = this.callback[okng].call(this, value); 
        } catch(e) {
            next = "ng";
            value = e; // 传递出错信息
            if (Deferred.onerror) Deferred.onerror(e); // 发生错误的回调
        }
        if (Deferred.isDeferred(value)) { // 判断是否是Deferred的实例
            // 这里的代码就是给Deferred.wait方法使用的,
            value._next = this._next;
        } else { // 如果不是,则继续执行
            if (this._next) this._next._fire(next, value);
        }
        return this;
    }
}
登入後複製

2.5 辅助静态方法

上面的代码中,可以看到一些Deferred对象的方法(静态方法),下面简单介绍一下:

// 默认的成功回调
Deferred.ok = function (x) {return x};

// 默认的失败回调
Deferred.ng = function (x) {throw x};

// 根据_id判断实例的实现
Deferred.isDeferred = function (obj) {
    return !!(obj && obj._id === Deferred.prototype._id);
}
登入後複製

2.6 简单小结

看到这里,我们需要停下来,看看一个简单的例子,来理解整个流程。

Defferred对象自身有next属性方法,在原型上也定义了next方法,需要注意这一点,例如以下代码:

var o = {};
Deferred.define(o);
o.next(function fn1(){
    console.log(1);
}).next(function fn2(){
    console.log(2);
});
登入後複製

o.next()是Deffered对象的属性方法,这个方法会返回一个Defferred对象的实例,因此下一个next()则是原型上的next方法。

第一个next()方法将后续的代码变成异步操作,后面的next()方法实际上是注册调用函数。

在第一个next()的异步操作里面唤起后面next()的调用链(d.call()),开始顺序的调用,换句话说就是,fn1和fn2是同步执行的。

那么,如果我们希望fn1和fn2也是异步执行,而不是同步执行的,这就得借助Deferred.wait方法了。

2.7 wait & register

我们可以使用wait来让fn1和fn2变成异步执行,代码如下:

Deferred.next(function fn1() {
    console.log(1)
}).wait(0).next(function fn2() {
    console.log(2)
});
登入後複製

wait方法很有意思,在Deferred的原型上并没有wait方法,而是在静态方法上找到了。

Deferred.wait = function (n) {
    var d = new Deferred(),
        t = new Date();
    // 使用定时器来变成异步操作
    var id = setTimeout(function () {
        d.call((new Date()).getTime() - t.getTime());
    }, n * 1000);

    d.canceller = function () {
        clearTimeout(id);
    }
    return d;
}
登入後複製

那么这个方法是怎么放到原型上的?原来是通过Deferred.register进行函数转换,绑定到原型上的。

Deferred.register = function (name, fun){
    this.prototype[name] = function () { // 柯里化
        var a = arguments;
        return this.next(function(){
            return fun.apply(this, a);
        });
    }
};

// 将方法注册到原型上
Deferred.register("wait", Deferred.wait);
登入後複製

我们需要思考为什么要用这种方式将wait方法register到Deferred的原型对象上去?,因为明显这种方式有点难以理解。

结合例子,我们进行讨论,便能够彻底地理解上述的问题。

Deferred.next(function fn1(){ // d1
    console.log(1);
})
.wait(1) // d2
.next(function fn2(){ // d3
    console.log(2);
});
登入後複製

这段代码首先会建立一个调用链

JavaScript 非同步程式設計之 jsdeferred 原理解析

之后,执行的过程为(如图所示)

JavaScript 非同步程式設計之 jsdeferred 原理解析

我们来看看执行过程的几个关键点

图中的d1、d2、d3、d_wait表示在调用链上生成的Deferred对象的实例

在调用了d2的callback.ok即包装了wait()方法的匿名函数之后,返回了在wait()方法中生成的Deferred对象的实例d_wait,保存在变量value中,在_fire()方法中有一个if判断

if(Deferred.isDeferred(value)){
    value._next = this._next;
}
登入後複製
在这里并没有继续往下执行调用链的函数,而是重新建立了一个调用链,此时链头为d_wait,在wait()方法中使用setTimeout,使其异步执行,使用d.call()重新唤起调用链。
登入後複製

理解了整个过程,就比较好回到上面的问题了。之所以使用register的方式是因为原型上的wait方法并非直接使用Deferred.wait,而是把Deferred.wait方法作为参数,对原型上的next()方法进行curry化,然后返回一个柯里化之后的next()方法。而Deferred.wait()其实和Deferred.next()的作用很类似,都是异步执行接下来的操作。

2.8 并归结果 parallel

设想一个场景,我们需要多个异步网络查询任务,这些任务没有依赖关系,不需要区分前后,但是需要等待所有查询结果回来之后才能进一步处理,那么你会怎么做?在比较复杂的应用中,这个场景经常会出现,如果我们采用以下的方式(见伪代码)

var result = [];
$.ajax("task1", function(ret1){
    result.push(ret1);
    $.ajax("task2", function(ret2){
        result.push(ret2);
        // 进行操作
    });
});
登入後複製

这种方式可以,但是却无法同时发送task1和task2(从代码上看还以为之间有依赖关系,实际上没有)。那怎么解决?这就是Deferred.parallel()所要解决的问题。

我们先来个简单的例子感受一下这种并归结果的方式。

Deferred.parallel(function () {
    return 1;
}, function () {
    return 2;
}, function () {
    return 3;
}).next(function (a) {
    console.log(a); // [1,2,3]
});
登入後複製

在parallel()方法执行之后,会将结果合并为一个数组,然后传递给next()中的callback.ok中。可以看到parallel里面都是同步的方法,先来看看parallel的源码是如何实现,再来看看能不能结合所学来改造实现我们所需要的ajax的效果。

Deferred.parallel = function (dl) {
    /* 
        前面都是对参数的处理,可以接收三种形式的参数 
        1. parallel(fn1, fn2, fn3).next()
        2. parallel({
                foo : $.get("foo.html"),
                bar : $.get("bar.html")
            }).next(function (v){
                v.foo // => foo.html data
                v.bar // => bar.html data
            });
        3. parallel([fn1, fn2, fn3]).next(function (v) {
                v[0] // fn1执行的结果
                v[1] // fn2执行的结果
                v[3] // fn3执行返回的结果
            });
    */
    var isArray = false;
    // 第一种形式
    if (arguments.length > 1) {
        dl = Array.prototype.slice.call(arguments);
        isArray = true;
    // 其余两种形式,数组,类数组
    } else if (Array.isArray && Array.isArray(dl) 
                || typeof dl.length == "number") {
        isArray = true;
    }
    var ret = new Deferred(), // 用于归并结果的Deferred对象的实例
        value = {}, // 收集函数执行的结果
        num = 0 ; // 计数器,当为0时说明所有任务都执行完毕
    
    // 开始遍历,这里使用for-in其实效率不高
    for (var i in dl) {
        // 预防遍历了所有属性,例如toString之类的
        if (dl.hasOwnProperty(i)) {
            // 利用闭包保存变量状态
            (function (d, i){
                // 使用Deferred.next()开始一个异步任务,并且执行完成之后,收集结果
                if (typeof d == "function") dl[i] = d = Deferred.next(d);
                d.next(function (v) {
                    values[i] = v;
                    if( --num <= 0){ // 计数器为0说明所有任务已经完成,可以返回
                        if(isArray){ // 如果是数组的话,结果可以转换成数组
                            values.length = dl.length;
                            values = Array.prototype.slice.call(values, 0);
                        }
                        // 调用parallel().next(function(v){}),唤起调用链
                        ret.call(values);
                    }
                }).error(function (e) {
                    ret.fail(e);
                });
                num++; // 计数器加1
            })(d[i], i);
        } 
    }
    
    // 当计算器为0的时候,处理可能没有参数或者非法参数的情况
    if (!num) {
        Deferred.next(function () { 
            ret.call();
        });
    } 

    ret.canceller = function () {
        for (var i in dl) {
            if (dl.hasOwnProperty(i)) {
                dl[i].cancel();
            }
        }
    };
    return ret; // 返回Deferred实例
};
登入後複製

结合上述知识,我们可以在parallel中使用异步方法,代码如下

Deferred.parallel(function fn1(){
    var d = new Deferred();
    $.ajax("task1", function(ret1){
        d.call(ret1);
    });
    return d;
}, function () {
    var d = new Deferred();
    $.ajax("task2", function fn2(ret2) {
        d.call(ret2)
    });
    return d;
}).next(function fn3(ret) {
    ret[0]; // => task1返回的结果
    ret[1]; // => task2返回的结果
});
登入後複製

为什么可以这样?我们来图解一下,加深一下理解。

JavaScript 非同步程式設計之 jsdeferred 原理解析

我们使用了_fire中的if判断,建立了新的调用链,获得去统计计数函数(即parallel中--num)的控制权,从而使得在parallel执行异步的方法。

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

如何使用WebSocket和JavaScript實現線上語音辨識系統 如何使用WebSocket和JavaScript實現線上語音辨識系統 Dec 17, 2023 pm 02:54 PM

如何使用WebSocket和JavaScript實現線上語音辨識系統引言:隨著科技的不斷發展,語音辨識技術已成為了人工智慧領域的重要組成部分。而基於WebSocket和JavaScript實現的線上語音辨識系統,具備了低延遲、即時性和跨平台的特點,成為了廣泛應用的解決方案。本文將介紹如何使用WebSocket和JavaScript來實現線上語音辨識系

WebSocket與JavaScript:實現即時監控系統的關鍵技術 WebSocket與JavaScript:實現即時監控系統的關鍵技術 Dec 17, 2023 pm 05:30 PM

WebSocket與JavaScript:實現即時監控系統的關鍵技術引言:隨著互聯網技術的快速發展,即時監控系統在各個領域中得到了廣泛的應用。而實現即時監控的關鍵技術之一就是WebSocket與JavaScript的結合使用。本文將介紹WebSocket與JavaScript在即時監控系統中的應用,並給出程式碼範例,詳細解釋其實作原理。一、WebSocket技

如何利用JavaScript和WebSocket實現即時線上點餐系統 如何利用JavaScript和WebSocket實現即時線上點餐系統 Dec 17, 2023 pm 12:09 PM

如何利用JavaScript和WebSocket實現即時線上點餐系統介紹:隨著網路的普及和技術的進步,越來越多的餐廳開始提供線上點餐服務。為了實現即時線上點餐系統,我們可以利用JavaScript和WebSocket技術。 WebSocket是一種基於TCP協定的全雙工通訊協議,可實現客戶端與伺服器的即時雙向通訊。在即時線上點餐系統中,當使用者選擇菜餚並下訂單

如何使用WebSocket和JavaScript實現線上預約系統 如何使用WebSocket和JavaScript實現線上預約系統 Dec 17, 2023 am 09:39 AM

如何使用WebSocket和JavaScript實現線上預約系統在當今數位化的時代,越來越多的業務和服務都需要提供線上預約功能。而實現一個高效、即時的線上預約系統是至關重要的。本文將介紹如何使用WebSocket和JavaScript來實作一個線上預約系統,並提供具體的程式碼範例。一、什麼是WebSocketWebSocket是一種在單一TCP連線上進行全雙工

JavaScript與WebSocket:打造高效率的即時天氣預報系統 JavaScript與WebSocket:打造高效率的即時天氣預報系統 Dec 17, 2023 pm 05:13 PM

JavaScript和WebSocket:打造高效的即時天氣預報系統引言:如今,天氣預報的準確性對於日常生活以及決策制定具有重要意義。隨著技術的發展,我們可以透過即時獲取天氣數據來提供更準確可靠的天氣預報。在本文中,我們將學習如何使用JavaScript和WebSocket技術,來建立一個高效的即時天氣預報系統。本文將透過具體的程式碼範例來展示實現的過程。 We

簡易JavaScript教學:取得HTTP狀態碼的方法 簡易JavaScript教學:取得HTTP狀態碼的方法 Jan 05, 2024 pm 06:08 PM

JavaScript教學:如何取得HTTP狀態碼,需要具體程式碼範例前言:在Web開發中,經常會涉及到與伺服器進行資料互動的場景。在與伺服器進行通訊時,我們經常需要取得傳回的HTTP狀態碼來判斷操作是否成功,並根據不同的狀態碼來進行對應的處理。本篇文章將教你如何使用JavaScript來取得HTTP狀態碼,並提供一些實用的程式碼範例。使用XMLHttpRequest

javascript如何使用insertBefore javascript如何使用insertBefore Nov 24, 2023 am 11:56 AM

用法:在JavaScript中,insertBefore()方法用於在DOM樹中插入一個新的節點。這個方法需要兩個參數:要插入的新節點和參考節點(即新節點將要插入的位置的節點)。

JavaScript與WebSocket:打造高效率的即時影像處理系統 JavaScript與WebSocket:打造高效率的即時影像處理系統 Dec 17, 2023 am 08:41 AM

JavaScript是一種廣泛應用於Web開發的程式語言,而WebSocket則是一種用於即時通訊的網路協定。結合二者的強大功能,我們可以打造一個高效率的即時影像處理系統。本文將介紹如何利用JavaScript和WebSocket來實作這個系統,並提供具體的程式碼範例。首先,我們需要明確指出即時影像處理系統的需求和目標。假設我們有一個攝影機設備,可以擷取即時的影像數

See all articles