论坛首页 Web前端技术论坛

[原创]ExtJS 2.3源码分析(2012年02月23日更新)

浏览 17963 次
精华帖 (3) :: 良好帖 (11) :: 新手帖 (1) :: 隐藏帖 (0)
作者 正文
   发表时间:2012-02-20  
对于函数这一级的分析,上流程图或者时序图更清晰一点。
继续,加油。
0 请登录后投票
   发表时间:2012-02-20  

补充一下:
Ext.js中
extend方法使用了其他方法没有使用的函数变换策略

extend : function() {
	// Object的构造函数
	var oc = Object.prototype.constructor;//(私有对象,仅下面的return function内部可以使用)
	// override函数,用来覆盖prototype上的属性的(私有方法,仅下面的return function内部可以使用)
	var io = function( o ) {
		for ( var m in o ) {
			this[m] = o[m];
		}
	};
	return function( sb , sp , overrides ) {
                    //.........
                    return sb;
         }
}()

 


这样使用后可以定义仅在函数内部使用的oc和io属性。

 

0 请登录后投票
   发表时间:2012-02-20  
adamed 写道

补充一下:
Ext.js中
extend方法使用了其他方法没有使用的函数变换策略

extend : function() {
	// Object的构造函数
	var oc = Object.prototype.constructor;//(私有对象,仅下面的return function内部可以使用)
	// override函数,用来覆盖prototype上的属性的(私有方法,仅下面的return function内部可以使用)
	var io = function( o ) {
		for ( var m in o ) {
			this[m] = o[m];
		}
	};
	return function( sb , sp , overrides ) {
                    //.........
                    return sb;
         }
}()

 


这样使用后可以定义仅在函数内部使用的oc和io属性。

 

 

已追加~

0 请登录后投票
   发表时间:2012-02-20  
目测要火,果断留名。

不错
0 请登录后投票
   发表时间:2012-02-20  
牛逼,是不是要想真正精通就必须去看一遍源码呢?那jquery有没有看一遍的必要?
0 请登录后投票
   发表时间:2012-02-21  
sniper114713 写道
牛逼,是不是要想真正精通就必须去看一遍源码呢?那jquery有没有看一遍的必要?

 

我觉得要是有时间的话,可以看看源代码。

0 请登录后投票
   发表时间:2012-02-21   最后修改:2012-02-21

接着分析ext-base文件,这个文件稍长,所以根据模块分成多个帖子。

 

接下来的代码主要处理:

 

一、IE的事件模型和W3C的事件模型之间的接口不同,就是兼容一下各大浏览器,让接口统一。

 

二、实现一个Dom加载完成的事件,这个事件触发要在window的onload前面。现在的浏览器都有DomContentLoaded事件,早期应该是没有这个事件。

 

下面的代码是用了setInterval来反复判断document,document.body,el.nextSibling这些变量,如果这些变量有值了,就表示Dom已经加载好了。

 

再阅读下面代码,主要有三个核心数据结构一个是listeners和unloadListeners,一个是onAvailStack。这块代码的入口函数是_tryPreloadAttach(),建议从这个函数开始阅读。

listeners的意义在于保存注册事件时的参数信息,这样当你想删除事件时可以用元素和事件名称就可以把相关事件都删除干净。
unloadListeners的意义在于利用window的unload事件,模拟实现所有元素的unload事件。这大概是因为早期浏览器没有unload这个事件。

onAvailStack的意义是保存要执行的函数队列,实现了类似jQuery的ready事件。

 

    Ext.lib.Event = function() {
        // loadComplete指的是window的onload事件是否触发,触发后就会执行_load函数,loadComplete就变成了true。
        var loadComplete = false;

        // listeners这个变量用来存放你注册事件时的参数,因为你删除事件的时候,也需要这些参数。
        var listeners = [];

        // unloadListeners这个变量中存放的事件句柄,在window的onunload事件触发时会执行
        var unloadListeners = [];

        // 重试的次数,_tryPreloadAttach函数中会使用它,每执行一次_tryPreloadAttach函数,这个值就减一,这个值的初始值是POLL_RETRYS
        var retryCount = 0;

        // 这个数组中存放的是当Dom加载完成时,要执行的一些函数
        var onAvailStack = [];

        // 这个变量有可能是在别的地方使用,这个文件中是没有使用过。也有可能是多写了。
        var counter = 0;

        // 这个变量的意义不大,在getTime中使用了一次。
        var lastError = null;

        return {
            POLL_RETRYS: 200,
            POLL_INTERVAL: 20,
            EL: 0,
            TYPE: 1,
            FN: 2,
            WFN: 3,
            OBJ: 3,
            ADJ_SCOPE: 4,
            _interval: null,

            // 定时函数,用来执行_tryPreloadAttach
            startInterval: function() {
                if (!this._interval) {
                    var self = this;
                    var callback = function() {
                        self._tryPreloadAttach();
                    };
                    this._interval = setInterval(callback, this.POLL_INTERVAL);

                }
            },

            // 当id为p_id的元素加载好了,就执行p_fn函数,p_obj是p_fn的第一个参数,如果p_override没有,则p_fn函数的this指向p_id元素
            // 如果p_override为true,则this指向p_obj,其他则this指向p_override
            onAvailable: function(p_id, p_fn, p_obj, p_override) {
                onAvailStack.push({ id:         p_id,
                    fn:         p_fn,
                    obj:        p_obj,
                    override:   p_override,
                    checkReady: false    });

                retryCount = this.POLL_RETRYS;
                this.startInterval();
            },

            // 注册事件函数,返回是否注册成功
            addListener: function(el, eventName, fn) {
                el = Ext.getDom(el);

                // 参数检查
                if (!el || !fn) {
                    return false;
                }

               // 如果是unload事件,那么直接添加在unloadListeners数组中,当window的onunload事件触发时执行
                if ("unload" == eventName) {
                    unloadListeners[unloadListeners.length] =
                    [el, eventName, fn];
                    return true;
                }

                // 封装函数,处理event变量不兼容的问题
                var wrappedFn = function(e) {
                    return typeof Ext != 'undefined' ? fn(Ext.lib.Event.getEvent(e)) : false;
                };

                var li = [el, eventName, fn, wrappedFn];

                var index = listeners.length;
                listeners[index] = li;

                // 下面语句就直接注册事件了
                this.doAdd(el, eventName, wrappedFn, false);
                return true;

            },

            // 删除注册事件
            removeListener: function(el, eventName, fn) {
                var i, len;

                el = Ext.getDom(el);

                if(!fn) {
                    // 如果没有传入fn变量,那么直接删除el元素所有eventName事件的注册函数
                    return this.purgeElement(el, false, eventName);
                }

                // 如果是unload事件,那么直接从unloadListeners中清除掉就可以了
                if ("unload" == eventName) {

                    for (i = 0,len = unloadListeners.length; i < len; i++) {
                        var li = unloadListeners[i];
                        if (li &&
                            li[0] == el &&
                            li[1] == eventName &&
                            li[2] == fn) {
                            unloadListeners.splice(i, 1);
                            return true;
                        }
                    }

                    return false;
                }

                // 下面的代码就是从listeners中找出,要删除的事件函数,然后清除之
                var cacheItem = null;


                var index = arguments[3];

                if ("undefined" == typeof index) {
                    index = this._getCacheIndex(el, eventName, fn);
                }

                if (index >= 0) {
                    cacheItem = listeners[index];
                }

                if (!el || !cacheItem) {
                    return false;
                }

                // 这个函数调用removeLisenre函数
                this.doRemove(el, eventName, cacheItem[this.WFN], false);

                delete listeners[index][this.WFN];
                delete listeners[index][this.FN];
                listeners.splice(index, 1);

                return true;

            },

            // 取得触发事件的dom元素
            getTarget: function(ev, resolveTextNode) {
               // 这个browserEvent不知道怎么设置的,应该不是W3C的标准,貌似也不属于IE系列的
                ev = ev.browserEvent || ev;
                var t = ev.target || ev.srcElement;
                return this.resolveTextNode(t);
            },

            // 取得文本节点
            resolveTextNode: function(node) {
                if (Ext.isWebKit && node && 3 == node.nodeType) {
                    return node.parentNode;
                } else {
                    return node;
                }
            },

            // getPageX就是取得当前鼠标相对文档的横坐标位置
            getPageX: function(ev) {
                ev = ev.browserEvent || ev;
                var x = ev.pageX;
                if (!x && 0 !== x) {
                    x = ev.clientX || 0;

                    if (Ext.isIE) {
                        x += this.getScroll()[1];
                    }
                }

                return x;
            },

           // getPageY就是取得当前鼠标相对于文档的纵坐标位置
            getPageY: function(ev) {
                ev = ev.browserEvent || ev;
                var y = ev.pageY;
                if (!y && 0 !== y) {
                    y = ev.clientY || 0;

                    if (Ext.isIE) {
                        y += this.getScroll()[0];
                    }
                }


                return y;
            },

            // 取得横纵坐标位置
            getXY: function(ev) {
                ev = ev.browserEvent || ev;
                return [this.getPageX(ev), this.getPageY(ev)];
            },

            // 取得相关的的事件源,针对mouseout和mouseover两个事件
            getRelatedTarget: function(ev) {
                ev = ev.browserEvent || ev;
                var t = ev.relatedTarget;
                if (!t) {
                    if (ev.type == "mouseout") {
                        t = ev.toElement;
                    } else if (ev.type == "mouseover") {
                        t = ev.fromElement;
                    }
                }

                return this.resolveTextNode(t);
            },

           // 取得当前执行时间
            getTime: function(ev) {
                ev = ev.browserEvent || ev;
                if (!ev.time) {
                    var t = new Date().getTime();
                    try {
                        ev.time = t;
                    } catch(ex) {
                        this.lastError = ex;
                        return t;
                    }
                }

                return ev.time;
            },

           // 停止事件传播,并且阻止默认行为
            stopEvent: function(ev) {
                this.stopPropagation(ev);
                this.preventDefault(ev);
            },

            // 停止传播
            stopPropagation: function(ev) {
                ev = ev.browserEvent || ev;
                if (ev.stopPropagation) {
                    ev.stopPropagation();
                } else {
                    ev.cancelBubble = true;
                }
            },

            // 阻止默认行为
            preventDefault: function(ev) {
                ev = ev.browserEvent || ev;
                if(ev.preventDefault) {
                    ev.preventDefault();
                } else {
                    ev.returnValue = false;
                }
            },

            // 取得event变量,IE是window.event,W3C是函数的第一参数
            getEvent: function(e) {
                // 实际上下面这句代码就可以正确取得event对象了,因为wrappedFn中传入了参数e
                var ev = e || window.event;

                // 下面这段代码是,当ev没有取到值,那么就找父函数的第一个参数,如果第一个参数不是Event类型,那么接着向上找父函数
                if (!ev) {
                    var c = this.getEvent.caller;
                    while (c) {
                        ev = c.arguments[0];
                        if (ev && Event == ev.constructor) {
                            break;
                        }
                        c = c.caller;
                    }
                }
                return ev;
            },

            // 取得键盘代码
            getCharCode: function(ev) {
                ev = ev.browserEvent || ev;
                return ev.charCode || ev.keyCode || 0;
            },

            // 从listeners中找出注册时间函数的索引
            _getCacheIndex: function(el, eventName, fn) {
                for (var i = 0,len = listeners.length; i < len; ++i) {
                    var li = listeners[i];
                    if (li &&
                        li[this.FN] == fn &&
                        li[this.EL] == el &&
                        li[this.TYPE] == eventName) {
                        return i;
                    }
                }

                return -1;
            },

            // 未使用
            elCache: {},

           // 取到dom元素
            getEl: function(id) {
                return document.getElementById(id);
            },

            // 这个应该是还没有完成
            clearCache: function() {
            },

            // 当window的load事件触发时,就会执行此函数
            _load: function(e) {
                loadComplete = true;
                var EU = Ext.lib.Event;

                // 清除_load函数,防止内存泄露
                if (Ext.isIE) {
                    EU.doRemove(window, "load", EU._load);
                }
            },

            // 这个函数的主要功能就是执行onAvailStack中的函数。
            _tryPreloadAttach: function() {

                // 此函数可以多次运行,但是onAvailStack是共享的,所以要锁一下
                if (this.locked) {
                    return false;
                }

                this.locked = true;

                // 这几行代码挺绕的,意思是这样的
                // loadComplete代表window的onload事件触发,默认值是false,这个前面说过
                // tryAgain主要是依据window是否load完成,和notAvail中是否还有未执行完成的
                // notAvail如果有没有需要执行的了话,retryCount等于0
                var tryAgain = !loadComplete;
                if (!tryAgain) {
                    tryAgain = (retryCount > 0);
                }

                // 这个表示还未执行的函数,因为那个Dom元素还没有加载好,所以叫notAvail
                var notAvail = [];

                // 遍历onAvialStack,尝试执行一遍里面的函数
                for (var i = 0,len = onAvailStack.length; i < len; ++i) {
                    var item = onAvailStack[i];
                    // 执行完的选项为null,所以这里要判断一下,执行过得就不再执行了
                    if (item) {
                        // 根据id获取Dom节点元素
                        var el = this.getEl(item.id);

                        if (el) {
                            // 如果节点元素存在,下面是连续的短路判断
                            // checkReady默认是false,其意思就是是否等Dom加载好了,再执行
                            // loadComplete开始是false,表示window的load事件是否加载好了
                            // el.nextSibling引用的是该节点的下一个兄弟节点
                            // document && document.body这个表示的body标签是否都加载好了
                            if (!item.checkReady ||
                                loadComplete ||
                                el.nextSibling ||
                                (document && document.body)) {

                                // 经过上面层层判断,如果Dom确实已经加载好了,那么就开始准备执行了
                                // 首先就是scope,默认是el,这个就是函数执行时的this值
                                var scope = el;

                                // 下面是可以重新设置这个scope的值,需要设置一下override的值,这个在前面讲过
                                if (item.override) {
                                    if (item.override === true) {
                                        scope = item.obj;
                                    } else {
                                        scope = item.override;
                                    }
                                }

                                // 最后是函数执行,执行完后,把该项引用设置成null
                                item.fn.call(scope, item.obj);
                                onAvailStack[i] = null;
                            }
                        } else {
                            // 如果el不存在,表示Dom还没有加载好,那么就push到notAvail中去
                            notAvail.push(item);
                        }
                    }
                }

                // 如果都执行成功,则retryCount为0,否则,retryCount每次减1,初始值是200
                retryCount = (notAvail.length === 0) ? 0 : retryCount - 1;


                if (tryAgain) {
                    // startInterval函数还会调用此函数,继续轮回
                    this.startInterval();
                } else {
                    // 不需要再执行了,就清除了,就可以
                    clearInterval(this._interval);
                    this._interval = null;
                }

                this.locked = false;

                return true;

            },

            // 删除一个元素事件函数,还支持递归删除子元素的,顶
            purgeElement: function(el, recurse, eventName) {
                var elListeners = this.getListeners(el, eventName);
                if (elListeners) {
                    for (var i = 0,len = elListeners.length; i < len; ++i) {
                        var l = elListeners[i];
                        this.removeListener(el, l.type, l.fn);
                    }
                }

                if (recurse && el && el.childNodes) {
                    for (i = 0,len = el.childNodes.length; i < len; ++i) {
                        this.purgeElement(el.childNodes[i], recurse, eventName);
                    }
                }
            },

           // 根据el和eventName取得监听的时间函数结果集
            getListeners: function(el, eventName) {
                var results = [], searchLists;
                if (!eventName) {
                    searchLists = [listeners, unloadListeners];
                } else if (eventName == "unload") {
                    searchLists = [unloadListeners];
                } else {
                    searchLists = [listeners];
                }

                for (var j = 0; j < searchLists.length; ++j) {
                    var searchList = searchLists[j];
                    if (searchList && searchList.length > 0) {
                        for (var i = 0,len = searchList.length; i < len; ++i) {
                            var l = searchList[i];
                            if (l && l[this.EL] === el &&
                                (!eventName || eventName === l[this.TYPE])) {
                                results.push({
                                    type:   l[this.TYPE],
                                    fn:     l[this.FN],
                                    obj:    l[this.OBJ],
                                    adjust: l[this.ADJ_SCOPE],
                                    index:  i
                                });
                            }
                        }
                    }
                }

                return (results.length) ? results : null;
            },

            // 当window的unload事件触发时,执行此函数,并且触发unloadListeners中的所有函数
            _unload: function(e) {

                var EU = Ext.lib.Event, i, j, l, len, index;

                for (i = 0,len = unloadListeners.length; i < len; ++i) {
                    l = unloadListeners[i];
                    if (l) {
                        var scope = window;
                        if (l[EU.ADJ_SCOPE]) {
                            if (l[EU.ADJ_SCOPE] === true) {
                                scope = l[EU.OBJ];
                            } else {
                                scope = l[EU.ADJ_SCOPE];
                            }
                        }
                        l[EU.FN].call(scope, EU.getEvent(e), l[EU.OBJ]);
                        unloadListeners[i] = null;
                        l = null;
                        scope = null;
                    }
                }

                unloadListeners = null;

                if (listeners && listeners.length > 0) {
                    j = listeners.length;
                    while (j) {
                        index = j - 1;
                        l = listeners[index];
                        if (l) {
                            EU.removeListener(l[EU.EL], l[EU.TYPE],
                                    l[EU.FN], index);
                        }
                        j = j - 1;
                    }
                    l = null;

                    EU.clearCache();
                }

                EU.doRemove(window, "unload", EU._unload);

            },

            // 取得文档的滚动距离
            getScroll: function() {
                var dd = document.documentElement, db = document.body;
                if (dd && (dd.scrollTop || dd.scrollLeft)) {
                    return [dd.scrollTop, dd.scrollLeft];
                } else if (db) {
                    return [db.scrollTop, db.scrollLeft];
                } else {
                    return [0, 0];
                }
            },

            // 注册事件
            doAdd: function () {
                if (window.addEventListener) {
                    return function(el, eventName, fn, capture) {
                        el.addEventListener(eventName, fn, (capture));
                    };
                } else if (window.attachEvent) {
                    return function(el, eventName, fn, capture) {
                        el.attachEvent("on" + eventName, fn);
                    };
                } else {
                    return function() {
                    };
                }
            }(),

            // 删除事件
            doRemove: function() {
                if (window.removeEventListener) {
                    return function (el, eventName, fn, capture) {
                        el.removeEventListener(eventName, fn, (capture));
                    };
                } else if (window.detachEvent) {
                    return function (el, eventName, fn) {
                        el.detachEvent("on" + eventName, fn);
                    };
                } else {
                    return function() {
                    };
                }
            }()
        };

    }();

    // 把addlistener和removeListener注册到on和un上,方便使用
    var E = Ext.lib.Event;
    E.on = E.addListener;
    E.un = E.removeListener;

    // 判断一下Dom是否加载完成
    if(document && document.body) {
        // 如果已经加载好了,就执行一下_load函数
        E._load();
    } else {
        // 如果还没有加载好,就注册到window的load事件上
        E.doAdd(window, "load", E._load);
    }
    // 注册unload事件
    E.doAdd(window, "unload", E._unload);
    // 这个_tryPreloadAttach函数应该是比_load函数执行的早,可以看做是入口函数
    E._tryPreloadAttach();
0 请登录后投票
   发表时间:2012-02-21  
这样的帖子 非常不错
0 请登录后投票
   发表时间:2012-02-21   最后修改:2012-02-21

临时详解下下面的代码:

// 这个函数可以在你执行完原函数以后,执行一下自定义的函数。
	  createSequence : function( fcn , scope ) {
		  if ( typeof fcn != "function" ) {
			  return this;
		  }
		  var method = this;
		  return function() {
			  var retval = method.apply( this || window , arguments );
			  fcn.apply( scope || this || window , arguments );
			  return retval;
		  };
	  }

 这个代码为什么要这么设计呢?

先看API中关于这个方法的使用方法:

 

var sayHi = function(name){    alert('Hi, ' + name);}
sayHi('Fred'); // alerts "Hi, Fred"
var sayGoodbye = sayHi.createSequence(function(name){   
                                           alert('Bye, ' + name);
                                   });
sayGoodbye('Fred'); // both alerts show

最终createSequence需要返回一个function类型的方法所以在方法的定义中return 了一个function。

且根据方法定义,该方法的第一个参数必须是一个function类型的对象,所以在方法的最开始首先判断该方法第一个参数的参数类型若不为function则放弃后续操作并将createSequence方法的调用者返回去。这样类似于var sayGoodbye = sayHi.createSequence({});在执行sayGoodbye();时等价于调用sayHi();

 

这里还有一个待解决的问题那就是如何将sayHi保存起来,因为根据createSequence的定义。该方法需要先调用sayHi()而后调用里面的那个匿名function。

且因为一定要返回一个function所以下面必须renturn function(){。。。。。}这里请注意一点,返回的function中的this和return外面的this并不是同一个对象。

我们看代码:var mathod = this; 返回的function中: var retval = method.apply( this || window , arguments );

这2个this是不一样的,return外面的this表示调用createSequence的对象,根据上面的例子就是sayHi这个function。而return中的this代表调用return的function的那个对象,也就是调用sayHi的那个对象不是sayHi本身

所以必须在return function之前先把代表sayHi的function也就是那个this用一个内部变量缓存起来利用闭包供return的function执行时使用。

所以才有了var retval = method.apply( this || window , arguments );的写法。根据上面的DEMO我们重新给方法定义添加一些注释以帮助理解2个不同的this:

 

// 这个函数可以在你执行完原函数以后,执行一下自定义的函数。
	  createSequence : function( fcn , scope ) {
                //这里的fcn就是DEMO中的那个function(name){alert('Bye, ' + name);}

                  if ( typeof fcn != "function" ) {
			  return this;
		  }
		  var method = this;//这个this为sayHi (类型为function)
		  return function() {
			  var retval = method.apply( this || window , arguments );//这个this为调用sayHi的对象也就是window
                         //上面那一句相当于调用sayHi(/* 这个地方应该是arguments 也就是 'Fred' */);
                           fcn.apply( scope || this || window , arguments );//这一句相当于执行function(name){alert('Bye, ' + name);}('Fred')
			  return retval;//根据定义返回sayHi执行结果
		  };
	  }
0 请登录后投票
   发表时间:2012-02-21  
Sequence字面意思是“序列”的意思,所以简而言之,createSequence就是一种把函数按顺序调用的效果。
0 请登录后投票
论坛首页 Web前端技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics