当前位置: 首页 > 编程日记 > 正文

jQuery-1.9.1源码分析系列(四) 缓存系统

先前在分析Sizzle的时候分析到Sizzle有自己的缓存机制,点击这里查看。不过Sizzle的缓存只是对内使用的(内部自己存,自己取)。接下来分析jQuery可以对外使用的缓存(可存可取)。

首先需要明白jQuery缓存需要解决什么问题,实现它的意义?

jQuery缓存要解决的是在往DOM节点添加数据(这些数据往往和该DOM节点紧密相关),但是给DOM添加数据或自定义属性可能起内存泄漏(DOM发生缓存泄漏导致DOM的数据没法被删除,那么添加到DOM的数据也无法被回收。久而久之,会出现一大片内存得不到释放。),所以应该要尽量避免这样做。更好的解决方法是使用一种低耦合的方式让DOM和缓存数据能够联系起来。

  jQuery怎么做?

  jQuery定义了一个属性cache = {}来保存所有的缓存数据。在DOM节点上添加一个expando的值(expando的值等于”jQuery”+当前时间)为属性名称的属性,这个属性的值id = dom[jQuery.expando]用来查找jQuery.cache上对应的缓存数据:jQuery.cache[id].data 即为DOM对应的缓存数据。为了保证了id 的全局唯一性,这个id使用jQuery.guid自增。例如

$("#demo").data("name","chua");
$("#demo").data("name");//"chua"

  jQuery底层接口还做了拓展,不仅仅能缓存在DOM节点的数据,还可以缓存非DOM节点的对象的数据。这种方式最终数据是附加到了对象obj自己身上,而没有使用全局缓存jQuery.cache。该方式也会在对象obj上添加一个expando的值为属性名称的属性,缓存数据保存在obj[jQuery.expando].data上。不过这个一般不推荐外部使用,因为调用的是更底层的api: jQuery.data,而非$(...).data。例子:

var obj = {};
$.data(obj,'name','chua');
$.data(obj,'name');//"chua"

接下来我们开始解析源码。

jQuery.fn.extend({data: function( key, value ) {…    },//内部使用基础api:jQuery.data来实现,可以使用参数(key,value),也可以使用参数(obj)removeData: function( key ) {…}//内部使用基础api: jQuery.removeData来实现
});

所以我们主要看底层基础API部分

jQuery.extend({cache: {},// 每个jQuery拷贝都有一个其唯一的标志。比如你的页面有两个iframe且每个iframe都用到的jQuery。name你的两个iframe就有两份jQuery拷贝。expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ),// 下面的元素将抛出不可捕获的异常,如果你尝试给他们添加expando属性//主要用在acceptData函数中确定元素是否可以添加expando属性
    noData: {"embed": true,// Ban all objects except for Flash (which handle expandos)"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000","applet": true},hasData: function( elem ) {…},data: function( elem, name, data ) {return internalData( elem, name, data );},removeData: function( elem, name ) {return internalRemoveData( elem, name );},// 内部使用_data: function( elem, name, data ) {return internalData( elem, name, data, true );},_removeData: function( elem, name ) {return internalRemoveData( elem, name, true );},// 用来确定DOM节点是否能够添加expando 数据acceptData: function( elem ) {// non-element不能添加数据if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) {return false;}var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ];// nodes accept data unless otherwise specified; rejection can be conditionalreturn !noData || noData !== true && elem.getAttribute("classid") === noData;}
});

所以归根结底缓存数据和取出缓存数据是使用内部函数internalData( elem, name, data, pvt /* Internal Use Only */ )

a. 存取缓存的内部函数function internalData( elem, name, data, pvt /* Internal Use Only */ )


internalData的处理流程为:

1.判断如果元素不支持添加属性的直接返回。取出缓存容器和缓存中使用的ID备用。这里面粉两种情况,如果传递的对象elem是DOM对象,则缓存取全局缓存容器jQuery.cache,id取elem[jQuery.expando](如果没有的话使用jQuery.guid自增值设置一个);如果elem不是DOM对象,则缓存容器取对象本身elem,id取jQuery.expando

var thisCache, ret,//获取每份jquery拷贝的标志internalKey = jQuery.expando,getByName = typeof name === "string",//我们将DOM节点和JS对象区分开来,因为IE6-7不能跨越DOM-JS界限正确回收对象引用//所有DOM节点上没有附加数据,而是存放再来全局jQuery缓存jQuery.cache中isNode = elem.nodeType,//只有DOM节点需要全局jQuery缓存;js对象数据直接附加到对象上使得垃圾回收机制能够自动回收cache = isNode ? jQuery.cache : elem,//假如JS对象的缓存已经存在,则拷贝标志作为一个访问ID。//如果是DOM对象,则拷贝标志作为一个属性附加到dom上,这个属性的值作为访问全局缓存的一个路径IDid = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;//预先处理要获取数据,但是缓存中没有数据的情况。这种情况应当直接返回if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) {return;}if ( !id ) {//只有dom节点的每个元素都需要唯一的ID,直到他们的数据在全局缓存中清除if ( isNode ) {//启用删除的id中最后一个id,或是对象的全局GUID统计elem[ internalKey ] = id = core_deletedIds.pop() || jQuery.guid++;} else {id = internalKey;}}
View Code

2.取出的缓存容器cache[ id ]如果不存在就初始化为空对象

        //如果缓存中没有数据if ( !cache[ id ] ) {//初始化对象cache[ id ] = {};//避免在对象使用JSON.stringify序列化的时候暴露jQuery的元数据给普通对象if ( !isNode ) {cache[ id ].toJSON = jQuery.noop;}}
View Code

3.cache[ id ]并非真正的缓存数据,真正的缓存数据保存在cache[ id ].data上。在初始化之前需要对特殊情况(要缓存的数据是对象,且没指定缓存名称的时候,意味着要替换掉原来的整个缓存。比如:$.data(elem,{"name": "chua"})。)处理。如果cache[ id ].data不存在则初始化他。

    //使用对象的话用来替代key/value这种成对的方式。
//比如$.data(document,{name:'chenhua'})//添加后,直接通过$.data(document,'name')就可以获取if ( typeof name === "object" || typeof name === "function" ) {//内部使用的时候if ( pvt ) {//内部使用直接添加到cache[ id ]上cache[ id ] = jQuery.extend( cache[ id ], name );} else {//添加缓存数据到cache[ id ].data上cache[ id ].data = jQuery.extend( cache[ id ].data, name );}}thisCache = cache[ id ];//非内部使用if ( !pvt ) {//如果.data没有初始化则先初始化if ( !thisCache.data ) {thisCache.data = {};}thisCache = thisCache.data;}
View Code

4.如果是存数据,则存之,返回整个缓存;取数据则取之,返回取得的数据

    //添加缓存数据if ( data !== undefined ) {thisCache[ jQuery.camelCase( name ) ] = data;}//如果name是字符串,则返回对应的数据,否则返回整个缓存if ( getByName ) {//通过本身neme或不同浏览器的驼峰写法获取缓存ret = thisCache[ name ];if ( ret == null ) {ret = thisCache[ jQuery.camelCase( name ) ];}} else {ret = thisCache;}return ret;
View Code

ok。流程就到这里。

解析完jQuery.data的流程以后我们来做一组实验,区分高级api:$(...).data和底层api:jQuery.data的区别。在jQuery的官方文档中,提示用户jQuery.data是一个低级的方法,应该用$(...).data()方法来代替。$.data( element, key, value )可以对DOM元素附加任何类型的数据,但应避免循环引用而导致的内存。

var t1=$(document);
var t2=$(document);//=======第一组$(…).data()方法
t1.data('age',0);
t2.data('age',1);
t1.data('age')  //1
t2.data('age')  //1//=======第二组$.data()方法
$.data(t1,"name","chua")
$.data(t2,"name","yling")
$.data(t1,"name")   //chua
$.data(t2,"name")   //yling

可以看出其中的不同吧。使用$(...).data最终传递给internalData的第一个参数elem是节点对象document,所以使用全局缓存jQuery.cache保存。而使用$.data方式最终传递给internalData的第一个参数elem是jQuery对象[document],这样的属性结果是保存在对象自己身上,而t1和t2是不同的对象,分别取自己对象上的缓存,结果当然不同了。

var t1=$(document);t1.data('age',0);的缓存效果

$.data(t1,"name","chua")的缓存效果

b. 删除缓存


使用$(...).data(key,value)方式保存的缓存直接使用$(...).removeData(key)来删除缓存。当然也可以使用低级方法$.data(elem,key,value)来缓存数据,使用$.removeData(elem,key)来删除缓存,但是建议不要使用低级方法。删除缓存最终会调用内部函数internalRemoveData( elem, name, pvt/*内部使用,默认为false*/ )来处理。我们跟踪一下处理流程

1.判断如果元素不支持添加属性的直接返回。取出缓存容器和相应的id。如果缓存容器不存在则直接返回

        //元素不支持添加属性的直接返回if ( !jQuery.acceptData( elem ) ) {return;}var i, l, thisCache,isNode = elem.nodeType,//详细信息查看jQuery.data源码cache = isNode ? jQuery.cache : elem,id = isNode ? elem[ jQuery.expando ] : jQuery.expando;//缓存中没有数据直接返回if ( !cache[ id ] ) {return;}
View Code

2.如果有需要删除的缓存名称(参数name,可以是字符串【如果要删除多个缓存名指定的缓存可以使用空格隔开】,也可以是数组),则根据名称删除元素。删除后如果缓存不是空对象则返回。

        if ( name ) {//pvt内部使用,默认为false(undefined)thisCache = pvt ? cache[ id ] : cache[ id ].data;//缓存对象if ( thisCache ) {//name为字串if ( !jQuery.isArray( name ) ) {// name就是一个keyif ( name in thisCache ) {name = [ name ];} else {//处理ie兼容,如果name是空格相连的字符串,使用split分割name = jQuery.camelCase( name );if ( name in thisCache ) {name = [ name ];} else {name = name.split(" ");}}//name为数组} else {//如果name是key的数组// 当数据初始化完成,通过("key", "val")签名,// keys将转化成骆驼写法.// 如果没有办法告知_how_这种key被添加,移除原始类型的key和骆驼写法的keyname = name.concat( jQuery.map( name, jQuery.camelCase ) );}for ( i = 0, l = name.length; i < l; i++ ) {delete thisCache[ name[i] ];}//如果缓存不为空则返回,如果为空则我们在后面通过delete删除该对象if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {return;}}}
View Code

3.如果没有传递要删除的缓存名,表示要删除全部缓存。删除缓存后如果缓存容器不为空对象则返回

        //详细信息查看jQuery.data源码if ( !pvt ) {delete cache[ id ].data;//cache不为空则返回if ( !isEmptyDataObject( cache[ id ] ) ) {return;}}
View Code

4.走到最后一步,表示缓存容器内的缓存都被清空了,删除缓存容器

        //走到这一步表示cache中没有数据了,销毁cacheif ( isNode ) {jQuery.cleanData( [ elem ], true );// 当支持删除expandos 或'cache'不是window (#10080)} else if ( jQuery.support.deleteExpando || cache != cache.window ) {delete cache[ id ];// 当所有都失败时,处理为null} else {cache[ id ] = null;}
View Code

附上完整源码

function internalRemoveData( elem, name, pvt ) {//元素不支持添加属性的直接返回if ( !jQuery.acceptData( elem ) ) {return;}var i, l, thisCache,isNode = elem.nodeType,//详细信息查看jQuery.data源码cache = isNode ? jQuery.cache : elem,id = isNode ? elem[ jQuery.expando ] : jQuery.expando;//缓存中没有数据直接返回if ( !cache[ id ] ) {return;}if ( name ) {//pvt内部使用,默认为false(undefined)thisCache = pvt ? cache[ id ] : cache[ id ].data;//缓存对象if ( thisCache ) {//name为字串if ( !jQuery.isArray( name ) ) {// name就是一个keyif ( name in thisCache ) {name = [ name ];} else {//处理ie兼容,如果name是空格相连的字符串,使用split分割name = jQuery.camelCase( name );if ( name in thisCache ) {name = [ name ];} else {name = name.split(" ");}}//name为数组} else {//如果name是key的数组// 当数据初始化完成,通过("key", "val")签名,// keys将转化成骆驼写法.// 如果没有办法告知_how_这种key被添加,移除原始类型的key和骆驼写法的keyname = name.concat( jQuery.map( name, jQuery.camelCase ) );}for ( i = 0, l = name.length; i < l; i++ ) {delete thisCache[ name[i] ];}//如果缓存不为空则返回,如果为空则我们在后面通过delete删除该对象if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {return;}}}//详细信息查看jQuery.data源码if ( !pvt ) {delete cache[ id ].data;//cache不为空则返回if ( !isEmptyDataObject( cache[ id ] ) ) {return;}}//走到这一步表示cache中没有数据了,销毁cacheif ( isNode ) {jQuery.cleanData( [ elem ], true );// 当支持删除expandos 或'cache'不是window (#10080)} else if ( jQuery.support.deleteExpando || cache != cache.window ) {delete cache[ id ];// 当所有都失败时,处理为null} else {cache[ id ] = null;}}

如果觉得本文不错,请点击右下方【推荐】!

转载于:https://www.cnblogs.com/chuaWeb/p/jQuery-1-9-1-cache.html

相关文章:

CBA 赛程的笔记 - 北京首钢

2014-11-01 19:35北京首钢103:89广东宏远结束技术统计 发挥不错&#xff0c;打的比较好&#xff01;2014-11-05 19:35八一双鹿89:100北京首钢结束技术统计 第一节国内球员打的太屎&#xff0c;最后一节国内球员发挥不错&#xff01;2014-11-07 19:35浙江稠州107:116北京首钢结束…

个人技术生涯的感悟(2)

很多时候&#xff0c;一门门槛很低的编程语言对于初学者的帮助是非常大的&#xff0c;从而决定这个人的技术路线的选择。 在经历过大一下学期对C&#xff0c;和C的迷茫之后&#xff0c;在大二上学期&#xff0c;学校在搞一个网页设计比赛&#xff0c;正好和两个朋友一起参赛。开…

HtmlAgilityPack 处理通配的contains

//选择不包含class属性的节点 var result node.SelectNodes(".//span[not(class)]"); //选择不包含class和id属性的节点 var result node.SelectNodes(".//span[not(class) and not(id)]"); //选择不包含class"expire"的span var result node…

游戏风格化角色创建入门指南视频教程

游戏风格化角色创建入门指南视频教程 时间 33小时 包括项目文件 1920X1080 MP4 语言&#xff1a;英语中文字幕&#xff08;根据原英文字幕机译更准确&#xff09;原英文字幕 游戏中的风格化角色创建入门指南 百度一下 云桥网络 平台huo取 教程&#xff01; 信息。 教程33小…

Java学习总结:6

String类(接上篇) 字符串的拆分 public class Test1_1_2_1 {public static void main(String args[]){String str "hello world";String result[] str.split(" "); //以空格作为间隔for(int x0;x<result.length;x){System.out.print(result[x]"…

java面试常见问题之Hibernate总结

1 Hibernate的检索方式 导航对象图检索&#xff08;根据已经加载的对象&#xff0c;导航到其他对象。&#xff09; OID检索&#xff08;按照对象的OID来检索对象。&#xff09; HQL检索&#xff08;使用面向对象的HQL查询语言。&#xff09; QBC检索&#xff08;使…

HDU 4267 线段树 离散点区间更新, 自叶子节点至根单点查询

题意&#xff1a; n个数字 下面n个数字表示数列 2个操作 1 [u, v] k add [u,v ]区间 &#xff08;u点要计算&#xff09;每隔k个位置&#xff0c;该数字add 2 pos 询问 pos下标的值&#xff08;下标从1开始&#xff09; 思路&#xff1a; 因为k很小&#xff0c; 可以直接存 k…

Java学习总结:7

static关键字 一个类的主要组成就是属性和方法(分为构造方法和普通方法两种)&#xff0c;而每一个对象都分别拥有各自的属性内容(不同对象的属性保存在不同的堆内存中)&#xff0c;如果类中的某个属性希望定义为公共属性(即所有对象都可以使用的属性)&#xff0c;则可以在声明…

mybatis 使用resultMap实现数据库的操作

resultType:直接表示返回类型 resultMap&#xff1a;对外部resultMap的引用 二者不能同时使用 创建一个实体类Role和User public class Role {private Integer id;private String roleCode;private String roleName;//省略set、get方法 创建User类&#xff08;在User中有roleId…

【3DMax教程】三维产品可视化视频教程 3d Products Visualization Course

【3DMax教程】三维产品可视化视频教程 3d Products Visualization Course 三维产品可视化课程 教程大小&#xff1a;5.38G 1280X720 含课程素材文件 你会学到什么 项目简介及其必须包含的内容 蓝图以及如何获得和使用 逐步建模流程 如何制作UV和纹理 用UV投射材料 生成…

Spring MVC 和WebFlux 区别

本节主要对比了WebMvc 和 WebFlux两个Web框架,Spring已经为我们开发做了很大努力了,所以在合适的场景下这种异步框架还是非常可行的。但是还要考虑后期其它异步框架是否能够完善,全链路异步才能发挥异步最大的优势。

Cygwin鸡毛蒜皮

2019独角兽企业重金招聘Python工程师标准>>> Windows命令乱码: cygwin控制台mintty的编码缺省是UTF-8, 右键调整mintty选项[text] 改编码为GBK UNIX路径和Windows路径互转: 使用cygpath工具. 如: #cd cygpath C:\\Windows 安装包管理器apt-cyg: 安装: # svn --fo…

Using unique option prefix myisam-recover instead of myisam-recover-option

[转载]关于mysql error.log报"Using unique option prefix myisam-recover instead of myisam-recover-options ..."转载&#xff1a;http://blog.csdn.net/cloud_xy/article/details/21756601启动时日志中有这个警告的&#xff1a;[Warning] Using unique option pr…

Maya硬表面建模学习教程 Master Hard Surface Modeling in Maya 2020

Maya硬表面建模学习教程 Master Hard Surface Modeling in Maya 2020 流派:电子学习| MP4 |视频:h264&#xff0c;1280720 |音频:aac&#xff0c;48000 Hz 语言:英语 中文字幕&#xff08;机译&#xff09;原英文字幕 |大小:33.0 GB | 145节课程| (36h 18m) 你会学到什么 云…

Java学习总结:8

链表 class Node2{ //定义一个节点private String data;private Node2 next; //要保存的下一个节点public Node2(String data){ //每一个Node2对象都必须保存相应的数据this.datadata;}public void setNext(Node2 next){this.nextnext;}public Node2 getNext(){return this.…

(原创)c#学习笔记10--定义类成员03--接口的实现01--显示实现接口成员

10.3 接口的实现 在继续前&#xff0c;先讨论一下如何定义和实现接口。第9章介绍了接口定义的方式与类相似&#xff0c;使用的代码如下&#xff1a; interface IMyInterface {// Interface members. } 接口成员的定义与类成员的定义相似&#xff0c;但有几个重要的区别&#…

JVM架构解析

本文阐述了JVM的构成和组件&#xff0c;配图清晰易懂&#xff0c;是学习Java开发者的入门必读文章。 每个Java开发人员都知道字节码经由JRE&#xff08;Java运行时环境&#xff09;执行。但他们或许不知道JRE其实是由Java虚拟机&#xff08;JVM&#xff09;实现&#xff0c;JV…

cmd实用命令

1.netstat 查看电脑端口状况 实际应用举例&#xff1a;查看某软件坚监听的电脑端口。 在任务管理器中选择列...&#xff0c;打开PID的显示。在这里查看某个应用程序的线程ID是多少。例如QQ&#xff1a;4904. 运行&#xff0c;cmd&#xff0c;输入netstat -ano&#xff0c;显示当…

嵌入式BootLoader技术内幕(三)

四、 关于串口终端 在 boot loader 程序的设计与实现中&#xff0c;没有什么能够比从串口终端正确地收到打印信息能更令人激动了。此外&#xff0c;向串口终端打印信息也是一个非常重要而又有效的调试手段。但是&#xff0c;我们经常会碰到串口终端显示乱码或根本没有显示的问题…

Maya 2020面部绑定动画学习视频教程 Facial Rigging 101 – Maya 2020

Maya 2020面部绑定动画学习视频教程 Facial Rigging 101 – Maya 2020 时长:16h 55m |视频:. MP4 1280x720&#xff0c;30 fps(r) |音频:AAC&#xff0c;44100 Hz&#xff0c;2ch |大小:15.5 GB 共62小节课程 流派:电子学习|语言:英语中文字幕&#xff08;机译&#xff09;含…

Java学习总结:9

继承 继承性是面向对象的第二大主要特征&#xff0c;而继承性要解决的就是代码重用的问题&#xff0c;利用继承性可以从已有的类继续派生出新的子类&#xff0c;也可以利用子类扩展出更多的操作功能。 继承的实现 继承的格式 class 子类 extends 父类 {}子类实际上是将父类…

转 小辉_Ray CORS(跨域资源共享)

前言&#xff1a;上一篇文章在写如何使用JSONP实现跨域请求的时候&#xff0c;偶然间提到CORS&#xff0c;即Cross-Origin Resource Sharing&#xff08;跨域资源共享&#xff09;。虽然前些天也看了一下CORS相关的文章&#xff0c;但是今天兴趣一来还是亲自地写篇博客来研究一…

使用dd命令复制ASM磁盘的spfile

通过下面sql查询参数文件在ASM磁盘中的AU分布SELECT x1.file_number,x1.name,x2.GROUP_KFFXP,x2.DISK_KFFXP,x2.AU_KFFXP,x3.pathFROM (SELECT *FROM (SELECT t1.GROUP_NUMBER, t1.FILE_NUMBER, t2.NAME, rownum AS rnFROM v$asm_file t1LEFT JOIN v$asm_alias t2ON t1.FILE_NU…

[转载]IPMSG(飞鸽传书)协议翻译

/***********************************************************本人(ypxing)根据下面的协议&#xff0c;C语言写的ipmsg(聊天&#xff0c;文件/文件夹传输)*请参见&#xff1a;http://blog.chinaunix.net/u1/35100/showart_689330.html**************************************…

SketchUp Pro 2021基础入门学习视频教程

SketchUp Pro 2021基础入门学习视频教程 1280X720 MP4 |视频:h264&#xff0c;1280720 |音频:AAC&#xff0c;44.1 KHz&#xff0c;2 Ch 流派:电子学习|语言:英语中文字幕&#xff08;根据原英文字幕机译更准确&#xff09; |时长:74节课(7h 31m) |大小:4.9 GB 含课程工程文件…

Java学习总结:10

覆写 在子类定义属性或方法时&#xff0c;有可能出现定义的属性或方法与父类同名的情况&#xff0c;这样的操作就称为覆写。 方法的覆写 当子类定义了和父类的方法名称、返回值类型、参数类型及个数完全相同的方法时&#xff0c;就称为方法的覆写。 class A1{public void f…

ubuntu中启用ssh服务

ssh程序分为有客户端程序openssh-client和服务端程序openssh-server。如果需要ssh登陆到别的电脑&#xff0c;需要安装openssh-client&#xff0c;该程序ubuntu是默认安装的。而如果需要从远程连接到本机&#xff0c;则需要安装openssh-server&#xff0c;该程序需要自己安装。…

JVM 常见异常及内存诊断

栈内存溢出 栈内存大小设置&#xff1a;-Xss size 默认除了window以外的所有操作系统默认情况大小为 1MB&#xff0c;window 的默认大小依赖于虚拟机内存。 栈帧过多导致栈内存溢出 下述示例代码&#xff0c;由于递归深度没有限制且没有设置出口&#xff0c;每次方法的调用都…

解决文字无法缩小的问题

在css设置文字大小的时候&#xff0c;到12px 的时候你在怎么缩小他&#xff0c;他的大小就是不变font-size&#xff1a;百分比来控制也不起作用-webkit-transform: scale(0.8); -o-transform: scale(1); display: inline-block; 转载于:https://www.cnblogs.com/xinlinux/p/408…

asp.net图片浏览器效果

技术来源于同学会实践 前台设计 <% Page Language"C#" AutoEventWireup"true" CodeFile"txh.aspx.cs" Inherits"txh" %> <!DOCTYPE html> <html xmlns"http://www.w3.org/1999/xhtml"> <head runat&qu…