[memoization.js]是 Knockout.js 框架中用于处理 DOM 模板备忘录的核心模块。它提供了一种机制,允许将 JavaScript 函数与 DOM 注释节点关联起来,在适当的时机执行这些函数。这种机制主要用于模板系统中,处理那些需要延迟执行的绑定和逻辑。
核心概念
什么是备忘录(Memoization)?
在 Knockout.js 中,备忘录是一种将函数与 DOM 节点关联的技术。通过在 DOM 中插入特殊的注释节点作为占位符,将需要稍后执行的函数存储起来,等到合适的时机再执行这些函数。
应用场景
- 模板渲染 - 在模板渲染过程中,某些绑定需要在 DOM 节点插入后再执行
- 延迟绑定 - 对于还没有 DOM 节点的绑定,可以先备忘录化,等节点可用时再执行
- 复杂绑定处理 - 处理嵌套或条件绑定时的复杂逻辑
核心实现
备忘录存储
var memos = {};
使用一个全局对象来存储所有备忘录,键为随机生成的 ID,值为对应的函数。
ID 生成
function randomMax8HexChars() {
return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
}
function generateRandomId() {
return randomMax8HexChars() + randomMax8HexChars();
}
通过生成随机的 16 位十六进制字符串作为备忘录的唯一标识符。
备忘录节点查找
function findMemoNodes(rootNode, appendToArray) {
if (!rootNode)
return;
if (rootNode.nodeType == 8) {
var memoId = ko.memoization.parseMemoText(rootNode.nodeValue);
if (memoId != null)
appendToArray.push({ domNode: rootNode, memoId: memoId });
} else if (rootNode.nodeType == 1) {
for (var i = 0, childNodes = rootNode.childNodes, j = childNodes.length; i < j; i++)
findMemoNodes(childNodes[i], appendToArray);
}
}
递归遍历 DOM 树,查找所有包含备忘录的注释节点。
核心 API
memoize
memoize: function (callback) {
if (typeof callback != "function")
throw new Error("You can only pass a function to ko.memoization.memoize()");
var memoId = generateRandomId();
memos[memoId] = callback;
return "<!--[ko_memo:" + memoId + "]-->";
}
将函数存储到备忘录中,并返回对应的注释节点 HTML 字符串。
unmemoize
unmemoize: function (memoId, callbackParams) {
var callback = memos[memoId];
if (callback === undefined)
throw new Error("Couldn't find any memo with ID " + memoId + ". Perhaps it's already been unmemoized.");
try {
callback.apply(null, callbackParams || []);
return true;
}
finally { delete memos[memoId]; }
}
执行指定 ID 的备忘录函数,并从存储中删除。
unmemoizeDomNodeAndDescendants
unmemoizeDomNodeAndDescendants: function (domNode, extraCallbackParamsArray) {
var memos = [];
findMemoNodes(domNode, memos);
for (var i = 0, j = memos.length; i < j; i++) {
var node = memos[i].domNode;
var combinedParams = [node];
if (extraCallbackParamsArray)
ko.utils.arrayPushAll(combinedParams, extraCallbackParamsArray);
ko.memoization.unmemoize(memos[i].memoId, combinedParams);
node.nodeValue = ""; // Neuter this node so we don't try to unmemoize it again
if (node.parentNode)
node.parentNode.removeChild(node); // If possible, erase it totally
}
}
查找并执行指定 DOM 节点及其后代中的所有备忘录。
parseMemoText
parseMemoText: function (memoText) {
var match = memoText.match(/^\[ko_memo\:(.*?)\]$/);
return match ? match[1] : null;
}
解析注释节点文本,提取备忘录 ID。
在 Knockout.js 中的应用
模板系统
在模板系统中,当还没有可用的 DOM 节点时,使用备忘录机制:
return ko.memoization.memoize(function (domNode) {
ko.renderTemplate(template, dataOrBindingContext, options, domNode, "replaceNode");
});
绑定处理
在处理绑定时,先应用绑定再执行备忘录:
invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) {
if (node.nodeType === 1 || node.nodeType === 8)
ko.applyBindings(bindingContext, node);
});
invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) {
if (node.nodeType === 1 || node.nodeType === 8)
ko.memoization.unmemoizeDomNodeAndDescendants(node, [bindingContext]);
});
优化方案(针对现代浏览器)
针对现代浏览器,我们可以简化备忘录模块的实现:
ko.memoization = (function () {
const memos = new Map();
function generateRandomId() {
return crypto.randomUUID ? crypto.randomUUID() :
`${Math.random().toString(36).substr(2, 9)}-${Date.now().toString(36)}`;
}
function findMemoNodes(rootNode, appendToArray) {
if (!rootNode) return;
if (rootNode.nodeType == 8) {
const memoId = ko.memoization.parseMemoText(rootNode.nodeValue);
if (memoId != null)
appendToArray.push({ domNode: rootNode, memoId });
} else if (rootNode.nodeType == 1) {
// 使用现代遍历方法
[...rootNode.childNodes].forEach(childNode =>
findMemoNodes(childNode, appendToArray));
}
}
return {
memoize(callback) {
if (typeof callback != "function")
throw new Error("You can only pass a function to ko.memoization.memoize()");
const memoId = generateRandomId();
memos.set(memoId, callback);
return `<!--[ko_memo:${memoId}]-->`;
},
unmemoize(memoId, callbackParams) {
const callback = memos.get(memoId);
if (callback === undefined)
throw new Error(`Couldn't find any memo with ID ${memoId}. Perhaps it's already been unmemoized.`);
try {
callback.apply(null, callbackParams || []);
return true;
} finally {
memos.delete(memoId);
}
},
unmemoizeDomNodeAndDescendants(domNode, extraCallbackParamsArray) {
const memoNodes = [];
findMemoNodes(domNode, memoNodes);
memoNodes.forEach(({ domNode: node, memoId }) => {
const combinedParams = [node];
if (extraCallbackParamsArray)
combinedParams.push(...extraCallbackParamsArray);
ko.memoization.unmemoize(memoId, combinedParams);
node.nodeValue = "";
node.parentNode?.removeChild(node);
});
},
parseMemoText(memoText) {
const match = memoText.match(/^\[ko_memo\:(.*?)\]$/);
return match ? match[1] : null;
}
};
})();
ko.exportSymbol('memoization', ko.memoization);
ko.exportSymbol('memoization.memoize', ko.memoization.memoize);
ko.exportSymbol('memoization.unmemoize', ko.memoization.unmemoize);
ko.exportSymbol('memoization.parseMemoText', ko.memoization.parseMemoText);
ko.exportSymbol('memoization.unmemoizeDomNodeAndDescendants', ko.memoization.unmemoizeDomNodeAndDescendants);
优化要点
- 使用现代数据结构 - 使用
Map
替代普通对象存储备忘录 - 使用现代 ID 生成 - 利用
crypto.randomUUID
API - 简化代码 - 使用
const[/](file:///Users/xianhao/jvy/nodejs/gitee/@licence/Apache-2.0/dist/index.d.ts)let
和箭头函数 - 使用现代数组方法 - 使用展开语法和
forEach
- 可选链操作符 - 使用
?.
安全地访问属性
使用示例
基本用法
// 创建备忘录
const memoHtml = ko.memoization.memoize(function(domNode, context) {
console.log('Memo executed on node:', domNode);
// 执行一些需要 DOM 节点的操作
});
// memoHtml 现在包含类似 <!--ko_memo:abcd1234--> 的字符串
console.log(memoHtml);
// 执行备忘录(通常由 Knockout.js 内部处理)
// ko.memoization.unmemoize(memoId, [domNode, context]);
实际应用场景
// 在自定义模板引擎中使用
ko.customTemplateEngine = function() {
this.renderTemplateSource = function(templateSource, bindingContext, options) {
const templateText = templateSource.text();
// 如果还没有 DOM 节点,创建备忘录
if (!options.targetNode) {
return ko.memoization.memoize(function(domNode) {
// 当 DOM 节点可用时执行实际的渲染
const nodes = ko.utils.parseHtmlFragment(templateText);
ko.utils.setDomNodeChildren(domNode, nodes);
ko.applyBindings(bindingContext, domNode);
});
}
// 如果有 DOM 节点,直接渲染
const nodes = ko.utils.parseHtmlFragment(templateText);
ko.utils.setDomNodeChildren(options.targetNode, nodes);
ko.applyBindings(bindingContext, options.targetNode);
return nodes;
};
};
与组件系统的集成
// 在组件加载中使用备忘录
ko.components.register('my-component', {
template: '<div data-bind="text: message"></div>',
viewModel: function(params) {
this.message = ko.observable('Hello World');
// 对于异步加载的组件,可以使用备忘录机制
return ko.memoization.memoize(function(element) {
ko.applyBindingsToDescendants(this, element);
}.bind(this));
}
});
总结
[memoization.js]是 Knockout.js 中一个巧妙的模块,它通过将函数与 DOM 注释节点关联,实现了延迟执行的机制。这种设计解决了模板系统中没有可用 DOM 节点时的绑定处理问题,是 Knockout.js 能够灵活处理各种复杂绑定场景的关键技术之一。
该模块的设计体现了在现代 Web 开发中对延迟执行和异步处理的重视。通过合理的抽象和封装,为开发者提供了简单易用的 API 来处理复杂的 DOM 操作场景。对于现代浏览器,我们可以利用新的 Web API 进一步简化其实现,提高代码的可读性和性能。
备忘录机制虽然在 Knockout.js 的现代使用中可能不如早期版本那么常见,但它仍然是框架处理复杂模板和绑定场景的重要工具,体现了 Knockout.js 设计的灵活性和强大功能。