Knockout.js 备忘录模块详解
[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 againif (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 设计的灵活性和强大功能。