门户级UGC系统的技术进化路线——新浪新闻评论系统的架构演进和经验总结

mikel阅读(1096)

.

评论系统,或者称为跟帖、留言板,是所有门户网站的核心标准服务组件之一。与论坛、博客等其他互联网UGC系统相比, 评论系统虽然从产品功能角度衡量相对简单,但因为需要能够在突发热点新闻事件时,在没有任何预警和准备的前提下支撑住短短几分钟内上百倍甚至更高的访问量 暴涨,而评论系统既无法像静态新闻内容业务那样通过CDN和反向代理等中间缓存手段化解冲击,也不可能在平时储备大量冗余设备应对突发新闻,所以如何在有 限的设备资源条件下提升系统的抗压性和伸缩性,也是对一个貌似简单的UGC系统的不小考验。

新闻评论系统的起源

新浪网很早就在新闻中提供了评论功能,最开始是使用Perl语言开发的简单脚本,目前能找到的最早具备评论功能的新闻是2000年4月7日的,经过多次系统升级,2014年前的评论地址已经失效了,但数据仍保存在数据库中。直到今天,评论仍是国内所有新闻网站的标配功能。

评论系统3.0

2003年左右,我接手负责评论系统,系统版本为3.0。当时的评论系统运行在单机环境,一台x86版本Solaris系统的Dell 6300服务器提供了全部服务,包括MySQL和Apache,以及所有前后台CGI程序,使用C++开发。

图1  3.0系统流程和架构

3.0系统的缓存模块设计得比较巧妙,以显示页面为单位缓存数据,因为评论页面依照提交时间降序排列,每新增一条评论,所有帖子都需要向下移动一位,所以缓存格式设计为每两页数据一个文件,前后相邻的两个文件有一页数据重复,最新的缓存文件通常情况下不满两页数据。

图2  页面缓存算法示意图

图2是假设评论总数95条,每页显示20条时的页面缓存结构,此时用户看到的第一页数据读取自“缓存页4”的95~76,第二页数据读取自“缓存页3”的75~56,以此类推。

这 样发帖动作对应的缓存更新可简化为一次文件追加写操作,效率最高。而且可保证任意评论总量和显示顺序下的翻页动作,都可在一个缓存文件中读到所需的全部数 据,而不需要跨页读取再合并。缺点是更新评论状态时(如删除),需要清空自被删除帖子开始的所有后续缓存文件。缓存模块采取主动+被动更新模式,发帖为主 动,每次发帖后触发一次页面缓存追加写操作。更新评论状态为被动,所涉及缓存页面文件会被清空,直到下一次用户读取页面缓存时再连接数据库完成查询,然后 更新页面缓存,以备下次读取。这个针对发帖优化的页面缓存算法继续沿用到了后续版本的评论系统中。

此时的评论系统就已具备了将同一专题事件下所有新闻评论汇总显示的能力,在很长一段时间内这都是新浪评论系统的独有功能。

虽 然3.0系统基本满足了当时的产品需求,但毕竟是单机系统,热点新闻时瞬间涌来的大量发帖和读取操作,经常会压垮这台当时已属高配的4U服务器,频繁显示 资源耗尽的错误页面。我接手后的首要任务就是尽量在最短时间内最大限度降低系统的宕机频率,通过观察分析确定主要性能瓶颈在数据库层面。

3.0 系统中,每个新闻频道的全部评论数据都保存在一张MyISAM表中,部分频道的数据量已经超过百万,在当时已属海量规模,而且只有一个数据库实例,读写竞 争非常严重。一旦有评论状态更新,就会导致很多缓存页面失效,瞬间引发大量数据库查询,进一步加剧了读写竞争。当所有CGI进程都阻塞在数据库环节无法退 出时,殃及Apache,进而导致系统Load值急剧上升无法响应任何操作,只有重启才能恢复。

解决方案是增加了一台 FreeBSD系统的低配服务器用于数据库分流,当时MySQL的版本是3.23,Replication主从同步还未发布,采取的办法是每天给数据表减 肥,把超过一周的评论数据搬到2号服务器上,保证主服务器的评论表数据量维持在合理范围,在这样的临时方案下,3.0系统又撑了几个月。

现在看来,在相当简陋的系统架构下,新浪评论系统3.0与中国互联网产业的门户时代一起经历了南海撞机、911劫机、非典、孙志刚等新闻事件。

评论系统4.0启动

2004年左右,运行了近三年的3.0系统已无法支撑新浪新闻流量的持续上涨,技术部门启动了4.0计划,核心需求就是三个字:不宕机。

因为当时我还负责了新浪聊天系统的工作,不得不分身应对新旧系统的开发维护和其他项目任务,所以在现有评论系统线上服务不能中断的前提下,制定了数据库结构不变,历史数据全部保留,双系统逐步无缝切换,升级期间新旧系统并存的大方针。

第一阶段:文件系统代替数据库,基于ICE的分布式系统

既然3.0系统数据库结构不可变,除了把数据库升级到MySQL 4.0启用Repliaction分解读写压力以外,最开始的设计重点是如何把数据库与用户行为隔离开。

解 决方案是在MySQL数据库和页面缓存模块之间,新建一个带索引的数据文件层,每条新闻的所有评论都单独保存在一个索引文件和一个数据文件中,期望通过把 对数据库单一表文件的读写操作,分解为文件系统上互不干涉可并发执行的读写操作,来提高系统并发处理能力。在新的索引数据模块中,查询评论总数、追加评 论、更新评论状态都是针对性优化过的高效率操作。从这时起,MySQL数据库就降到了只提供归档备份和内部管理查询的角色,不再直接承载任何用户更新和查 询请求了。

同时引入了数据库更新队列来缓解数据库并发写操作的压力,因为当时消息队列中间件远不如现在百花齐放,自行实现了一个简单的文件方式消息队列模块,逐步应用到4.0系统各个模块间异步通信场合中。

图3  4.0系统流程

选用了ICE作为RPC组件,用于所有的模块间调用和网络通信,这大概是刚设计4.0系统时唯一没做错的选择,在整个4.0系统项目生命周期,ICE的稳定性和性能表现从未成为过问题。

图4  4.0索引缓存结构

4.0系统开发语言仍为C++,因为同时选用了MySQL 4.0、ICE、Linux系统和新文件系统等多项应用经验不足的新技术,也为后来的系统表现动荡埋下了伏笔(新浪到2005年左右才逐步从FreeBSD和Solaris迁移到了CentOS系统)。

图5  4.0系统架构

此时的4.0评论系统已从双机互备扩容到五机集群,进入小范围试用阶段,虽然扛过了刘翔第一次夺金时创纪录的发帖高峰,但倒在了2004年亚洲杯中国队1 : 3败于日本队的那个夜晚。

当时系统在进入宕机之前的最高发帖速度大约是每分钟千帖量级,在十年前还算得上是业界同类系统的峰值,最终确认问题出在文件系统的I/O负载上。

设 计索引缓存模块时的设想过于理想化,虽然把单一数据表的读写操作分解到了文件系统的多个文件上,但不可避免地带来了对机械磁盘的大量随机读写操作,在 CentOS默认的Ext3文件系统上,每条新闻对应两个文件的设计(2004年新浪新闻总量为千万左右),虽然已采取了128×256的两层目录 HASH来预防单目录下文件过多隐患,但刚上线时还表现良好的系统,稍过几个月后就把文件系统彻底拖垮了。

既然 Ext3无法应对大数量文件的频繁随机读写,当时我们还可以选择使用B*树数据结构专为海量文件优化的ReiserFS文件系统,在与系统部同事配合反复 对比测试,解决了ReiserFS与特定Linux Kernel版本搭配时的kswapd进程大量消耗CPU资源的问题后,终于选定了可以正常工作的Kernel和ReiserFS对应版本,当然这也埋下 了ReiserFS作者杀妻入狱后新装的CentOS服务器找不到可用的ReiserFS安装包这个大隐患。

第二阶段:全系统异步化,索引分页算法优化

直 到这个阶段,新浪评论系统的前端页面仍是传统的Apache+CGI模式,随着剩余频道的逐步切换,新浪评论系统升级为静态HTML页面使用 XMLHTTP组件异步加载XML数据的AJAX模式,当时跨域限制更少的JSON还未流行。升级为当时刚刚开始流行的AJAX模式并不是盲目追新,而是 为了实现一个非常重要的目标:缓存被动更新的异步化。

随着消息队列的普遍应用,4.0系统中所有的数据库写操作和缓存 主动更新(即后台程序逻辑触发的更新)都异步化了,当时已在实践中证明,系统访问量大幅波动时,模块间异步化通信是解决系统伸缩性和保证系统响应性的唯一 途径。但在CGI页面模式下,由用户动作触发的缓存被动更新,只能阻塞在等待状态,直到查询数据和更新缓存完成后才能返回,会导致前端服务器Apache CGI进程的堆积。

使用AJAX模式异步加载数据,可在几乎不影响用户体验的前提下完成等待和循环重试动作,接收缓 存更新请求的支持优先级的消息队列还可合并对同一页面的重复请求,也隔离了用户行为对前端服务器的直接冲击,极大提高了前端服务器的伸缩性和适应能力,甚 至连低硬件配置的客户端电脑在AJAX模式加载数据时都明显更顺畅了。前端页面静态化还可将全部数据组装和渲染逻辑,包括分页计算都转移到了客户端浏览器 上,充分借用用户端资源,唯一的缺点是对SEO不友好。

通过以上各项措施,此时的4.0系统抗冲击能力已有明显改善,但是接下来出现了新的问题。在3.0系统时代,上万条评论的新闻已属少见,随着业务的增长,类似2005年超女专题或者体育频道NBA专题这样千万评论数级别的巨无霸留言板开始出现。

为 了提高分页操作时定位和读取索引的效率,4.0系统的算法是先通过mmap操作把一个评论的索引文件加载到内存,然后按照评论状态(通过或者删除)和评论 时间进行快速排序,筛选出通过状态的帖子并按时间降序排列,这样读取任意一页的索引数据,都是内存中一次常量时间成本的偏移量定位和读取操作。几百条或者 几千条评论时,上述方案运作得很好,但在千万留言数量的索引文件上进行全量排序,占用大量内存和CPU资源,严重影响系统性能。我们曾尝试改用 BerkeleyDB的Btree模式来存储评论索引,但性能不升反降。

为避免大数据量排序操作的成本,只能改为简单 遍历方式,从头开始依次读取,直到获取所需的数据。虽可通过从索引文件的两端分别作为起点,来提升较新和较早页面的定位效率,但遍历读取本身就是一个随着 请求页数增大越来越慢的线性算法,并且随着4.0系统滑动翻页功能的上线,原本用户无法轻易访问到的中间页面数据也开始被频繁请求,因此最终改为了两端精 确分页,中间模糊分页的方式。模糊分页就是根据评论帖子的通过比例,假设可显示帖子均匀分布,一步跳到估算的索引偏移位置。毕竟在数十万甚至上百万页的评 论里,精确计算分页偏移量没有太大实际意义。

图6  异步缓存更新流程

2005 年非常受关注的日本申请加入联合国常任理事国事件,引发了各家网站的民意沸腾,新浪推出了征集反日入常签名活动并在短短几天内征集到2000多万签名。因 为没有预计到会有如此多的网民参与,最开始简单实现的PHP+MySQL系统在很短时间内就无法响应了,然后基于4.0评论系统紧急加班开发了一个签名请 愿功能,系统表现稳定。

评论系统4.0第三阶段:简化缓存策略,进一步降低文件系统I/O

到 了这个阶段,硬件资源进一步扩容,评论系统的服务器数量终于达到了两位数,4.0系统已实现了当初的“不宕机”设计目标,随着网站的改版,所有新闻页面 (包括网站首页)都开始实时加载和显示最新的评论数量和最新的帖子列表,此时4.0系统承受的Hits量级已接近新浪新闻静态池的水平。从这时起,新浪评 论系统再没有因为流量压力宕机或者暂停服务过。

前面提到,新装的CentOS系统很难找到足够新版本的ReiserFS安装包,甚至不得不降级系统版本,一直困扰性能表现的文件系统也接近了优化的极限,这时候Memcached出现了。

图7  系统架构

2006 年左右Memcached取代了4.0系统中索引缓存模块的实体数据部分(主要是评论正文),索引缓存模块在文件系统上只存储索引数据,评论文本都改用 Memcached存储,极大降低了文件系统的I/O压力。因为系统流量与热点事件的时间相关性,仅保存最近几周的评论就足以保证系统性能,极少量过期数 据访问即使穿透到MySQL也问题不大,当然服务器宕机重启和新装服务器上线时要非常留意数据的加载预热。

之后4.0系统进入稳定状态,小修小补,又坚持服役了若干年,并逐步拓展到股票社区、签名活动、三方辩论、专家答疑、观点投票等产品线,直到2010年之后5.0系统的上线。

2008年5月12日,我发现很多网友在地震新闻评论中询问亲友信息,就立即开发了基于评论系统的地震寻亲功能并于当晚上线。大约一周后为了配合Google发起的寻亲数据汇总项目,还专门为Google爬虫提供了非异步加载模式的数据页面以方便其抓取。

2004年上线的4.0系统,2010~2011年后被5.0系统取代逐步下线,从上线到下线期间系统处理的用户提交数据量变化趋势如图8所示。

图8  系统流量变化图

高访问量UGC系统设计总结

纵观整个4.0系统的设计和优化过程,在硬件资源有限的约束下,依靠过渡设计的多层缓冲,完成了流量剧烈波动时保障服务稳定的最基本目标,但也确实影响到了UGC系统最重要的数据更新实时性指标,数据更新的实时性也是之后5.0系统的重点改进方向。

总结下来,一般UGC系统的设计方针就是通过降低系统次要环节的实时一致性,在合理的成本范围内,尽量提高系统响应性能,而提高响应性能的手段归根结底就是三板斧:队列(Queue)、缓存(Cache)和分区(Sharding)。

  • 队列:可以缓解并发写操作的压力,提高系统伸缩性,同时也是异步化系统的最常见实现手段。
  • 缓存:从文件系统到数据库再到内存的各级缓存模块,解决了数据就近读取的需求。
  • 分区:保证了系统规模扩张和长期数据积累时,频繁操作的数据集规模在合理范围。

关于数据库,区分冷热数据,按照读写操作规律合理拆分存储,一般UGC系统近期数据才是热点,历史数据是冷数据。

  • 区分索引和实体数据,索引数据是Key,易变,一般用于筛选和定位,要保证充分的拆分存储,极端情况下要把关系数据库当NoSQL用;实体数据是Value,一般是正文文本,通常不变,一般业务下只按主键查询;两者要分开。
  • 区分核心业务和附加业务数据,每一项附加的新业务数据都单独存储,与核心业务数据表分开,既可降低核心业务数据库的变更成本,还可避免新业务频繁调整上下线时影响核心业务。

目前的互联网系统大都严重依赖MySQL的Replication主从同步来实现系统横向扩展,虽然MySQL在新版本中陆续加入RBR复制和半同步等机制,但从库的单线程写操作限制还是最大的制约因素,到现在还没有看到很理想的革新性解决方案。

关于缓存,从浏览器到文件系统很多环节都有涉及,这里主要说的是应用系统自己的部分。

  • 最好的缓存方案是不用缓存,缓存带来的问题往往多于它解决的问题。
  • 只有一次更新多次读取的数据才有必要缓存,个性化的冷数据没必要缓存。
  • 缓 存分为主动(Server推)和被动(Client拉)两种更新方式,各自适用于不用场景。主动更新方式一般适用于更新频率较高的热数据,可保证缓存未命 中时,失控的用户行为不会引发系统连锁反应,导致雪崩。被动更新方式一般适用于更新频率相对较低的数据,也可以通过上文提到的异步更新模式,避免连锁反应 和雪崩。
  • 缓存的更新操作尽量设计为覆盖方式,避免偶发数据错误的累积效应。

一 个UGC系统流量刚开始上涨时,初期的表面性能瓶颈一般会表现在Web Server层面,而实际上大多是数据库的原因,但经充分优化后,最终会落在文件系统或网络通信的I/O瓶颈上。直接承载用户访问冲击的前端服务器最好尽 量设计为无状态模式,降低宕机重启后的修复工作量。

顺带提及,我在新浪聊天和评论系统的开发过程中,逐步积累了一个Web应用开发组件库,在新浪全面转向PHP之前,曾用于新浪的内容管理(CMS)、用户注册和通行证、日志分析和论坛等使用C++的系统,目前发布于github.com/pi1ot/webapplib。

评论系统5.0方案

2010 年后针对4.0系统的缺陷,启动了5.0系统工作。因为工作的交接,5.0系统我只负责了方案设计,具体开发是交给其他同事负责的,线上的具体实现与原始 设计方案可能会有区别。5.0系统极大简化了系统层次,在保证抵抗突发流量波动性能的前提下,数据更新的及时性有了明显提高。

图9  4.5系统流程

图10  5.0系统流程

设计方案上的主要变化有以下几点。

  • 评论帖子ID从数据库自增整数改为UUID,提交时即可确定,消除了必须等待主库写入后才能确定评论ID的瓶颈,对各个层面的缓存逻辑优化有极大帮助。
  • 重新设计数据库结构,通过充分的数据切分,保证了所有高频业务操作都可在一个有限数据量的数据表中的一次简单读取操作完成,索引和文本数据隔离存储,在数据库中实现了原4.0系统中索引模块的功能,取消了4.0系统的索引缓存层。
  • 改用内存NoSQL缓存用户频繁读取的最新10~20页数据,取消了原4.0系统文件方式的页面缓存层。
  • 系统运行环境迁移到新浪云的内部版本:新浪动态平台,设备资源富裕度有了极大改善。
  • 改为Python语言开发,不用再像4.0系统那样每次更新时都要等待半个小时的编译过程,也不用再打包几百兆的执行文件同步到几十台服务器上,而语言层面的性能损失可以忽略不计。

新闻评论产品总结

新 闻评论作为微博之前最能反映舆情民意的UGC平台,长期承载了国内互联网用户对时事新闻的匿名表达欲望,曾经一度成为上到政府下到网民的关注焦点。虽然面 临了相对其他社区系统更为严厉的管控力度,也错过了实施实名制改造时迈向社区化的最佳时机,但无论如何,在21世纪的前十年,国内门户网站的新闻评论服 务,都是中国互联网产品和技术发展历史上绝对不能错过的一笔。

作者刘立,2000年毕业于哈尔滨工业大学计算机系,2000-2013年工作于新浪网研发中心和门户技术部门,目前在一家社交电商平台创业团队任技术负责人。

[转载]Android ImageView如何加载网络图片资源 - 尝尽世间绚烂 - ITeye技术网站

mikel阅读(978)

[转载]Android ImageView如何加载网络图片资源 – 尝尽世间绚烂 – ITeye技术网站.

package com.android.antking.imageview;

import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;

public class MainActivity extends Activity {
	//定义一个图片显示控件
	private ImageView imageView;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        //图片资源
        String url = "http://s16.sinaimg.cn/orignal/89429f6dhb99b4903ebcf&690";
        //得到可用的图片
        Bitmap bitmap = getHttpBitmap(url);
        imageView = (ImageView)this.findViewById(R.id.imageViewId);
        //显示
        imageView.setImageBitmap(bitmap);
       
    }
    /**
     * 获取网落图片资源 
     * @param url
     * @return
     */
    public static Bitmap getHttpBitmap(String url){
    	URL myFileURL;
    	Bitmap bitmap=null;
    	try{
    		myFileURL = new URL(url);
    		//获得连接
    		HttpURLConnection conn=(HttpURLConnection)myFileURL.openConnection();
    		//设置超时时间为6000毫秒,conn.setConnectionTiem(0);表示没有时间限制
    		conn.setConnectTimeout(6000);
    		//连接设置获得数据流
    		conn.setDoInput(true);
    		//不使用缓存
    		conn.setUseCaches(false);
    		//这句可有可无,没有影响
    		//conn.connect();
    		//得到数据流
    		InputStream is = conn.getInputStream();
    		//解析得到图片
    		bitmap = BitmapFactory.decodeStream(is);
    		//关闭数据流
    		is.close();
    	}catch(Exception e){
    		e.printStackTrace();
    	}
    	
		return bitmap;
    	
    }
}

[转载]TortoiseSVN 合并操作简明教程 - Roy Cheng - 博客园

mikel阅读(1025)

[转载]TortoiseSVN 合并操作简明教程 – Roy Cheng – 博客园.

下列步骤展示了如何将分支A中的修改合并到分支B

1.分支B的本地副本目录中选择合并(Merge

2.选择“合并一个版本范围(Merge a range of revisions)”,点击下一步。

3.输入分支Asvn服务器端路径(注意:此路径应与步骤1中的分支B本地路径相对应)。

4.选择需要合并的修改内容,并点击下一步。

5.合并(Merge

6.解决冲突(如果有的话),提交代码。完成!

38分钟,100亿,什么概念!!!!

mikel阅读(1193)

双十一还没结束,交易额已经让人大呼见鬼了!!

38分钟,100亿,什么概念!!一分钟平均2.63亿!!一秒钟438.33333万!!!

阿里你们用的是啥服务器?! 这是我第一反应,这么大的交易量,还能正常运转,可见技术实力多强,10年积累的技术团队来支持这么一个庞大的平台运转正常,实属不易!!

除了这些贡献交易额的女人们,马云背后的男人们更让人感动!!

但是最悲催的是今天的光棍儿们,活生生的狂欢节被阿里一下子弄成了购物节!买啥啥特价,线上线下都是人,这让光棍儿们情何以堪啊!!!

双十一还在继续,我们拭目以待最终的成交量,就算是拧拧水份,怎么也得上百亿不成问题了,考验全国物流的时刻随着双十一的落幕即将开启,然后是各家的客服能力!!!

电商从网络到线下的任督二脉还有多少症结没有打开,就让双十一来试试,电商到底哪家强吧!?

每年双十一貌似都像是互联网给传统企业的上的一次大课,用成交量来证明,互联网时代来了,赶紧触网要不然就分不到一杯羹了!!!

也是再教育消费者,现在人消费都理性了,不再那么刚需驱动,没有啥必须要买的,今天我逛了逛发觉自己真不知道买啥,看着热火朝天的天猫界面,当个看客,总觉得这巨大交易量的背后是不是缺点儿什么?

至于缺啥,一时半会儿说不清楚,总感觉看着那些商品页面,没啥人情味在里面,就是买卖!

那些改变生活的技术

mikel阅读(962)

智能手机真的改变了人们的生活方式,如果手机突然没电了,在中国真得是彻底消失了,因为没有固定电话亭了,那么没有手机支付,是不是就没有团购了呢?

还真的的是那样,阿里创造了个支付帝国,最近又推出5000元透支阿里金融计划,真得是拼了。

未来技术会更加平民化,人人都能编程做网站已经基本实现了,下一步就是人人都能做应用了,最近起来很多第三方开发平台,盯着移动应用开发这块蛋糕,不知道下一个蓝海市场是不是就在此。

传统行业都在被颠覆,可传统依然是传统不是一时半会儿能颠覆完的,怎么也得有个过渡期,拿餐饮行业来说,雕爷牛腩算是颠覆吗?不完全是,只是一种新的玩儿法而已。

下一步估计要颠覆餐饮行业得是移动应用和大数据,利用微信这个人最多的平台,订餐、团购、在线支付,甚至订包间,然后对接餐厅的管理系统,直接走小米的抢购模式,按需采购蔬菜、酒水等等,然后大厨后台提前操作备料,最后坐等顾客到店品尝了。

那这套系统怎么收费?收费已经没有必要了,中间各个环节节省下来的开支,提高得效率,免费就ok,然后建立一个线上线下的推荐平台,羊毛出在猪身上才赚钱,想吃饭少不了酒水,那就在线订餐推荐酒水,想被推荐那就竞价推荐位吧。

总而言之,技术只是个辅助,重要的还是运营和模式。

[转载]Ecshop的transport.js/run() error:undefined_hello_新浪博客

mikel阅读(820)

在使用ECshop的AJAX(即:transport.js)

IE有时候会出现:ReferenceError: process_request is not defined,FF则出现:transport.js/run() error:undefined,其实这完全和transport.js无关。那么问题出在哪里呢?

(1)首先找到445-447行发现了这两句:

/* 定义两个别名 */
var Ajax = Transport;
Ajax.call = Transport.run;

(2)然后再找到735发现了这句:

Ajax.onRunning = showLoader;

哈哈….其实问题就是出在这句的showLoader函数里面。因为在该函数里753行有这段语句

if ( ! obj && process_request),而出现上述问题就是说变量process_request没有定义!!

(3)为什么说该变量没有定义呢?

原因很简单,因为EC很多时候都是将这句话:

<script type=”text/JavaScript”>
var process_request = “{$lang.process_request}”;

…..

</script>

放在最后面的….在中途有其他什么处理、JS载入、JS运行而还没运行到最下面的时候当然就出现在上述的错误!!!

process_request这个变量到底有什么用呢?其实就是为了创建一个DIV层显示“正在处理您的请求…”这个信息!!为什么要这样实现呢?因为这样可以支持多语言。

解决办法:

(1)在showLoader函数体里面加入这句var process_request = ‘正在处理您的请求…’;英文的话就改成英文了。。。

(2)将var process_request = “{$lang.process_request}”;这句话放在<header>下面的<script>里面

(3)重新定义Ajax.onRunning这一事件例如可以自己新建一个函数run,Ajax.onRunning = run

第二种方法:对于这个问题,官方管理员的回复是”有时ajax抓取订单信息不全,出现这样的错误信息,但这个不会影响您的正常使用,请您放心。如果要关 闭,可以在后台里的商店设置 基本设置中去除。”,实际上,按照官方这个说法,问题还是照样存在,解决的办法是“眼不见,心不烦”,将这条弹出语句注释掉: 找到js/transport.js,第227行: alert(this.filename + “/run() error:” + ex.description); 复制代码将之改为: /* alert(this.filename + “/run() error:” + ex.description); */

[转载]Prototype之详细解说[转] - Sam Lin - 博客园

mikel阅读(1058)

[转载]Prototype之详细解说[转] – Sam Lin – 博客园.

/*  Prototype JavaScript framework, version 1.4.0

 *  (c) 2005 Sam Stephenson <sam@conio.net>

 *

 *  Prototype is freely distributable under the terms of an MIT-style license.

 *  For details, see the Prototype web site: http://prototype.conio.net/

 

 *  这是一个JavaScript的框架,致力于简化动态的Web开发,完全按照面对对象的思想

 *  进行Javascript开发,添加了迭代器的概念增强Javascript的设计能力。

 *  Prototype框架构思独特充满了技巧性的代码,方便易用的工具类!

 *

/*--------------------------------------------------------------------------*/

 

  

/* 【Prototype】定义一个全局对象,提供一个全局的基本信息和工具 */

var Prototype = {

  Version: '1.4.0', //可以作为版本检测用

//用于脚本检测的正则表达式,经常使用所以放在这里起到全局常量的作用

  ScriptFragment: '(?:<script.*?>)(( | |.)*?)(?:</script>)',



  emptyFunction: function() {},//空函数

  K: function(x) {return x}//K方法返回参数本身,在后面经常用到

}



/*========================================================================================*/



/*

 *【Class】对象的作用只有一个就是提供了一个定义类的模式

 * 仅含有create 一个方法,返回一个构造函数。 

 * 一般使用如下  

 *     var X = Class.create();  返回一个类型  

 * 要使用 X 类型,需继续用 new X()来获取一个实例 这与C# JAVA类似

 * 返回的构造函数会执行名为 initialize 的方法, initialize 是 Ruby 对象的构造器方法名字。 

 * 此时initialize方法还没有定义,其后的代码中创建新类型时会建立相应的同名方法,可以看作是一个抽象方法。 

 * 从C#角度讲可以理解为用Class.create()创建一个继承Object基类的类。 

 * 

 */ 



var Class = {

  create: function() {

    return function() {  //下面使用Apply方法传递参数是一个常用的技巧

      this.initialize.apply(this, arguments);

    }

  }

}



/*========================================================================================*/





//Abstract是一个空对象,它的作用仅仅是作为一个抽象的命名空间,所有在该对象下定义的类都是抽象类

//从而从形式上与实体类分开

var Abstract = new Object();



/*

 *Object是所有对象的基类,这个基类有两个静态方法

 */ 

//把Source的所有属性和方法传递给Destination,实现了继承的效果

Object.extend = function(destination, source) {

  for (property in source) {

    destination[property] = source[property];

  }

  return destination;

}

//观察Object的组成,需要参数中的object实现自己的inspect方法

Object.inspect = function(object) {

  try {

    if (object == undefined) return 'undefined';

    if (object == null) return 'null';

    return object.inspect ? object.inspect() : object.toString();

  } catch (e) {

    if (e instanceof RangeError) return '';

    throw e;

  }

}



/*========================================================================================*/





//【Function】是所有函数对象的基类,可以通过对它的扩展实现对所有函数对象添加功能

/* 这里在绑定的时候旧可以传递参数而不是调用的时候才指定参数,bind方法接收多个参数

  将函数绑定到第一个参数指定的对象上,并返回该方法的调用句柄

*/

Function.prototype.bind = function() {

  // $A()方法的作用是把参数专为数组 

  //数组的shift方法作用是删除数组的第一个元素

  var __method = this, args = $A(arguments), object = args.shift();

  return function() {

    return __method.apply(object, args.concat($A(arguments)));

    //这里的$A(arguments)是条用返回函数句柄时传递的参数,不是bind方法的参数

  }

}

/** 

 * 和bind一样,不过这个方法一般用做html控件对象的事件处理。所以要传递event对象 

 * 好像是重载了_method方法

 */ 



Function.prototype.bindAsEventListener = function(object) {

  var __method = this;

  return function(event) {

    return __method.call(object, event || window.event);

  }

}



/*========================================================================================*/



//【Function】是所有数值类型的基类



Object.extend(Number.prototype, {

  toColorPart: function() {           //RGB-》16进制颜色

    var digits = this.toString(16);

    if (this < 16) return '0' + digits;

    return digits;

  },



  succ: function() { //数值加一 var a=1; var b=a.succ; 那么b=2

    return this + 1;

  },



  times: function(iterator) { 

  //这里的参数iterator是一个迭代器作用就是循环调用指定次数的iterator函数

  //$R(start,end,exclusive)是创建ObjectRnge对象的快捷方式

    $R(0, this, true).each(iterator);

    return this;

  }

});



/*========================================================================================*/



//提供了一个方法these,要求参数是函数的句柄,可以有多个。作用就是返回第一个成功执行的函数的返回值

var Try = {

  these: function() {

    var returnValue;



    for (var i = 0; i < arguments.length; i++) {

      var lambda = arguments[i];

      try {

        returnValue = lambda();

        break;

      } catch (e) {}

    }



    return returnValue;

  }

}



/*========================================================================================*/



//定时器类用来实现Window.setInterval的效果,在给定时间间隔执行某一个函数,增加了对重复执行的控制S

var PeriodicalExecuter = Class.create();

PeriodicalExecuter.prototype = {

  initialize: function(callback, frequency) {

  //构造函数指定回调函数和执行频率,单位是秒

    this.callback = callback;

    this.frequency = frequency;

    this.currentlyExecuting = false;



    this.registerCallback();

  },

  

/* 开始调用定时器,无需显示调用,在构造函数中就实现了自动调用,这一下面的

this.onTimerEvent.bind(this)如果写成this.onTimerEvent则this指针就会指向widows对象即setInterval的默认对象

从而不能正确的引用到下面两个函数上,也就失去了对正在执行函数的控制

*/



  registerCallback: function() {

    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);

  },

//下面的函数相当于是回调函数的一个代理, setInterval是到了指定的时间就会强制执行而这里

//加入了一个判断如果callback函数执行的时间超过了一个时间片,则阻止其被重复执行

  onTimerEvent: function() {

    if (!this.currentlyExecuting) {

      try {

        this.currentlyExecuting = true;

        this.callback();

      } finally {

        this.currentlyExecuting = false;

      }

    }

  }

}

/*使用举例:

function GetOnlineCount()

{//获得在线人数

}

new PeriodicalExecuter(GetOnlineCount,10)每10秒获取一次

*/



/*========================================================================================*/

/* 框架核心内容--------【基础工具类】

/*========================================================================================*/



 /*document.getElementById(id)获取一个指定ID的结点,是这个方法的快捷方式和扩展

 可以指定多个参数返回一个对象数组。参数也不一定是ID也可以是对象本身的引用,例如

 $('id')等价于$($('id'))

  */

function $() {

  var elements = new Array();



  for (var i = 0; i < arguments.length; i++) {

    var element = arguments[i];

    if (typeof element == 'string')

      element = document.getElementById(element);



    if (arguments.length == 1)

      return element;



    elements.push(element);

  }



  return elements;

}



/*========================================================================================*/

//【String】类的扩展



//删除字符串中的HTML标记

Object.extend(String.prototype, {

  stripTags: function() {

    return this.replace(/</?[^>]+>/gi, '');

  },

//删除字符串中的脚本块

  stripScripts: function() {

    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');

  },

//提取字符串中的所有脚本块,作为数组返回,每一个脚本作为一个数组元素

  extractScripts: function() {

    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');

    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');

    return (this.match(matchAll) || []).map(function(scriptTag) {

      return (scriptTag.match(matchOne) || ['', ''])[1];

    });

  },

//提取字符串中的脚本并执行

  evalScripts: function() {

    return this.extractScripts().map(eval);

  },

//将字符串进行html编码例如"<" --》"&lt"

  escapeHTML: function() {

    var div = document.createElement('div');

    var text = document.createTextNode(this);

    div.appendChild(text);

    return div.innerHTML;

  },

//对字符串进行html解码例如"&lt"--》"<"

  unescapeHTML: function() {

    var div = document.createElement('div');

    div.innerHTML = this.stripTags();

    return div.childNodes[0] ? div.childNodes[0].nodeValue : '';

  },

//将查询字符串格式的字符串转换为键值对数组

//例如var s="a=1&b=2&c=3"; var s2=s.toQueryParams(); s2为{a:1,b:2,c:3}

  toQueryParams: function() {

    var pairs = this.match(/^??(.*)$/)[1].split('&');

    return pairs.inject({}, function(params, pairString) {

      var pair = pairString.split('=');

      params[pair[0]] = pair[1];

      return params;

    });

  },

//字符串转换为字符数组 "abc"-->['a','b','c']

  toArray: function() {

    return this.split('');

  },

//连字符--》骆驼样式 'good-man'-->'goodMan'

  camelize: function() {

    var oStringList = this.split('-');

    if (oStringList.length == 1) return oStringList[0];



    var camelizedString = this.indexOf('-') == 0

      ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)

      : oStringList[0];



    for (var i = 1, len = oStringList.length; i < len; i++) {

      var s = oStringList[i];

      camelizedString += s.charAt(0).toUpperCase() + s.substring(1);

    }



    return camelizedString;

  },

//得到字符串的组成结构

  inspect: function() {

    return "'" + this.replace('\', '\\').replace("'", '\'') + "'";

  }

});

//定义了一个等价的函数

String.prototype.parseQuery = String.prototype.toQueryParams;



/*========================================================================================*/



//【Enumerable】可枚举接口 是整个1.4.0框架的核心工具,所有实现此接口的类必须要实现_each(iterator)方法





var $break    = new Object(); //首先定义了两个异常对象,用于进行迭代计算的控制

var $continue = new Object();



var Enumerable = {

//用于对对象的每一个元素遍历执行iterator迭代器函数

  each: function(iterator) {

    var index = 0;                //可选参数表示元素在枚举对象的次序

    try {

      this._each(function(value) {//value是枚举元素的值

        try {

          iterator(value, index++);

        } catch (e) {

          if (e != $continue) throw e;

        }

      });

    } catch (e) {

      if (e != $break) throw e;

    }

  },

//判断是否所有的枚举元素都能使iterator返回true

  all: function(iterator) {

    var result = true;

    this.each(function(value, index) {

      result = result && !!(iterator || Prototype.K)(value, index);

      if (!result) throw $break;

    });

    return result;

  },

//判断是否有枚举元素能使iterator返回true,有一个就是True

  any: function(iterator) {

    var result = true;

    this.each(function(value, index) {

      if (result = !!(iterator || Prototype.K)(value, index))

        throw $break;

    });

    return result;

  },

//对所有的枚举元素执行iterator迭代器函数 结果作为一个数组返回

  collect: function(iterator) {

    var results = [];

    this.each(function(value, index) {

      results.push(iterator(value, index));

    });

    return results;

  },

//第一个素能使iterator返回true的枚举元素的值,没有返回undefined

  detect: function (iterator) {

    var result;

    this.each(function(value, index) {

      if (iterator(value, index)) {

        result = value;

        throw $break;

      }

    });

    return result;

  },

//找到所有的能使iterator迭代器函数返回true的枚举元素 作为一个数组返回

  findAll: function(iterator) {

    var results = [];

    this.each(function(value, index) {

      if (iterator(value, index))

        results.push(value);

    });

    return results;

  },

//找到素有匹配pattern的枚举元素,结果作为数组返回,iterator可选,如果不指定旧返回素有匹配pattern的枚举元素

  grep: function(pattern, iterator) {//正则模式 迭代器

    var results = [];

    this.each(function(value, index) {

      var stringValue = value.toString();

      if (stringValue.match(pattern))

        results.push((iterator || Prototype.K)(value, index));

    })

    return results;

  },

//判断枚举对象中是否含有参数Object指定的值

  include: function(object) {

    var found = false;

    this.each(function(value) {

      if (value == object) {

        found = true;

        throw $break;

      }

    });

    return found;

  },

//将memo作为iterator的第一个参数,枚举元素作为iterator的第二个参数,枚举元素的次序作为第三个

//参数每次迭代器的返回值将作为下一个iterator的memo参数,从而所有的迭代执行都通过memo联系起来了



  inject: function(memo, iterator) {

    this.each(function(value, index) {

      memo = iterator(memo, value, index);

    });

    return memo;

  },

//对所有的枚举元素执行method方法 后面是要传递的参数

  invoke: function(method) {

    var args = $A(arguments).slice(1);

    return this.collect(function(value) {

      return value[method].apply(value, args);

    });

  },

//返回的最大的迭代器执行结果

  max: function(iterator) {

    var result;

    this.each(function(value, index) {

      value = (iterator || Prototype.K)(value, index);

      if (value >= (result || value))

        result = value;

    });

    return result;

  },

//反之

  min: function(iterator) {

    var result;

    this.each(function(value, index) {

      value = (iterator || Prototype.K)(value, index);

      if (value <= (result || value))

        result = value;

    });

    return result;

  },

//返回两个数组一组能使iterator返回true 另一组返回false

  partition: function(iterator) {

    var trues = [], falses = [];

    this.each(function(value, index) {

      ((iterator || Prototype.K)(value, index) ?

        trues : falses).push(value);

    });

    return [trues, falses];

  },

//获取所有枚举元素的property属性值作为数组的返回

  pluck: function(property) {

    var results = [];

    this.each(function(value, index) {

      results.push(value[property]);

    });

    return results;

  },

//与findall相反 

  reject: function(iterator) {

    var results = [];

    this.each(function(value, index) {

      if (!iterator(value, index))

        results.push(value);

    });

    return results;

  },

//根据iterator的结果排序 最小的在前面 作为数组返回

  sortBy: function(iterator) {

    return this.collect(function(value, index) {

      return {value: value, criteria: iterator(value, index)};

    }).sort(function(left, right) {

      var a = left.criteria, b = right.criteria;

      return a < b ? -1 : a > b ? 1 : 0;

    }).pluck('value');

  },

//枚举对象--》数组

  toArray: function() {

    return this.collect(Prototype.K);

  },

//接收多个枚举对象参数,最后一个可以是迭代器用于阵列转换

  zip: function() {

    var iterator = Prototype.K, args = $A(arguments);

    if (typeof args.last() == 'function')

      iterator = args.pop();



    var collections = [this].concat(args).map($A);

    return this.map(function(value, index) {

      iterator(value = collections.pluck(index));

      return value;

    });

  },

//返回枚举对象的字符串描述

  inspect: function() {

    return '#<Enumerable:' + this.toArray().inspect() + '>';

  }

}

//等价函数定义

Object.extend(Enumerable, {

  map:     Enumerable.collect,

  find:    Enumerable.detect,

  select:  Enumerable.findAll,

  member:  Enumerable.include,

  entries: Enumerable.toArray

});



/*========================================================================================*/

//【Array】数组对象



//参数转换为数组,如果参数定义了toarray则直接调用,否则枚举获得数组

var $A = Array.from = function(iterable) {

  if (!iterable) return [];

  if (iterable.toArray) {

    return iterable.toArray();

  } else {

    var results = [];

    for (var i = 0; i < iterable.length; i++)

      results.push(iterable[i]);

    return results;

  }

}

//数组对象继承了Enumerable接口

Object.extend(Array.prototype, Enumerable);



Array.prototype._reverse = Array.prototype.reverse;



Object.extend(Array.prototype, {

//实现了枚举接口方法,用于对数组内的元素执行迭代器

  _each: function(iterator) {

    for (var i = 0; i < this.length; i++)

      iterator(this[i]);

  },

//清空数组

  clear: function() {

    this.length = 0;

    return this;

  },

//取得第一个元素

  first: function() {

    return this[0];

  },

//取得最后一个元素

  last: function() {

    return this[this.length - 1];

  },

//删除数组元素中所有的null undefined值 作为新数组返回,原数组不受影响

  compact: function() {

    return this.select(function(value) {

      return value != undefined || value != null;

    });

  },

//展开所有数组元素不再嵌套

  flatten: function() {

    return this.inject([], function(array, value) {

      return array.concat(value.constructor == Array ?

        value.flatten() : [value]);

    });

  },

//数组中删除指定的元素,返回删除后的结果,原数组不受影响

  without: function() {

    var values = $A(arguments);

    return this.select(function(value) {

      return !values.include(value);

    });

  },

//Value元素在数组中的索引值

  indexOf: function(object) {

    for (var i = 0; i < this.length; i++)

      if (this[i] == object) return i;

    return -1;

  },

//反转数组

  reverse: function(inline) {

    return (inline !== false ? this : this.toArray())._reverse();

  },

//删除数组中第一个元素并返回这个元素的值

  shift: function() {

    var result = this[0];

    for (var i = 0; i < this.length - 1; i++)

      this[i] = this[i + 1];

    this.length--;

    return result;

  },

//得到数组的字符串描述例如arr=[1,2,3]-----> "[1,2,3]"

  inspect: function() {

    return '[' + this.map(Object.inspect).join(', ') + ']';

  }

});



/*========================================================================================*/

//【Hash】哈希对象





var Hash = {

//实现枚举接口方法从而使Hash对象也是枚举对象

  _each: function(iterator) {

    for (key in this) {

      var value = this[key];

      if (typeof value == 'function') continue;



      var pair = [key, value];

      pair.key = key;

      pair.value = value;

      iterator(pair);

    }

  },

//所有键数组

  keys: function() {

    return this.pluck('key');

  },

//所有值数组

  values: function() {

    return this.pluck('value');

  },

//合并哈希表,键相同就覆盖调用者

  merge: function(hash) {

    return $H(hash).inject($H(this), function(mergedHash, pair) {

      mergedHash[pair.key] = pair.value;

      return mergedHash;

    });

  },

//键值对组成查询字符串的形式

  toQueryString: function() {

    return this.map(function(pair) {

      return pair.map(encodeURIComponent).join('=');

    }).join('&');

  },

//获取Hash对象的字符串描述

  inspect: function() {

    return '#<Hash:{' + this.map(function(pair) {

      return pair.map(Object.inspect).join(': ');

    }).join(', ') + '}>';

  }

}

/*哈希表不是类而是对象所以不能用new的方法创建,而是用$H()函数

  这个方法吧一个对象转换为哈希对象,对象的属性名为key 值为value

  可枚举!!

*/

function $H(object) {

  var hash = Object.extend({}, object || {});

  Object.extend(hash, Enumerable);

  Object.extend(hash, Hash);

  return hash;

}



/*========================================================================================*/

//【ObjectRange】用于进行指定次数的循环运算,同样继承于枚举接口并实现_each方法

//有了这个方法基本上Javascript里面就不用for循环了





ObjectRange = Class.create();

Object.extend(ObjectRange.prototype, Enumerable);

Object.extend(ObjectRange.prototype, {

//构造函数同事指定了3个参数的值

  initialize: function(start, end, exclusive) {

    this.start = start; //其实索引

    this.end = end;//结束索引

    this.exclusive = exclusive;//表示是否包含结束索引

  },



  _each: function(iterator) {

    var value = this.start;

    do {

      iterator(value);

      value = value.succ();

    } while (this.include(value));

  },

//重写枚举接口的include方法,判断改循环的索引是否包含参数value的指定的值

  include: function(value) {

    if (value < this.start)

      return false;

    if (this.exclusive)

      return value < this.end;

    return value <= this.end;

  }

});

//使用$R()方法快速创建objectRange对象

var $R = function(start, end, exclusive) {

  return new ObjectRange(start, end, exclusive);

}



/*========================================================================================*/

/* 【Ajax模块】

/*========================================================================================*/

//【ajax对象】



var Ajax = {//创建浏览器兼容的XMLHttpRequest对象

  getTransport: function() {

    return Try.these(

      function() {return new ActiveXObject('Msxml2.XMLHTTP')},

      function() {return new ActiveXObject('Microsoft.XMLHTTP')},

      function() {return new XMLHttpRequest()}

    ) || false;

  },

//当前激活的请求数目

  activeRequestCount: 0

}

//【Ajax.Responders对象】

Ajax.Responders = {

  responders: [],//表示所有响应处理代理



  _each: function(iterator) {

    this.responders._each(iterator);

  },

//注册一个响应代理

  register: function(responderToAdd) {

    if (!this.include(responderToAdd))

      this.responders.push(responderToAdd);

  },

//删除一个响应代理

  unregister: function(responderToRemove) {

    this.responders = this.responders.without(responderToRemove);

  },

//分发一个回调函数,让所有半酣callback回调函数事件标识符的响应代理都被调用

//传递三个参数最后一个可选表示Json对象

  dispatch: function(callback, request, transport, json) {

    this.each(function(responder) {

      if (responder[callback] && typeof responder[callback] == 'function') {

        try {

          responder[callback].apply(responder, [request, transport, json]);

        } catch (e) {}

      }

    });

  }

};



Object.extend(Ajax.Responders, Enumerable);

//统计当前活动请求的数目

Ajax.Responders.register({

//开始创建了一个请求

  onCreate: function() {

    Ajax.activeRequestCount++;

  },

//请求结束

  onComplete: function() {

    Ajax.activeRequestCount--;

  }

});



//【Ajax.Base类】进行服务器通信的基类

Ajax.Base = function() {};

Ajax.Base.prototype = {

  setOptions: function(options) {

    this.options = {

      method:       'post',

      asynchronous: true,

      parameters:   ''

    }

    Object.extend(this.options, options || {});

  },



  responseIsSuccess: function() {

    return this.transport.status == undefined

        || this.transport.status == 0

        || (this.transport.status >= 200 && this.transport.status < 300);

  },



  responseIsFailure: function() {

    return !this.responseIsSuccess();

  }

}

//【Ajax.Request类】用于向服务器端发送请求,封装了XmlHttpRequest

Ajax.Request = Class.create();

Ajax.Request.Events =

  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];



Ajax.Request.prototype = Object.extend(new Ajax.Base(), {

  initialize: function(url, options) {

    this.transport = Ajax.getTransport();

    this.setOptions(options);//使用基类方法设置

    this.request(url);

  },



//向指定url发送请求 一般不会显示调用

  request: function(url) {

    var parameters = this.options.parameters || '';

    if (parameters.length > 0) parameters += '&_=';



    try {

      this.url = url;

      if (this.options.method == 'get' && parameters.length > 0)

        this.url += (this.url.match(/?/) ? '&' : '?') + parameters;



      Ajax.Responders.dispatch('onCreate', this, this.transport);



      this.transport.open(this.options.method, this.url,

        this.options.asynchronous);



      if (this.options.asynchronous) {

        this.transport.onreadystatechange = this.onStateChange.bind(this);

        setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);

      }



      this.setRequestHeaders();



      var body = this.options.postBody ? this.options.postBody : parameters;

      this.transport.send(this.options.method == 'post' ? body : null);



    } catch (e) {

      this.dispatchException(e);

    }

  },

//设置请求的Http头 类内部使用 一般无需显示调用

  setRequestHeaders: function() {

    var requestHeaders =

      ['X-Requested-With', 'XMLHttpRequest',

       'X-Prototype-Version', Prototype.Version];



    if (this.options.method == 'post') {

      requestHeaders.push('Content-type',

        'application/x-www-form-urlencoded');



      /* Force "Connection: close" for Mozilla browsers to work around

       * a bug where XMLHttpReqeuest sends an incorrect Content-length

       * header. See Mozilla Bugzilla #246651.

       */

      if (this.transport.overrideMimeType)

        requestHeaders.push('Connection', 'close');

    }



    if (this.options.requestHeaders)

      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);



    for (var i = 0; i < requestHeaders.length; i += 2)

      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);

  },

//检测XMLHttpRquest对象的onstatechange事件类内部使用 一般无需显示调用

  onStateChange: function() {

    var readyState = this.transport.readyState;

    if (readyState != 1)

      this.respondToReadyState(this.transport.readyState);

  },

//获取指定的Http头的内容

  header: function(name) {

    try {

      return this.transport.getResponseHeader(name);

    } catch (e) {}

  },

//如果服务器返回了Http头"X-JOSN"则将其作为js执行并返回执行结果

  evalJSON: function() {

    try {

      return eval(this.header('X-JSON'));

    } catch (e) {}

  },

//将XMLHttpRequest返回的responseText作为JS语句执行并返回执行结果

  evalResponse: function() {

    try {

      return eval(this.transport.responseText);

    } catch (e) {

      this.dispatchException(e);

    }

  },

//处理XMLHttpRequest的readystate属性根据成功或失败调用Options对象中相应的回调函数

//同时通知Ajax.Responders中注册的响应处理句柄

  respondToReadyState: function(readyState) {

    var event = Ajax.Request.Events[readyState];

    var transport = this.transport, json = this.evalJSON();



    if (event == 'Complete') {

      try {

        (this.options['on' + this.transport.status]

         || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]

         || Prototype.emptyFunction)(transport, json);

      } catch (e) {

        this.dispatchException(e);

      }



      if ((this.header('Content-type') || '').match(/^text/javascript/i))

        this.evalResponse();

    }



    try {

      (this.options['on' + event] || Prototype.emptyFunction)(transport, json);

      Ajax.Responders.dispatch('on' + event, this, transport, json);

    } catch (e) {

      this.dispatchException(e);

    }



    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */

    if (event == 'Complete')

      this.transport.onreadystatechange = Prototype.emptyFunction;

  },



  dispatchException: function(exception) {

    (this.options.onException || Prototype.emptyFunction)(this, exception);

    Ajax.Responders.dispatch('onException', this, exception);

  }

});





/*========================================================================================*/

//【Ajax.Updater类】用于将获取的内容填充到指定的容器中去

Ajax.Updater = Class.create();



Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {

//重写了构造函数

  initialize: function(container, url, options) {

    this.containers = {

      success: container.success ? $(container.success) : $(container),

      failure: container.failure ? $(container.failure) :

        (container.success ? null : $(container))

    }



    this.transport = Ajax.getTransport();

    this.setOptions(options);//可以指定evalscripts属性和insertion属性



    var onComplete = this.options.onComplete || Prototype.emptyFunction;

    this.options.onComplete = (function(transport, object) {

      this.updateContent();

      onComplete(transport, object);

    }).bind(this);



    this.request(url);

  },



  updateContent: function() {

    var receiver = this.responseIsSuccess() ?

      this.containers.success : this.containers.failure;

    var response = this.transport.responseText;



    if (!this.options.evalScripts)

      response = response.stripScripts();



    if (receiver) {

      if (this.options.insertion) {

        new this.options.insertion(receiver, response);

      } else {

        Element.update(receiver, response);

      }

    }



    if (this.responseIsSuccess()) {

      if (this.onComplete)

        setTimeout(this.onComplete.bind(this), 10);

    }

  }

});



/*========================================================================================*/

//【Ajax.PeriodicalUpdater类】用于定时执行服务器异步调用,功能与Updater相同只是有了定时功能



Ajax.PeriodicalUpdater = Class.create();

Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {

  initialize: function(container, url, options) {

    this.setOptions(options);

    this.onComplete = this.options.onComplete;

//区别就在下面两个属性 频率 衰减参数 指数增长!只要有一次不同旧回复原有频率

    this.frequency = (this.options.frequency || 2);

    this.decay = (this.options.decay || 1);



    this.updater = {};

    this.container = container;

    this.url = url;



    this.start();

  },



  start: function() {

    this.options.onComplete = this.updateComplete.bind(this);

    this.onTimerEvent();

  },



  stop: function() {

    this.updater.onComplete = undefined;

    clearTimeout(this.timer);

    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);

  },



  updateComplete: function(request) {

    if (this.options.decay) {

      this.decay = (request.responseText == this.lastText ?

        this.decay * this.options.decay : 1);



      this.lastText = request.responseText;

    }

    this.timer = setTimeout(this.onTimerEvent.bind(this),

      this.decay * this.frequency * 1000);

  },



  onTimerEvent: function() {

    this.updater = new Ajax.Updater(this.container, this.url, this.options);

  }

});





/*========================================================================================*/

/*【文档操作的封装】

/*========================================================================================*/



//返回了一个数组包含了所有符合条件的结点的引用

document.getElementsByClassName = function(className, parentElement) { //parentElement不指定就在全局查找

  var children = ($(parentElement) || document.body).getElementsByTagName('*');

  return $A(children).inject([], function(elements, child) {

    if (child.className.match(new RegExp("(^|\s)" + className + "(\s|$)")))

      elements.push(child);

    return elements;

  });

}



/*【Element对象】用于对文档结点做一些统一的操作



*/



if (!window.Element) {

  var Element = new Object();

}



Object.extend(Element, {

  visible: function(element) {

    return $(element).style.display != 'none';

  },

//指定结点可见性的切换

  toggle: function() {

    for (var i = 0; i < arguments.length; i++) {

      var element = $(arguments[i]);

      Element[Element.visible(element) ? 'hide' : 'show'](element);

    }

  },



  hide: function() {

    for (var i = 0; i < arguments.length; i++) {

      var element = $(arguments[i]);

      element.style.display = 'none';

    }

  },



  show: function() {

    for (var i = 0; i < arguments.length; i++) {

      var element = $(arguments[i]);

      element.style.display = '';

    }

  },



  remove: function(element) {

    element = $(element);

    element.parentNode.removeChild(element);

  },

//将html片段填充到element指定的结点中

  update: function(element, html) {

    $(element).innerHTML = html.stripScripts();

    setTimeout(function() {html.evalScripts()}, 10);

  },

//获得element的绝对高度

  getHeight: function(element) {

    element = $(element);

    return element.offsetHeight;

  },



  classNames: function(element) {

    return new Element.ClassNames(element);

  },



  hasClassName: function(element, className) {

    if (!(element = $(element))) return;

    return Element.classNames(element).include(className);

  },



  addClassName: function(element, className) {

    if (!(element = $(element))) return;

    return Element.classNames(element).add(className);

  },



  removeClassName: function(element, className) {

    if (!(element = $(element))) return;

    return Element.classNames(element).remove(className);

  },



  // removes whitespace-only text node children

  cleanWhitespace: function(element) {

    element = $(element);

    for (var i = 0; i < element.childNodes.length; i++) {

      var node = element.childNodes[i];

      if (node.nodeType == 3 && !/S/.test(node.nodeValue))

        Element.remove(node);

    }

  },



  empty: function(element) {

    return $(element).innerHTML.match(/^s*$/);

  },

//将浏览器的滚动条滚动到指定的结点的位置

  scrollTo: function(element) {

    element = $(element);

    var x = element.x ? element.x : element.offsetLeft,

        y = element.y ? element.y : element.offsetTop;

    window.scrollTo(x, y);

  },

//得到element的结点的绝对样式

  getStyle: function(element, style) {

    element = $(element);

    var value = element.style[style.camelize()];

    if (!value) {

      if (document.defaultView && document.defaultView.getComputedStyle) {

        var css = document.defaultView.getComputedStyle(element, null);

        value = css ? css.getPropertyValue(style) : null;

      } else if (element.currentStyle) {

        value = element.currentStyle[style.camelize()];

      }

    }



    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))

      if (Element.getStyle(element, 'position') == 'static') value = 'auto';



    return value == 'auto' ? null : value;

  },

//设置结点样式 var style={background-color:'black',color:'red'} Element.setStyle($('div1'),style)

  setStyle: function(element, style) {

    element = $(element);

    for (name in style)

      element.style[name.camelize()] = style[name];

  },

  

  //获得结点的宽度高度 该方法无论结点是否可见 都能获得其大小

  getDimensions: function(element) {

    element = $(element);

    if (Element.getStyle(element, 'display') != 'none')

      return {width: element.offsetWidth, height: element.offsetHeight};



    // All *Width and *Height properties give 0 on elements with display none,

    // so enable the element temporarily

    var els = element.style;

    var originalVisibility = els.visibility;

    var originalPosition = els.position;

    els.visibility = 'hidden';

    els.position = 'absolute';

    els.display = '';

    var originalWidth = element.clientWidth;

    var originalHeight = element.clientHeight;

    els.display = 'none';

    els.position = originalPosition;

    els.visibility = originalVisibility;

    return {width: originalWidth, height: originalHeight};

  },

//相对定位

  makePositioned: function(element) {

    element = $(element);

    var pos = Element.getStyle(element, 'position');

    if (pos == 'static' || !pos) {

      element._madePositioned = true;

      element.style.position = 'relative';

      // Opera returns the offset relative to the positioning context, when an

      // element is position relative but top and left have not been defined

      if (window.opera) {

        element.style.top = 0;

        element.style.left = 0;

      }

    }

  },

//取消相对定位

  undoPositioned: function(element) {

    element = $(element);

    if (element._madePositioned) {

      element._madePositioned = undefined;

      element.style.position =

        element.style.top =

        element.style.left =

        element.style.bottom =

        element.style.right = '';

    }

  },

//使结点隐藏超出的部分 overflow=relative

  makeClipping: function(element) {

    element = $(element);

    if (element._overflow) return;

    element._overflow = element.style.overflow;

    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')

      element.style.overflow = 'hidden';

  },



  undoClipping: function(element) {

    element = $(element);

    if (element._overflow) return;

    element.style.overflow = element._overflow;

    element._overflow = undefined;

  }

});





/*========================================================================================*/

//Toggle对象是Element.toggle方法的一个快捷用法

var Toggle = new Object();

Toggle.display = Element.toggle;



/*========================================================================================*/

//【Insertion命名空间】包含了4个类把指定的html片段插入到指定的结点的相应位置上

Abstract.Insertion = function(adjacency) {

  this.adjacency = adjacency;

}



Abstract.Insertion.prototype = {

  initialize: function(element, content) {

    this.element = $(element);

    this.content = content.stripScripts();



    if (this.adjacency && this.element.insertAdjacentHTML) {

      try {

        this.element.insertAdjacentHTML(this.adjacency, this.content);

      } catch (e) {

        if (this.element.tagName.toLowerCase() == 'tbody') {

          this.insertContent(this.contentFromAnonymousTable());

        } else {

          throw e;

        }

      }

    } else {

      this.range = this.element.ownerDocument.createRange();

      if (this.initializeRange) this.initializeRange();

      this.insertContent([this.range.createContextualFragment(this.content)]);

    }



    setTimeout(function() {content.evalScripts()}, 10);

  },



  contentFromAnonymousTable: function() {

    var div = document.createElement('div');

    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';

    return $A(div.childNodes[0].childNodes[0].childNodes);

  }

}



var Insertion = new Object();



Insertion.Before = Class.create();

Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {

  initializeRange: function() {

    this.range.setStartBefore(this.element);

  },



  insertContent: function(fragments) {

    fragments.each((function(fragment) {

      this.element.parentNode.insertBefore(fragment, this.element);

    }).bind(this));

  }

});



Insertion.Top = Class.create();

Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {

  initializeRange: function() {

    this.range.selectNodeContents(this.element);

    this.range.collapse(true);

  },



  insertContent: function(fragments) {

    fragments.reverse(false).each((function(fragment) {

      this.element.insertBefore(fragment, this.element.firstChild);

    }).bind(this));

  }

});



Insertion.Bottom = Class.create();

Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {

  initializeRange: function() {

    this.range.selectNodeContents(this.element);

    this.range.collapse(this.element);

  },



  insertContent: function(fragments) {

    fragments.each((function(fragment) {

      this.element.appendChild(fragment);

    }).bind(this));

  }

});



Insertion.After = Class.create();

Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {

  initializeRange: function() {

    this.range.setStartAfter(this.element);

  },



  insertContent: function(fragments) {

    fragments.each((function(fragment) {

      this.element.parentNode.insertBefore(fragment,

        this.element.nextSibling);

    }).bind(this));

  }

});



/*========================================================================================*/

//【Element.ClassNames】获得一个结点的所有的class名称组成的对象,实现了枚举接口



Element.ClassNames = Class.create();

Element.ClassNames.prototype = {

  initialize: function(element) {

    this.element = $(element);

  },



  _each: function(iterator) {

    this.element.className.split(/s+/).select(function(name) {

      return name.length > 0;

    })._each(iterator);

  },



  set: function(className) {

    this.element.className = className;

  },



  add: function(classNameToAdd) {

    if (this.include(classNameToAdd)) return;

    this.set(this.toArray().concat(classNameToAdd).join(' '));

  },



  remove: function(classNameToRemove) {

    if (!this.include(classNameToRemove)) return;

    this.set(this.select(function(className) {

      return className != classNameToRemove;

    }).join(' '));

  },



  toString: function() {//获得所有class的字符串表示空格隔开

    return this.toArray().join(' ');

  }

}

//表单域对象工具类

Object.extend(Element.ClassNames.prototype, Enumerable);

var Field = {

  clear: function() {

    for (var i = 0; i < arguments.length; i++)

      $(arguments[i]).value = '';

  },



  focus: function(element) {

    $(element).focus();

  },



  present: function() {

    for (var i = 0; i < arguments.length; i++)

      if ($(arguments[i]).value == '') return false;

    return true;

  },

//选中表单域

  select: function(element) {

    $(element).select();

  },

//focus + select

  activate: function(element) {

    element = $(element);

    element.focus();

    if (element.select)

      element.select();

  }

}



/*【Form】表单工具对象*/



var Form = {

//表单数据序列化

  serialize: function(form) {

    var elements = Form.getElements($(form));

    var queryComponents = new Array();



    for (var i = 0; i < elements.length; i++) {

      var queryComponent = Form.Element.serialize(elements[i]);

      if (queryComponent)

        queryComponents.push(queryComponent);

    }



    return queryComponents.join('&');

  },

//获取所有表单域按照标记名排列

  getElements: function(form) {

    form = $(form);

    var elements = new Array();



    for (tagName in Form.Element.Serializers) {

      var tagElements = form.getElementsByTagName(tagName);

      for (var j = 0; j < tagElements.length; j++)

        elements.push(tagElements[j]);

    }

    return elements;

  },

//有一个满足就返回

  getInputs: function(form, typeName, name) {

    form = $(form);

    var inputs = form.getElementsByTagName('input');



    if (!typeName && !name)

      return inputs;



    var matchingInputs = new Array();

    for (var i = 0; i < inputs.length; i++) {

      var input = inputs[i];

      if ((typeName && input.type != typeName) ||

          (name && input.name != name))

        continue;

      matchingInputs.push(input);

    }



    return matchingInputs;

  },

//整个表单域禁用

  disable: function(form) {

    var elements = Form.getElements(form);

    for (var i = 0; i < elements.length; i++) {

      var element = elements[i];

      element.blur();

      element.disabled = 'true';

    }

  },

//启用

  enable: function(form) {

    var elements = Form.getElements(form);

    for (var i = 0; i < elements.length; i++) {

      var element = elements[i];

      element.disabled = '';

    }

  },

//获取第一个可用的表单域

  findFirstElement: function(form) {

    return Form.getElements(form).find(function(element) {

      return element.type != 'hidden' && !element.disabled &&

        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());

    });

  },

//激活第一个表单域

  focusFirstElement: function(form) {

    Field.activate(Form.findFirstElement(form));

  },



  reset: function(form) {

    $(form).reset();

  }

}



//【Form.Element】 对特定的表单域进行操作

Form.Element = {

//序列化形成"&name=value"

  serialize: function(element) {

    element = $(element);

    var method = element.tagName.toLowerCase();

    var parameter = Form.Element.Serializers[method](element);



    if (parameter) {

      var key = encodeURIComponent(parameter[0]);

      if (key.length == 0) return;



      if (parameter[1].constructor != Array)

        parameter[1] = [parameter[1]];



      return parameter[1].map(function(value) {

        return key + '=' + encodeURIComponent(value);

      }).join('&');

    }

  },

//获得指定表单域的值

  getValue: function(element) {

    element = $(element);

    var method = element.tagName.toLowerCase();

    var parameter = Form.Element.Serializers[method](element);



    if (parameter)

      return parameter[1];

  }

}

//【Form.Element.Serializers】针对不同的表单域进行序列化

Form.Element.Serializers = {

//一般用所有的<input>标记的序列化都可以

  input: function(element) {

    switch (element.type.toLowerCase()) {

      case 'submit':

      case 'hidden':

      case 'password':

      case 'text':

        return Form.Element.Serializers.textarea(element);

      case 'checkbox':

      case 'radio':

        return Form.Element.Serializers.inputSelector(element);

    }

    return false;

  },



  inputSelector: function(element) {

    if (element.checked)

      return [element.name, element.value];

  },



  textarea: function(element) {

    return [element.name, element.value];

  },



  select: function(element) {

    return Form.Element.Serializers[element.type == 'select-one' ?

      'selectOne' : 'selectMany'](element);

  },



  selectOne: function(element) {

    var value = '', opt, index = element.selectedIndex;

    if (index >= 0) {

      opt = element.options[index];

      value = opt.value;

      if (!value && !('value' in opt))

        value = opt.text;

    }

    return [element.name, value];

  },



  selectMany: function(element) {

    var value = new Array();

    for (var i = 0; i < element.length; i++) {

      var opt = element.options[i];

      if (opt.selected) {

        var optValue = opt.value;

        if (!optValue && !('value' in opt))

          optValue = opt.text;

        value.push(optValue);

      }

    }

    return [element.name, value];

  }

}



//【获得表单域的值】

var $F = Form.Element.getValue;



/*========================================================================================*/

/*【Observer模式框架】 观察对象仅仅针对表单和表单域*/

/*========================================================================================*/



Abstract.TimedObserver = function() {}//定义了基于定时器的观察模式基类后面的类都继承了这个类

Abstract.TimedObserver.prototype = {

  initialize: function(element, frequency, callback) {

    this.frequency = frequency;//观察频率

    this.element   = $(element);//被观察的对象

    this.callback  = callback;//回调函数接受参数 被观察对象自身和触发事件时的值



    this.lastValue = this.getValue();//这是观察的值!!

    this.registerCallback();

  },

//注册回调函数

  registerCallback: function() {

    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);

  },

//每一个时间片触发一次该事件

  onTimerEvent: function() {

    var value = this.getValue();

    if (this.lastValue != value) {

      this.callback(this.element, value);

      this.lastValue = value;

    }

  }

}



//【Form.Element.Observer类】实现了getvalue方法

Form.Element.Observer = Class.create();

Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {

  getValue: function() {

    return Form.Element.getValue(this.element);

  }

});



//【Form.Observer类】实现了getvalue方法,只要有一个表单域发生变化就会触发回调

Form.Observer = Class.create();

Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {

  getValue: function() {

    return Form.serialize(this.element);

  }

});



//【Abstract.EventObserver 类】基于事件的Oberver模式的基类 后面的具体类都继承这个类



Abstract.EventObserver = function() {}

Abstract.EventObserver.prototype = {

  initialize: function(element, callback) {

    this.element  = $(element);

    this.callback = callback;



    this.lastValue = this.getValue();

    if (this.element.tagName.toLowerCase() == 'form')

      this.registerFormCallbacks();

    else

      this.registerCallback(this.element);

  },

//每次发生事件时被调用 内部调用 无需显示调用

  onElementEvent: function() {

    var value = this.getValue();

    if (this.lastValue != value) {

      this.callback(this.element, value);

      this.lastValue = value;

    }

  },

//注册表表单域事件

  registerFormCallbacks: function() {

    var elements = Form.getElements(this.element);

    for (var i = 0; i < elements.length; i++)

      this.registerCallback(elements[i]);

  },



  registerCallback: function(element) {

    if (element.type) {

      switch (element.type.toLowerCase()) {

        case 'checkbox':

        case 'radio':

          Event.observe(element, 'click', this.onElementEvent.bind(this));

          break;

        case 'password':

        case 'text':

        case 'textarea':

        case 'select-one':

        case 'select-multiple':

          Event.observe(element, 'change', this.onElementEvent.bind(this));

          break;

      }

    }

  }

}

//【Form.Element.EventObserver】

Form.Element.EventObserver = Class.create();

Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {

  getValue: function() {

    return Form.Element.getValue(this.element);

  }

});

//【Form.EventObserver】

Form.EventObserver = Class.create();

Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {

  getValue: function() {

    return Form.serialize(this.element);

  }

});







/*========================================================================================*/

/*【事件处理:对Event对象的扩展】对javascript事件处理机制进行封装*/

/*========================================================================================*/

if (!window.Event) {

  var Event = new Object();

}



Object.extend(Event, {

  KEY_BACKSPACE: 8,

  KEY_TAB:       9,

  KEY_RETURN:   13,

  KEY_ESC:      27,

  KEY_LEFT:     37,

  KEY_UP:       38,

  KEY_RIGHT:    39,

  KEY_DOWN:     40,

  KEY_DELETE:   46,

//获取触发事件的对象

  element: function(event) {

    return event.target || event.srcElement;

  },



  isLeftClick: function(event) {

    return (((event.which) && (event.which == 1)) ||

            ((event.button) && (event.button == 1)));

  },



  pointerX: function(event) {

    return event.pageX || (event.clientX +

      (document.documentElement.scrollLeft || document.body.scrollLeft));

  },



  pointerY: function(event) {

    return event.pageY || (event.clientY +

      (document.documentElement.scrollTop || document.body.scrollTop));

  },

//停止事件的传递

  stop: function(event) {

    if (event.preventDefault) {

      event.preventDefault();

      event.stopPropagation();

    } else {

      event.returnValue = false;

      event.cancelBubble = true;

    }

  },



  // find the first node with the given tagName, starting from the

  // node the event was triggered on; traverses the DOM upwards

  findElement: function(event, tagName) {

    var element = Event.element(event);

    while (element.parentNode && (!element.tagName ||

        (element.tagName.toUpperCase() != tagName.toUpperCase())))

      element = element.parentNode;

    return element;

  },



  observers: false,



  _observeAndCache: function(element, name, observer, useCapture) {

    if (!this.observers) this.observers = [];

    if (element.addEventListener) {

      this.observers.push([element, name, observer, useCapture]);

      element.addEventListener(name, observer, useCapture);

    } else if (element.attachEvent) {

      this.observers.push([element, name, observer, useCapture]);

      element.attachEvent('on' + name, observer);

    }

  },

//删除所有通过Event.observers绑定的事件

  unloadCache: function() {

    if (!Event.observers) return;

    for (var i = 0; i < Event.observers.length; i++) {

      Event.stopObserving.apply(this, Event.observers[i]);

      Event.observers[i][0] = null;

    }

    Event.observers = false;

  },

//绑定一个事件

  observe: function(element, name, observer, useCapture) {

    var element = $(element);

    useCapture = useCapture || false;



    if (name == 'keypress' &&

        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)

        || element.attachEvent))

      name = 'keydown';



    this._observeAndCache(element, name, observer, useCapture);

  },

//取消事件绑定

  stopObserving: function(element, name, observer, useCapture) {

    var element = $(element);

    useCapture = useCapture || false;



    if (name == 'keypress' &&

        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)

        || element.detachEvent))

      name = 'keydown';



    if (element.removeEventListener) {

      element.removeEventListener(name, observer, useCapture);

    } else if (element.detachEvent) {

      element.detachEvent('on' + name, observer);

    }

  }

});



//【结点的位置处理:Position对象】

/* prevent memory leaks in IE */

Event.observe(window, 'unload', Event.unloadCache, false);

var Position = {

  // set to true if needed, warning: firefox performance problems

  // NOT neeeded for page scrolling, only if draggable contained in

  // scrollable elements

  includeScrollOffsets: false,



  // must be called before calling withinIncludingScrolloffset, every time the

  // page is scrolled用于计算滚动条的位置信息

  prepare: function() {

    this.deltaX =  window.pageXOffset

                || document.documentElement.scrollLeft

                || document.body.scrollLeft

                || 0;

    this.deltaY =  window.pageYOffset

                || document.documentElement.scrollTop

                || document.body.scrollTop

                || 0;

  },

//计算节点的绝对滚动位置返回包含两个元素的数组到文档左侧 顶部的距离

  realOffset: function(element) {

    var valueT = 0, valueL = 0;

    do {

      valueT += element.scrollTop  || 0;

      valueL += element.scrollLeft || 0;

      element = element.parentNode;

    } while (element);

    return [valueL, valueT];

  },

//计算结点相对于文档的绝对滚动位置,返回包含两个元素的数组到文档左侧 顶部的距离

  cumulativeOffset: function(element) {

    var valueT = 0, valueL = 0;

    do {

      valueT += element.offsetTop  || 0;

      valueL += element.offsetLeft || 0;

      element = element.offsetParent;

    } while (element);

    return [valueL, valueT];

  },

//相对位置

  positionedOffset: function(element) {

    var valueT = 0, valueL = 0;

    do {

      valueT += element.offsetTop  || 0;

      valueL += element.offsetLeft || 0;

      element = element.offsetParent;

      if (element) {

        p = Element.getStyle(element, 'position');

        if (p == 'relative' || p == 'absolute') break;

      }

    } while (element);

    return [valueL, valueT];

  },



  offsetParent: function(element) {

    if (element.offsetParent) return element.offsetParent;

    if (element == document.body) return element;



    while ((element = element.parentNode) && element != document.body)

      if (Element.getStyle(element, 'position') != 'static')

        return element;



    return document.body;

  },



  // caches x/y coordinate pair to use with overlap是否在element内

  within: function(element, x, y) {

    if (this.includeScrollOffsets)

      return this.withinIncludingScrolloffsets(element, x, y);

    this.xcomp = x;

    this.ycomp = y;

    this.offset = this.cumulativeOffset(element);



    return (y >= this.offset[1] &&

            y <  this.offset[1] + element.offsetHeight &&

            x >= this.offset[0] &&

            x <  this.offset[0] + element.offsetWidth);

  },



  withinIncludingScrolloffsets: function(element, x, y) {

    var offsetcache = this.realOffset(element);



    this.xcomp = x + offsetcache[0] - this.deltaX;

    this.ycomp = y + offsetcache[1] - this.deltaY;

    this.offset = this.cumulativeOffset(element);



    return (this.ycomp >= this.offset[1] &&

            this.ycomp <  this.offset[1] + element.offsetHeight &&

            this.xcomp >= this.offset[0] &&

            this.xcomp <  this.offset[0] + element.offsetWidth);

  },



  // within must be called directly before

  overlap: function(mode, element) {

    if (!mode) return 0;

    if (mode == 'vertical')

      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /

        element.offsetHeight;

    if (mode == 'horizontal')

      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /

        element.offsetWidth;

  },

//克隆结点 常用于拖放

  clone: function(source, target) {

    source = $(source);

    target = $(target);

    target.style.position = 'absolute';

    var offsets = this.cumulativeOffset(source);

    target.style.top    = offsets[1] + 'px';

    target.style.left   = offsets[0] + 'px';

    target.style.width  = source.offsetWidth + 'px';

    target.style.height = source.offsetHeight + 'px';

  },



  page: function(forElement) {

    var valueT = 0, valueL = 0;



    var element = forElement;

    do {

      valueT += element.offsetTop  || 0;

      valueL += element.offsetLeft || 0;



      // Safari fix

      if (element.offsetParent==document.body)

        if (Element.getStyle(element,'position')=='absolute') break;



    } while (element = element.offsetParent);



    element = forElement;

    do {

      valueT -= element.scrollTop  || 0;

      valueL -= element.scrollLeft || 0;

    } while (element = element.parentNode);



    return [valueL, valueT];

  },



  clone: function(source, target) {

    var options = Object.extend({

      setLeft:    true,

      setTop:     true,

      setWidth:   true,

      setHeight:  true,

      offsetTop:  0,

      offsetLeft: 0

    }, arguments[2] || {})



    // find page position of source

    source = $(source);

    var p = Position.page(source);



    // find coordinate system to use

    target = $(target);

    var delta = [0, 0];

    var parent = null;

    // delta [0,0] will do fine with position: fixed elements,

    // position:absolute needs offsetParent deltas

    if (Element.getStyle(target,'position') == 'absolute') {

      parent = Position.offsetParent(target);

      delta = Position.page(parent);

    }



    // correct by body offsets (fixes Safari)

    if (parent == document.body) {

      delta[0] -= document.body.offsetLeft;

      delta[1] -= document.body.offsetTop;

    }



    // set position

    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';

    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';

    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';

    if(options.setHeight) target.style.height = source.offsetHeight + 'px';

  },

//绝对定位

  absolutize: function(element) {

    element = $(element);

    if (element.style.position == 'absolute') return;

    Position.prepare();



    var offsets = Position.positionedOffset(element);

    var top     = offsets[1];

    var left    = offsets[0];

    var width   = element.clientWidth;

    var height  = element.clientHeight;



    element._originalLeft   = left - parseFloat(element.style.left  || 0);

    element._originalTop    = top  - parseFloat(element.style.top || 0);

    element._originalWidth  = element.style.width;

    element._originalHeight = element.style.height;



    element.style.position = 'absolute';

    element.style.top    = top + 'px';;

    element.style.left   = left + 'px';;

    element.style.width  = width + 'px';;

    element.style.height = height + 'px';;

  },

//相对定位

  relativize: function(element) {

    element = $(element);

    if (element.style.position == 'relative') return;

    Position.prepare();



    element.style.position = 'relative';

    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);

    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);



    element.style.top    = top + 'px';

    element.style.left   = left + 'px';

    element.style.height = element._originalHeight;

    element.style.width  = element._originalWidth;

  }

}



// Safari returns margins on body which is incorrect if the child is absolutely

// positioned.  For performance reasons, redefine Position.cumulativeOffset for

// KHTML/WebKit only.

if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {

  Position.cumulativeOffset = function(element) {

    var valueT = 0, valueL = 0;

    do {

      valueT += element.offsetTop  || 0;

      valueL += element.offsetLeft || 0;

      if (element.offsetParent == document.body)

        if (Element.getStyle(element, 'position') == 'absolute') break;



      element = element.offsetParent;

    } while (element);



    return [valueL, valueT];

  }

}



//--------->完成

[转载]JavaScript中的prototype - 轩脉刃 - 博客园

mikel阅读(988)

[转载]JS中的prototype – 轩脉刃 – 博客园.

JS中的prototype是JS中比较难理解的一个部分

 

本文基于下面几个知识点:

 

1 原型法设计模式

在.Net中可以使用clone()来实现原型法

原型法的主要思想是,现在有1个类A,我想要创建一个类B,这个类是以A为原型的,并且能进行扩展。我们称B的原型为A。

 

2 JavaScript的方法可以分为三类:

a 类方法

b 对象方法

c 原型方法

例子:

function People(name)
{
this.name=name;
//对象方法
this.Introduce=function(){
alert("My name is "+this.name);
}
}
//类方法
People.Run=function(){
alert("I can run");
}
//原型方法
People.prototype.IntroduceChinese=function(){
alert("我的名字是"+this.name);
}

//测试

var p1=new People("Windking");

p1.Introduce();

People.Run();

p1.IntroduceChinese();

3 obj1.func.call(obj)方法

意思是将obj看成obj1,调用func方法

 

 

好了,下面一个一个问题解决:

 

prototype是什么含义?

 

JavaScript中的每个对象都有prototype属性,Javascript中对象的prototype属性的解释是:返回对象类型原型的引用。

A.prototype = new B();

理解prototype不应把它和继承混淆。A的prototype为B的一个实例,可以理解A将B中的方法和属性全部克隆了一遍。A能使用B的方 法和属性。这里强调的是克隆而不是继承。可以出现这种情况:A的prototype是B的实例,同时B的prototype也是A的实例。

 

先看一个实验的例子:

function baseClass()
{
this.showMsg = function()
{
alert("baseClass::showMsg");
}
}

function extendClass()
{
}

extendClass.prototype = new baseClass();
var instance = new extendClass();
instance.showMsg(); // 显示baseClass::showMsg

我们首先定义了baseClass类,然后我们要定义extentClass,但是我们打算以baseClass的一个实例为原型,来克隆的extendClass也同时包含showMsg这个对象方法。

extendClass.prototype = new baseClass()就可以阅读为:extendClass是以baseClass的一个实例为原型克隆创建的。

那么就会有一个问题,如果extendClass中本身包含有一个与baseClass的方法同名的方法会怎么样?

下面是扩展实验2:

function baseClass()
{
this.showMsg = function()
{
alert("baseClass::showMsg");
}
}

function extendClass()
{
this.showMsg =function ()
{
alert("extendClass::showMsg");
}
}

extendClass.prototype = new baseClass();
var instance = new extendClass();

instance.showMsg();//显示extendClass::showMsg

实验证明:函数运行时会先去本体的函数中去找,如果找到则运行,找不到则去prototype中寻找函数。或者可以理解为prototype不会克隆同名函数。

那么又会有一个新的问题:

如果我想使用extendClass的一个实例instance调用baseClass的对象方法showMsg怎么办?

答案是可以使用call:

extendClass.prototype = new baseClass();
var instance = new extendClass();

var baseinstance = new baseClass();
baseinstance.showMsg.call(instance);//显示baseClass::showMsg

这里的baseinstance.showMsg.call(instance);阅读为“将instance当做baseinstance来调用,调用它的对象方法showMsg”

好了,这里可能有人会问,为什么不用baseClass.showMsg.call(instance);

这就是对象方法和类方法的区别,我们想调用的是baseClass的对象方法

最后,下面这个代码如果理解清晰,那么这篇文章说的就已经理解了:

<script type="text/javascript">// <![CDATA[
function baseClass()
{
    this.showMsg = function()
    {
        alert("baseClass::showMsg");   
    }
   
    this.baseShowMsg = function()
    {
        alert("baseClass::baseShowMsg");
    }
}
baseClass.showMsg = function()
{
    alert("baseClass::showMsg static");
}

function extendClass()
{
    this.showMsg =function ()
    {
        alert("extendClass::showMsg");
    }
}
extendClass.showMsg = function()
{
    alert("extendClass::showMsg static")
}

extendClass.prototype = new baseClass();
var instance = new extendClass();

instance.showMsg(); //显示extendClass::showMsg
instance.baseShowMsg(); //显示baseClass::baseShowMsg
instance.showMsg(); //显示extendClass::showMsg

baseClass.showMsg.call(instance);//显示baseClass::showMsg static

var baseinstance = new baseClass();
baseinstance.showMsg.call(instance);//显示baseClass::showMsg
// ]]></script>

 

作者:轩脉刃(yjf512)
出处:(http://www.cnblogs.com/yjf512/
版权声明:本文的版权归作者与博客园共有。欢迎转载阅读,转载时须注明本文的详细链接。

 

[参考文章]

http://tech.ddvip.com/2009-05/1243588303121461.html

http://jetway.iteye.com/blog/56533

http://xiaofeizm55333.iteye.com/blog/80913

[转载]javascript in-between原理与应用_前端开发

mikel阅读(1273)

[转载]javascript in-between原理与应用_前端开发.

简介

本文将介绍in-between的概念,以及in-between类库Tween.js的实现。接着,我将介绍一些常见的in-between的好玩的用法。最后,我还将介绍JQuery Effects对in-between的应用。

目录

  • 什么是tween
  • Tween.js
  • 有趣的应用
  • JQuery中的Tween

什么是tween

tween是in-between的另一种写法。一个tween指的是让对象的属性值平滑变化的一个过程

那么,什么是平滑变化?假设在9点10分的时候,对象foo.a的值为0。在9点20分的时候,我希望它的值变成1。如果foo.a是非平滑变化的,在9点10分到9点20分(除9点20分外)之间它依然是0。如果它是平滑变化的,那么它应该在9点10分到9点20分之间的每一个时间点上的值都不同,并且是根据一定函数规律变化的。

tween就是这个平滑变化的过程。

javascript in-between原理与应用

这就好比一个人溜冰一样。你要从点a滑到点b,你是不可能一开始一直呆在a点,直到最后通过超时空转换直接把自己变到b点的。要从a点滑到b点,你必须经过一个路径,从而平滑地从a点滑到b点。

Tween.js

Tween.js是用来在JavaScript当中实现tween的一个工具库。我们接下来讲解它的实现。在实际应用中,我们一般自己编写自己的Tween类,或者复制并修改开源工具库中的Tween类,因为自己编写的总是最符合自己业务需求的。大部分Tween工具库包含了很多你用不到的东西,在后面我会提到。

为了使用Tween.js,你需要先有一个待变化的对象。在下面的例子里,我们将对象foo初始化为{a: 1}(初始状态),并要求它在3000毫秒后变成{a: 4}(目标状态)。变化过程采用线性变化,即每个时间点的变化速率相等。

var foo = {a: 1}, /*初始状态*/
  tween = new TWEEN.Tween(foo)
    .to({a: 4} /*目标状态*/, 3000 /*变化时间*/)
    .start();

  (function animate() {
    if ( foo.a < 4 ) {
      requestAnimationFrame(animate);
    }
    TWEEN.update();
    console.log(foo);
  })();

如果你查看Chrome Inspector(或者Firefox下的Firebug插件),你将会看到控制台中输出了下面的数据

Object {a: 1.0001740000443533} 
Object {a: 1.0924470000900328} 
Object {a: 1.1527340000029653} 
Object {a: 1.1701550000580028} 
Object {a: 1.185736000072211}
... ...

喘口气

回过头来,我们来稍微解释一下上面的代码段。首先我们创建一个foo对象的tween

var foo = {a: 1}, /*初始状态*/
  tween = new TWEEN.Tween(foo);

接下来,我们需要将确认foo对象的目标状态,在这里是{a: 4},并且要求它正好在3000毫秒后到达目标状态。

tween.to({a: 4} /*目标状态*/, 3000 /*变化时间*/);

最后,我们需要激活这个tween,代表开始变化。调用tween.start()的时间就是开始变化的时间时间戳,除非你调用了tween.delay()方法。你还可以给tween.start(time)传入一个额外参数time,直接指定开始变化的时间戳。我们可以通过源码验证这点

_startTime = time !== undefined ? time : ( typeof window !== 'undefined' && window.performance !== undefined && window.performance.now !== undefined ? window.performance.now() : Date.now() );
_startTime += _delayTime;

值得注意的是,在没有delay和指定time参数的情况下,Tween.js将优先使用window.performance.now()获取当前的时间戳,这样的时间戳是高精度时间戳(精度为10μs)。这是HTML5当中的新增的DOMHighResTimeStamp API。
询问进度

我们通过animate函数来轮询foo对象目前的状态。采用requestAnimationFrame进行异步调用,效率会更高。你也可以选择用setTimeout,jQuery就是这样做的。

在询问的时候,你首先需要调用TWEEN.update()更新所有的tween。

(function animate() {
  if ( foo.a < 4 ) {
    requestAnimationFrame(animate);
  }
  TWEEN.update();
  console.log(foo);
})();

精髓

使用in-between的精髓就在于,它将属性的变化和询问分离。如果你熟悉MVC,属性的变化就好像是MVC里面的Model,而询问就好像是Controller,最后我们输出到控制台(Console)就好像是View。

“历史总是惊人地相似”

分离带来的好处是什么呢?它使得我们可以统一管理所有页面上的Tween,而不用关心它们究竟用于什么途径。接下来,我们通过实践来证明这一点。
有趣的应用

首先你需要先将Tween.js的GitHub代码仓库复制到本地

git clone git@github.com:sole/tween.js.git
cd tween.js

在examples目录里面有许多有趣的应用,我们只看其中第二个例子01_bars.html。在这个例子中,有1000个彩条在屏幕上水平移动。每个彩条都对应两个tween,一个是从出发位置到目标位置的,一个是返回出发位置的。

var tween = new TWEEN.Tween(elem)
  .to({ x: endValue }, 4000)
  .delay(Math.random() * 1000)
  .onUpdate(updateCallback)
  .easing(TWEEN.Easing.Back.Out)
  .start();

var tweenBack = new TWEEN.Tween(elem, false)
  .to({ x: startValue}, 4000)
  .delay(Math.random() * 1000)
  .onUpdate(updateCallback)
  .easing(TWEEN.Easing.Elastic.InOut);

tween.chain(tweenBack);
tweenBack.chain(tween);

Tween.js支持事件onUpdate,每当TWEEN.update()被调用的时候,会触发所有tween的update事件。另外,你还能为每个tween设置easing function。如果你不清楚什么是easing function,可以看我昨天写的文章《JavaScript动画实现初探》。

由于Tween.js和其他支持in-between的类库都含有大量预置的easing function,其中有很多我们用不到的。所以,就像本文前面提到的一样,我们经常需要定制自己的Tween类库。

这里还用到了chaining来循环动画,tween结束后将启动tweenBack,tweenBack启动后会再次启动tween。
jQuery中的Tween

jQuery中也采用了Tween来管理动画的效果进度。在jQuery 1.8之后,引入了Tween来管理动画效果进度,原先的jQuery.fx和Tween.prototype.init是相同的。之所以保留jQuery.fx,是为了兼容以前的插件。

jQuery.fx = Tween.prototype.init;

对于动画中需要变化的每一个属性,jQuery都为其创建一个Tween。

jQuery.map( props, createTween, animation );

function createTween( value, prop, animation ) {
  var tween,
    collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
    index = 0,
    length = collection.length;
  for ( ; index < length; index++ ) {
    if ( (tween = collection[ index ].call( animation, prop, value )) ) {
      // we're done with this property
      return tween;
    }
  }
}

每隔一段时间,jQuery要求每隔DOM节点的tween根据当前进度更新style。

for ( ; index < length ; index++ ) {
  animation.tweens[ index ].run( percent /*当前动画的时间进度*/);
}

jQuery当中并没有用requestAnimationFrame一直去询问,而是采用setTimeout每隔13ms去询问,然后更新界面。13ms是一个平衡点,不会太长,也不会太短。

jQuery.fx.start = function() {
  if ( !timerId ) {
    timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
  }
};

总结

本文介绍了in-between,并介绍了它的原理以及一些应用。in-between主要用在页面效果动画,数据可视化当中。你可以让它和一些著名的数据可视化库(如d3.js)协同工作。你可以查看Tween.js的examples,学到更多相关的应用。
讨论

欢迎到我们的团队博客进行讨论,我在团队博客里面的讨论时间较多。