Knockout.js DOM 数据存储模块详解
概述
[utils.domData.js] 是 Knockout.js 框架中用于在 DOM 节点上存储数据的核心模块。它提供了一种跨浏览器的机制,允许开发者将任意数据与 DOM 节点关联起来,这对于框架内部管理节点状态和清理资源至关重要。
核心概念
为什么需要 DOM 数据存储?
在 Web 应用开发中,经常需要将数据与 DOM 节点关联起来。例如:
- 存储与节点相关的 Knockout 绑定上下文
- 保存节点的清理回调函数
- 关联模板渲染相关数据
DOM 数据存储模块提供了一种标准化的方法来实现这一需求。
设计挑战
在设计 DOM 数据存储时,需要考虑以下问题:
- 内存泄漏 - 避免因循环引用导致的内存泄漏
- 跨浏览器兼容性 - 不同浏览器对 DOM 扩展的支持不同
- 性能 - 数据存取操作需要高效
- 清理机制 - 节点移除时需要正确清理相关数据
核心实现
数据存储策略
在原始实现中,Knockout.js 根据浏览器类型采用不同的存储策略:
var getDataForNode, clear;
if (!ko.utils.ieVersion) {// 现代浏览器:直接在节点上存储数据getDataForNode = function (node, createIfNotFound) {var dataForNode = node[dataStoreKeyExpandoPropertyName];if (!dataForNode && createIfNotFound) {dataForNode = node[dataStoreKeyExpandoPropertyName] = {};}return dataForNode;};clear = function (node) {if (node[dataStoreKeyExpandoPropertyName]) {delete node[dataStoreKeyExpandoPropertyName];return true;}return false;};
} else {// 旧版 IE:使用单独的数据存储对象getDataForNode = function (node, createIfNotFound) {var dataStoreKey = node[dataStoreKeyExpandoPropertyName];var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey];if (!hasExistingDataStore) {if (!createIfNotFound)return undefined;dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++;dataStore[dataStoreKey] = {};}return dataStore[dataStoreKey];};clear = function (node) {var dataStoreKey = node[dataStoreKeyExpandoPropertyName];if (dataStoreKey) {delete dataStore[dataStoreKey];node[dataStoreKeyExpandoPropertyName] = null;return true;}return false;};
}
这种设计主要是为了解决旧版 IE 浏览器中存在的内存泄漏问题。
唯一标识符生成
var uniqueId = 0;
var dataStoreKeyExpandoPropertyName = "__ko__" + (new Date).getTime();
var dataStore = {};
通过时间戳和递增计数器确保每个数据存储键的唯一性。
核心 API
get 方法
get: function (node, key) {var dataForNode = getDataForNode(node, false);return dataForNode && dataForNode[key];
}
获取与指定节点关联的特定键的数据,如果数据不存在则返回 undefined
。
set 方法
set: function (node, key, value) {// Make sure we don't actually create a new domData key if we are actually deleting a valuevar dataForNode = getDataForNode(node, value !== undefined /* createIfNotFound */);dataForNode && (dataForNode[key] = value);
}
设置与指定节点关联的数据。当值为 undefined
时,不会创建新的数据存储对象。
getOrSet 方法
getOrSet: function (node, key, value) {var dataForNode = getDataForNode(node, true /* createIfNotFound */);return dataForNode[key] || (dataForNode[key] = value);
}
获取与节点关联的数据,如果不存在则设置默认值。
clear 方法
clear: clear
清理与节点关联的所有数据,防止内存泄漏。
nextKey 方法
nextKey: function () {return (uniqueId++) + dataStoreKeyExpandoPropertyName;
}
生成唯一的键名,用于在节点上存储不同类型的数据。
在 Knockout.js 中的应用
DOM 节点清理系统
在 [utils.domNodeDisposal.js] 中,DOM 数据存储用于管理节点的清理回调:
var domDataKey = ko.utils.domData.nextKey();
function getDisposeCallbacksCollection(node, createIfNotFound) {var allDisposeCallbacks = ko.utils.domData.get(node, domDataKey);if ((allDisposeCallbacks === undefined) && createIfNotFound) {allDisposeCallbacks = [];ko.utils.domData.set(node, domDataKey, allDisposeCallbacks);}return allDisposeCallbacks;
}
模板系统
在模板系统中,DOM 数据存储用于跟踪模板计算:
var templateComputedDomDataKey = ko.utils.domData.nextKey();
function disposeOldComputed(element) {var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);if (oldComputed && (typeof(oldComputed.dispose) == 'function'))oldComputed.dispose();
}
虚拟元素
在虚拟元素系统中,用于标记已匹配的结束注释:
var matchedEndCommentDataKey = "__ko_matchedEndComment__"
// ...
ko.utils.domData.set(currentNode, matchedEndCommentDataKey, true);
优化方案(针对现代浏览器)
针对现代浏览器,我们可以大幅简化 DOM 数据存储的实现:
ko.utils.domData = new (function () {var uniqueId = 0;var dataStoreKeyExpandoPropertyName = "__ko__" + (new Date).getTime();return {get: function (node, key) {if (!node[dataStoreKeyExpandoPropertyName]) {return undefined;}return node[dataStoreKeyExpandoPropertyName][key];},set: function (node, key, value) {// 如果要删除值且节点上没有数据存储,则直接返回if (value === undefined && !node[dataStoreKeyExpandoPropertyName]) {return;}// 确保节点上有数据存储对象if (!node[dataStoreKeyExpandoPropertyName]) {node[dataStoreKeyExpandoPropertyName] = {};}// 设置或删除值if (value !== undefined) {node[dataStoreKeyExpandoPropertyName][key] = value;} else {delete node[dataStoreKeyExpandoPropertyName][key];}},getOrSet: function (node, key, value) {if (!node[dataStoreKeyExpandoPropertyName]) {node[dataStoreKeyExpandoPropertyName] = {};}if (!(key in node[dataStoreKeyExpandoPropertyName])) {node[dataStoreKeyExpandoPropertyName][key] = value;}return node[dataStoreKeyExpandoPropertyName][key];},clear: function (node) {if (node[dataStoreKeyExpandoPropertyName]) {delete node[dataStoreKeyExpandoPropertyName];return true;}return false;},nextKey: function () {return (uniqueId++) + dataStoreKeyExpandoPropertyName;}};
})();
优化要点
- 移除 IE 兼容代码 - 现代浏览器可以直接在节点上存储对象
- 简化逻辑 - 不再需要额外的数据存储对象
- 提高性能 - 减少了间接访问层
- 降低内存占用 - 不再需要全局数据存储对象
使用示例
基本用法
// 设置数据
ko.utils.domData.set(element, 'myKey', { name: 'John', age: 30 });// 获取数据
var data = ko.utils.domData.get(element, 'myKey');
console.log(data.name); // 输出: John// 获取或设置默认值
var config = ko.utils.domData.getOrSet(element, 'config', { theme: 'default' });// 清理数据
ko.utils.domData.clear(element);
实际应用场景
// 存储绑定上下文
ko.utils.domData.set(node, 'bindingContext', bindingContext);// 存储清理回调
var disposeKey = ko.utils.domData.nextKey();
ko.utils.domData.set(node, disposeKey, function() {// 清理资源的代码console.log('Node is being cleaned up');
});// 在节点清理时执行回调
ko.utils.domNodeDisposal.addDisposeCallback(node, function() {var callback = ko.utils.domData.get(node, disposeKey);if (callback) callback();ko.utils.domData.clear(node);
});
总结
[utils.domData.js]是 Knockout.js 框架中一个关键但低调的模块,它为框架提供了在 DOM 节点上存储数据的能力。通过考虑不同浏览器的兼容性问题,该模块确保了框架在各种环境下的稳定运行。
对于现代浏览器,我们可以大幅简化其实现,移除历史兼容代码,从而提高性能并降低维护成本。无论采用哪种实现方式,DOM 数据存储模块都体现了在 Web 开发中处理 DOM 扩展数据的重要性和复杂性。