在使用jQuery的$(elem).data()方法时,很少有人会思考一个问题:这个数据到底存在哪里?为什么有的对象调用data方法会静默失败?答案就藏在jQuery内部一个不起眼的函数——acceptData中。这个函数只有短短几行代码,却是整个jQuery数据缓存体系的入口守卫,任何想使用jQuery.data()或.data()方法的对象,都必须先经过它的检验。本文将从源码层面逐行剖析这个函数的判断逻辑,并顺着调用链看清数据存储的完整流程。

acceptData函数的源码与逐行解读
先来看这段函数的源码(以jQuery 1.x和2.x版本为参考,3.x移除了对XML节点的部分支持但核心逻辑一致):
var rnoData = /^(?:\{\{[\s\S]*\}\}|\[[\s\S]*\])$/,
noData = {
"applet ": true,
"embed ": true,
// ...除了object以外的所有对象标签
"object ": true,
"img": true
};
function acceptData( owner ) {
// 只接受以下节点类型:
// 1(元素节点)、9(document)、11(文档片段)
return owner.nodeType === 1 || owner.nodeType === 9 || owner.nodeType === 11;
}在更早期的jQuery版本(1.8以前),这个函数的逻辑更复杂一些,除了判断nodeType之外,还会检查元素是否在noData黑名单中:
acceptData: function( elem ) {
// 不接受data存在于applet、embed以及除Flash以外的object标签上
var match = jQuery.noData[ (elem.nodeName + " ").toLowerCase() ],
noData = match && match !== true && elem.getAttribute("classid") === match;
return !noData;
}这段旧版代码的逻辑值得仔细品味。它先用元素标签名加上一个空格去查jQuery.noData表。为什么要加空格?因为黑名单的键都是带尾部空格的,比如"embed ",这样可以做前缀式的精确匹配,避免"embedxxx"这种标签名被误伤。查表结果有三种情况:查不到说明该标签不在黑名单中,直接放行;查到的值是true,说明该标签被无条件禁止,比如embed和applet;查到的值不是true而是一个字符串(如Flash的classid值),则只有当元素的classid属性恰好等于该字符串时才拒绝,也就是说object标签默认可以存数据,但Flash对象例外。
为什么要单独针对这几个标签呢?因为applet、embed和Flash object是浏览器插件容器,它们与脚本引擎之间存在双向数据绑定。如果你在这些插件对象上挂载自定义属性,插件内部的Java或Flash代码可能会读取并干扰这个属性,导致jQuery存储的内部数据结构被破坏,进而引发缓存ID错乱甚至内存泄漏。jQuery选择直接从源头上拒绝,是一种防御性设计。
从acceptData到elemData:数据存储的完整链路
acceptData被拒绝只是返回false,但真正让开发者感到困惑的是调用.data()时没有任何报错,数据却存不进去。这就需要看Data类的核心方法,acceptData在其中扮演的角色如下:
function Data() {
this.expando = jQuery.expando + Data.uid++;
}
Data.accepts = jQuery.acceptData;
Data.prototype = {
key: function( owner ) {
if ( !Data.accepts( owner ) ) {
return 0;
}
// ...为owner生成或读取缓存的key
},
set: function( owner, data, value ) {
// 调用key方法,如果acceptData拒绝则key返回0
// 后续流程直接中断,set静默失败
}
};当调用$(elem).data("name", "value")时,内部执行顺序是:先由set方法调用key(owner),而key方法的第一步就是调用acceptData。如果owner不被接受,key直接返回0,整个存取流程就此中断,而且不会抛出任何异常。这就是静默失败的根源。理解了这一点,遇到data方法失灵时就应该首先检查目标对象是不是纯文本节点、注释节点或者一个插件元素。
那通过检验的元素,数据存在哪里呢?答案是存在一个叫expando的属性上。jQuery会为每个可接受的对象挂一个形如jQuery1900123456789012345的唯一属性名,这个字符串由jQuery.expando(包含版本号和时间戳,保证页面多次引入jQuery时不冲突)加上自增的uid组成。对象本身只保存一个指向缓存字典的key,真正的数据放在内部的Data.cache对象中。这种间接存储避免了直接把自定义属性挂到DOM元素上,既防止了循环引用导致的IE内存泄漏,也避免了自定义属性与元素原生属性重名的风险。
还有一个细节:对于nodeType为1的普通元素,jQuery优先尝试把expando挂在元素上;对于某些无法附加属性的对象(例如旧浏览器的XML节点),则回退到用JavaScript对象的方式包装。而nodeType为9的document对象和11的DocumentFragment也被允许存数据,这意味着你可以在文档级别维护全局缓存,这正是jQuery.cache体系能够支撑事件绑定、队列、动画状态等内部机制的基础——事件处理器本身就存储在这套缓存系统中。
实战排查与版本差异中的注意事项
在实际开发中,acceptData相关的坑主要出现在三种场景。第一种是对纯文本节点或注释节点调用data方法,比如$(elem).contents().filter(function(){return this.nodeType===3}).data("x",1),表面上代码能执行,但数据根本没存上,后续读取永远是undefined。排查方法很简单,在控制台检查目标的nodeType即可。
第二种场景是动态创建的object或embed元素。有些老旧的富媒体页面试图用$.data(flashObj, "state", ...)来跟踪播放器状态,结果时灵时不灵。此时应改用一个外部的普通JavaScript对象做映射,例如维护一个以元素id为键的字典,绕开黑名单限制:
// 不推荐:对插件元素直接使用data
// $.data(flashObject, "state", "playing");
// 推荐:用外部字典管理插件状态
var playerStates = {};
playerStates["player-1"] = "playing";
console.log(playerStates["player-1"]); // playing第三种是版本升级带来的行为差异。jQuery 1.8版本对数据模块做了大规模重构,把原来挂在jQuery命名空间下的$.data实现改为Data类,acceptData也随之从黑名单检查简化为纯nodeType检查(因为新版浏览器对插件元素的属性附加行为已经安全许多)。到了jQuery 3.x,官方完全移除了对旧版IE的支持,数据模块进一步精简,同时废弃了.data()读取HTML5data-*属性时的自动类型转换中的部分规则。如果你的项目还在维护基于1.7以前版本的代码,阅读源码时要注意以当时版本的实现为准,新旧实现逻辑差异不小。
最后需要提醒一点,acceptData返回true只代表有资格存数据,并不代表数据一定能被正确序列化或持久化。jQuery的缓存是纯内存态的,页面刷新后一切清空,这也是它与dataset、localStorage的本质区别。理解acceptData这个小小的函数,不仅能帮你看懂jQuery数据模块三分之一以上的源码结构,更能建立对框架内部防御性设计思想的认知:框架在入口处做的每一次拦截,背后往往都是前人踩过的真实坑。
jQueryacceptData数据缓存修改时间:2026-09-14 04:12:47