构建XHR连接队列

<!DOCTYPE HTML>
<html lang="en-US">
<head>
    <meta charset="utf-8">
    <title></title>
    <style>
        body {
            font: 100% georgia, times, serif;
        }

        h1, h2 {
            font-weight: normal;
        }

        #queue-items {
            padding: 0.5em;
            background: #ddd;
            border: 1px solid #bbb;
        }

        #results-area {
            padding: 0.5em;
            border: 1px solid #bbb;
        }
    </style>
</head>
<body id="example">
<div id="doc">
    <h1>Ajax Connection Queue</h1>

    <div id="queue-items"></div>
    <div id="add-stuff">
        <h2>Add Requests to Queue</h2>
        <ul id="adders">
            <li><a id="action-01" href="">Add "01" to Queue</a></li>
            <li><a id="action-02" href="">Add "02" to Queue</a></li>
            <li><a id="action-03" href="">Add "03" to Queue</a></li>
        </ul>
    </div>
    <h2>Other Queue Actions</h2>
    <ul id="items">
        <li><a id="flush" href="">Flush</a></li>
        <li><a id="dequeue" href="">Dequeue</a></li>
        <li><a id="pause" href="">Pause</a></li>
        <li><a id="clear" href="">Clear</a></li>
    </ul>
    <div id="results-area">
        <h2>Results:</h2>

        <div id="results"></div>
    </div>
</div>

<script src="Library.js"></script>
<script>
    var asyncRequest = (function () {
        function handleReadyState(o, callback) {
            var poll = window.setInterval(function () {
                if (o && o.readyState === 4) {
                    window.clearInterval(poll);
                    if (callback) {
                        callback.call(o, o.responseText, o.responseXML);
                    }
                }
            }, 50);
        }

        var getXHR = function () {
            var http;
            try {
                http = new XMLHttpRequest();
                getXHR = function () {
                    return new XMLHttpRequest();
                };
            } catch (e) {
                var msxml = [
                    'MSXML2.XMLHTTP.3.0',
                    'MSXML2,XMLHTTP',
                    'Microsoft.XMLHTTP'
                ];
                for (var i = 0, len = msxml.length; i < len; i++) {
                    try {
                        http = new ActiveXObject(msxml[i]);
                        getXHR = function () {
                            return new ActiveXObject(getXHR.str);
                        };
                        getXHR.str=msxml[i];
                        break;
                    } catch (e) {
                    }
                }
            }
            return http;
        };

        return function (method, url, callback, postVars) {
            var http = getXHR();
            handleReadyState(http, callback);
            http.open(method, url, true);
            http.send(postVars || null);
        }
    })();

    Function.prototype.method = function (name, fn) {
        this.prototype[name] = fn;
        return this;
    };

    if (!Array.prototype.forEach) {
        Array.method('forEach', function (fn, thisObj) {
            var scope = thisObj || window;
            for (var i = 0, len = this.length; i < len; i++) {
                fn.call(scope, this[i], i, this);
            }
        });
    }

    if (!Array.prototype.filter) {
        Array.method('filter', function (fn, thisObj) {
            var scope = thisObj || window;
            var a = [];
            for (var i = 0, len = this.length; o < len; i++) {
                if (!fn.call(scope, this[i], i, this)) {
                    continue;
                }
                a.push(this[i]);
            }
            return a;
        });
    }


    window.DED = window.DED || {};
    DED.util = DED.util || {};
    DED.util.Observer = function () {
        this.fns = [];
    };
    DED.util.Observer.prototype = {
        subscribe: function (fn) {
            this.fns.push(fn);
        },
        unsubscribe: function (fn) {
            this.fns = this.fns.filter(function (el) {
                if (el !== fn) {
                    return el;
                }
            });
        },
        fire: function (o) {
            this.fns.forEach(function (el) {
                el(o);
            });
        }
    };


    DED.Queue = function () {
        this.queue = [];
        this.onComplete = new DED.util.Observer();
        this.onFailure = new DED.util.Observer();
        this.onFlush = new DED.util.Observer();
        this.retryCount = 3;
        this.currentRetry = 0;
        this.paused = false;
        this.timeout = 5000;
        this.conn = {};
        this.timer = {};
    };
    DED.Queue.method('flush',function () {
        if (!this.queue.length > 0) {
            return;
        }
        if (this.paused) {
            this.paused = false;
            return;
        }
        var that = this;
        this.currentRetry++;
        var abort = function () {
            that.conn.abort();
            if (that.currentRetry === that.retryCount) {
                that.onFailure.fire();
                that.currentRetry = 0;
            } else {
                that.flush();
            }
        };
        this.timer = window.setTimeout(abort, that.timeout);
        var callback = function (o) {
            window.clearInterval(that.timer);
            that.currentRetry = 0;
            that.queue.shift();
            that.onFlush.fire(o);
            if (that.queue.length === 0) {
                that.onComplete.fire();
                return;
            }
            that.flush();
        };
        this.conn = asyncRequest(
                this.queue[0]['method'],
                this.queue[0]['url'],
                callback,
                this.queue[0]['param']
        );
    }).method('setRetryCount',function (count) {
                this.retryCount = count;
            }).method('setTimeout',function (time) {
                this.timeout = time;
            }).method('add',function (o) {
                this.queue.push(o);
            }).method('pause',function () {
                this.paused = true;
            }).method('dequeue',function () {
                this.queue.pop();
            }).method('clear', function () {
                this.queue = [];
            });
</script>
<script>
    addEvent(window, 'load', function () {
        var q = new DED.Queue();
        q.setRetryCount(5);
        q.setTimeout(3000);
        var items = $('items'),
                results = $('results'),
                queue = $('queue-items'),
                requests = [];
        q.onFlush.subscribe(function (data) {
            results.innerHTML = data;
            requests.shift();
            queue.innerHTML = requests.toString();
        });
        q.onFailure.subscribe(function () {
            results.innerHTML += '<span style="color:red;">Connection Error.</span>';
        });
        q.onComplete.subscribe(function () {
            results.innerHTML += '<span style="color:green;">Completed</span>';
        });
        var actionDispatcher = function (element) {
            switch (element) {
                case 'flush':
                    q.flush();
                    break;
                case 'dequeue':
                    q.dequeue();
                    requests.pop();
                    queue.innerHTML = requests.toString();
                    break;
                case 'pause':
                    q.pause();
                    break;
                case 'clear':
                    q.clear();
                    requests = [];
                    queue.innerHTML = '';
                    break;
                default:
                    break;
            }
        };
        var addRequest = function (data) {
            q.add({
                method: 'GET',
                url: 'main.js?ajax=true&s=' + data,
                params: null
            });
            requests.push(data);
            queue.innerHTML = requests.toString();
        };

        addEvent(items, 'click', function (e) {
            e = e || window.event;
            var src = e.target || e.srcElement;
            try {
                e.preventDefault();
            } catch (e) {
                e.returnValue = false;
            }
            actionDispatcher(src.id);
        });

        var adders = $('adders');
        addEvent(adders, 'click', function (e) {
            e = e || window.event;
            var src = e.target || e.srcElement;
            try {
                e.preventDefault();
            } catch (ex) {
                e.returnValue = false;
            }
            addRequest(src.id.split('-')[1]);
        });
    });
/**
     * 检查JSON文本,确保安全
     * @param {String} s JSON字符串
     * @param {Function} filter 过滤方法
     * @return {*}
     */
    function parseJSON(s, filter) {
        var j;

        /**
         * 递归地遍历了新生成的结构
         而且将每个名/值对传递给一个过滤函数,以便进行
         可能的转换
         * @param k
         * @param v
         * @return {*}
         */
        function walk(k, v) {
            if (v && typeof v === 'object') {
                for (var i in v) {
                    if (v.hasOwnProperty(i)) {
                        v[i] = walk(i, v[i]);
                    }
                }
            }
            return filter(k, v);
        }

        /*
         解析通过3个阶段进行。第一阶段,通过正则表达式
         检测JSON文本,查找非JSON字符串。其中,特别关注
         “()”和"new",因为它们会引起语句的调用,还有“=”,
         因为它会导致变量的值发生改变。不过,为安全起见
         这里会拒绝所有不希望出现的字符串
         */
        /*
         首先这个串分成两部分,看中间的或符号(|)
         "(\\.|[^\\\n\r])*?"和[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]
         先分解"(\\.|[^\\\n\r])*?"
         它匹配一个双引号的字符串,两边引号不说了括号内一个“|”又分成两段 “\\.“匹配一个转义字符
         比如js字符串里的\n,\r,\',\"等。[^\\\n\r]匹配一个非\,回车换行的字符 其实它就是js里字符串的规则---不包含回车换行,回车换行用 \n\r表示,\后面跟一个字符表示转义
         其次看[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]
         它匹配一个单个字符,这个字符可以是 ,,:,{,},[,],数字,除 "\n" 之外的任何单个字符,-,+,E,a,e,f,l,n,r-u之间的字符,回车,换行,制表符,
         */
        if (/^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/.test(s)) {
            /*
             第二阶段,使用eval()函数将JSON文本编译为js
             结构。其中的“{”操作符具有语法上的二义性,即它可
             以定义一个语句块,也可以表示对象字面量。这里将
             JSON文本用括号括起来是为了消除这种二义性
             */
            try {
                j = eval('(' + s + ')');
            } catch (e) {
                throw new SyntaxError('parseJSON');
            }
        } else {
            throw new SyntaxError('parseJSON');
        }

        /*
         在可选的第三阶段,代码递归地遍历了新生成的结构
         而且将每个名/值对传递给一个过滤函数,以便进行
         可能的转换
         */
        if (typeof filter === 'function') {
            j = walk('', j);
        }
        return j;
    }

    /**
     * 设置XMLHttpRequest对象的各个不同的部分
     * @param url
     * @param options
     *      参数options的成员属性
     *      method, 适用于请求的方法,默认为GET。
     *      send, 是一个包含在XMLHttpRequest.send()中的可选字符串,默认为null。
     *      loadListener, 是当readyState为1时调用的onreadystatechange侦听器
     *      loadedListener, 是当readyState为2时调用的onreadystatechange侦听器。
     *      interactiveListener, 是当readyState为3时调用的onreadystatechange侦听器。
     *      jsResponseListener, 是当请求成功并且响应的Content-Type为application/javascript或text/javascript时调用的侦听器,这个侦听器将从响应中取得js字符串作为其第一个参数。如果要执行该js字符串,必须使用eval()方法。
     *      jsonResponseListener, 是当请求成功并且响应的Content-Type为application/json时调用的侦听器。这个侦听器将从响应中取得JSON对象作为其第一个参数。
     *      xmlResponseListener, 是当请求成功并且响应的Content-Type为application/xml或application/xhtml+xml时调用的侦听器。这个侦听器将从响应中取得的XML DOM文档作为其第一个参数。
     *      htmlResponseListener, 是当请求成功并且响应的content-Type为text/html时调用的侦听器。这个侦听器将从响应中取得的HTML字符串作为其第一个参数。
     *      completeListener, 是当上面所列的针对Content-Type的响应侦听器调用之后被调用的侦听器。这个方法总是在成功的响应最后被调用,也就是说如果相应中没有适当的Content-Type头部信息,那么你可以制定这个方法作为兜底儿的侦听器。
     *      errorListener, 是当响应状态值不是200也不是0时被调用的侦听器。如果是在不会提供适当响应代码的系统上运行(如硬盘驱动器中的本地文件系统)XMLHttpRequest,那么状态值将始终为0.在这种情况下,只有completeListener会被调用。
     */
    /*
     demo:
     ADS.ajaxRequest('/path/to/script/',{
     method:'GET',
     completeListener:function(){
     alert(this.responseText);
     }
     });
     */
    // 因为使用了call和apply方法,此时的this引用的是
    // 请求对象而不是onreadystatechange方法。
    function getRequestObject(url, options) {
        // 初始化请求对象
        var req = false;
        if (window.XMLHttpRequest) {
            req = new window.XMLHttpRequest();
        } else if (window.ActiveXObject) {
            req = new ActiveXObject('Microsoft.XMLHTTP');
        }

        if (!req) {
            return false;
        }

        // 定义默认的选项
        options = options || {};
        options.method = options.method || 'GET';
        options.send = options.send || null;

        // 为请求的每个阶段定义不同的侦听器
        req.onreadystatechange = function () {
            switch (req.readyState) {
                case 1:
                    // 载入中
                    if (options.loadListener) {
                        options.loadListener.apply(req, arguments);
                    }
                    break;
                case 2:
                    // 载入完成
                    if (options.loadedListener) {
                        options.loadedListener.apply(req, arguments);
                    }
                    break;
                case 3:
                    // 交互
                    if (options.interactiveListener) {
                        options.interactiveListener.apply(req, arguments);
                    }
                    break;
                case 4:
                    // 完成
                    // 如果失败则抛出错误
                    try {
                        if (req.status && req.status === 200) {
                            // 针对Content-type的特殊侦听器
                            // 由于Content-Type头部中可能包含字符集,如:
                            // Content-Type: text/html; charset=ISO-8859-4
                            // 因此通过正则表达式提取出所需的部分
                            var contentType = req.getResponseHeader('Content-Type');
                            var mimeType = contentType.match(/\s*([^;]+)\s*(;|$)/i)[1];
                            switch (mimeType) {
                                case 'text/javascript':
                                case 'application/javascript':
                                    // 响应时javascript,因此以
                                    // req.responseText作为回调函数
                                    if (options.jsResponseListener) {
                                        options.jsResponseListener.call(req, req.responseText);
                                    }
                                    break;
                                case 'application/json':
                                    // 响应是JSON,因此需要用匿名函数对
                                    // req.responseText进行解析
                                    // 已返回作为回调参数的JSON对象
                                    if (options.jsonResponseListener) {
                                        var json;
                                        try {
                                            json = parseJSON(req.responseText);
                                        } catch (e) {
                                            json = false;
                                        }
                                        options.jsonResponseListener.call(req, json);
                                    }
                                    break;
                                case 'text/xml':
                                case 'application/xml':
                                case 'application/xhtml+xml':
                                    // 响应是XML,因此以
                                    // req.responseXML作为
                                    // 回调的参数
                                    // 此时是Document对象
                                    if (options.xmlResponseListener) {
                                        options.xmlResponseListener.call(req, req.responseText);
                                    }
                                    break;
                                case 'text/html':
                                    // 响应是HTML,因此以
                                    // req.responseText作为
                                    // 回调的参数
                                    if (options.htmlResponseListener) {
                                        options.htmlResponseListener.call(req, req.responseText);
                                    }
                                    break;
                                default:
                                    break;
                            }

                            // 针对响应成功完成的侦听器
                            if (options.completeListener) {
                                options.completeListener.apply(req, arguments);
                            }
                        } else {
                            // 相应完成但却存在错误
                            if (options.errorListener) {
                                options.errorListener.apply(req, arguments);
                            }
                        }
                    } catch (e) {
                        // 忽略错误
                    }
                    break;
            }
        };

        // 开启请求
        req.open(options.method, url, true);
        // 添加特殊的头部信息以标识请求
        req.setRequestHeader('X-ADS-Ajax-Request', 'AjaxRequest');
        return req;
    }

    window.ADS.getRequestObject = getRequestObject;

    // 通过简单的包装getRequestObject()和send()
    // 方法发送XMLHttpRequest对象的请求
    function ajaxRequest(url, options) {
        var req = getRequestObject(url, options);
        return req.send(options.send);
    }

    window.ADS.ajaxRequest = ajaxRequest;

 /* 一个复制对象的辅助方法 */
    function clone(myObj) {
        if (typeof myObj !== 'object') {
            return myObj;
        }
        if (myObj === null) {
            return myObj;
        }
        var myNewObj = {};
        for (var i in myObj) {
            myNewObj[i] = clone(myObj[i]);
        }
        return myNewObj;
    }

    /* 用于保存队列的数组 */
    var requestQueue = [];

    /**
     * 为ADS.ajaxRequest方法启用排队功能的包装对象
     * @param url
     * @param options
     * @param queue
     * @example
     *      ADS.ajaxRequestQueue('/your/script/', {
     *          completeListener: function(){
     *              alert(this.responseText);
     *          }
     *      }, 'Queue1');
     */
    function ajaxRequestQueue(url, options, queue) {
        queue = queue || 'default';

        // 这个对象将把可选的侦听器包装在另一个函数中
        // 因此,可选的对象必须唯一。否则,如果该方法
        // 被调用时使用的是共享的可选对象,那么会导致
        // 陷入递归中
        options = clone(options) || {};
        if (!requestQueue[queue]) {
            requestQueue[queue] = [];
        }

        // 当前一次请求完成时,需要使用completeListener
        // 调用队列中的下一次请求。如果完成侦听器已经
        // 有定义,那么需要首先调用它

        // 取得旧侦听器
        var userCompleteListener = options.completeListener;

        // 添加新侦听器
        options.completeListener = function () {
            // 如果存在旧的侦听器则首先调用它
            if (userCompleteListener) {
                // this引用的是情求对象
                userCompleteListener.apply(this, arguments);
            }

            // 从队列中移除这个请求
            requestQueue[queue].shift();

            // 调用队列中的下一项
            if (requestQueue[queue][0]) {
                // 请求保存在req属性中,但为防止它是
                // 一个POST请求,故也需包含send选项
                var q = requestQueue[queue][0].req.send(
                    requestQueue[queue][0].send
                );
            }
        };

        // 如果发生了错误,应该通过调用相应的
        // 错误处理方法取消队列中的其他请求

        // 取得旧侦听器
        var userErrorListener = options.errorListener;

        // 添加新侦听器
        options.errorListener = function () {
            if (userErrorListener) {
                userErrorListener.apply(this, arguments);
            }

            // 由于已经调用了错误侦听器
            // 股从队列中移除这个请求
            requestQueue[queue].shift();

            // 由于出错需要取消队列中的其余请求,但首先要调用
            // 每个请求的errorListener。通过调用队列中
            // 下一项的错误侦听器就会才清楚所有排队的请求,因为在
            // 链中的调研那个是一次发生的

            // 检测队列中是否还存在请求
            if (requestQueue[queue].length) {
                // 取得下一项
                var q = requestQueue[queue].shift();

                // 中断请求
                q.req.abort();

                // 伪造请求对象,以便errorListener
                // 认为请求已经完成并相应地运行

                var fakeRequest = {};

                // 将status设置为0,将readyState设置为4
                // 就好像请求虽然完成但却失败了一样
                fakeRequest.status = 0;
                fakeRequest.readyState = 4;

                fakeRequest.responseText = null;
                fakeRequest.responseXML = null;

                // 设置错误信息,以便需要时显示
                fakeRequest.statusText = 'A request in the queue received an error';

                // 调用状态改变,如果readyState是4,而
                // status不是200,则会调用errorListener
                q.error.apply(fakeRequest);
            }
        };

        // 将这个请求添加到队列中
        requestQueue[queue].push({
            req: getRequestObject(url, options),
            send: options.send,
            error: options.errorListener
        });

        // 如果队列的长度表明只有一个
        // 项(即第一个)则调用请求
        if (requestQueue[queue].length === 1) {
            ajaxRequest(url, options);
        }
    }

    window.ADS.ajaxRequestQueue = ajaxRequestQueue;


</script>
</body>
</html>

另一个版本

原文地址:https://www.cnblogs.com/webFrontDev/p/2888574.html