Knockout.js 备忘录模块详解

memoization.js\]是 Knockout.js 框架中用于处理 DOM 模板备忘录的核心模块。它提供了一种机制,允许将 JavaScript 函数与 DOM 注释节点关联起来,在适当的时机执行这些函数。这种机制主要用于模板系统中,处理那些需要延迟执行的绑定和逻辑。 ### 核心概念 #### 什么是备忘录(Memoization)? 在 Knockout.js 中,备忘录是一种将函数与 DOM 节点关联的技术。通过在 DOM 中插入特殊的注释节点作为占位符,将需要稍后执行的函数存储起来,等到合适的时机再执行这些函数。 #### 应用场景 1. **模板渲染** - 在模板渲染过程中,某些绑定需要在 DOM 节点插入后再执行 2. **延迟绑定** - 对于还没有 DOM 节点的绑定,可以先备忘录化,等节点可用时再执行 3. **复杂绑定处理** - 处理嵌套或条件绑定时的复杂逻辑 ### 核心实现 #### 备忘录存储 ```javascript var memos = {}; ``` 使用一个全局对象来存储所有备忘录,键为随机生成的 ID,值为对应的函数。 #### ID 生成 ```javascript function randomMax8HexChars() { return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1); } function generateRandomId() { return randomMax8HexChars() + randomMax8HexChars(); } ``` 通过生成随机的 16 位十六进制字符串作为备忘录的唯一标识符。 #### 备忘录节点查找 ```javascript 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 ```javascript 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 ""; } ``` 将函数存储到备忘录中,并返回对应的注释节点 HTML 字符串。 ##### unmemoize ```javascript 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 ```javascript 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 ```javascript parseMemoText: function (memoText) { var match = memoText.match(/^\[ko_memo\:(.*?)\]$/); return match ? match[1] : null; } ``` 解析注释节点文本,提取备忘录 ID。 ### 在 Knockout.js 中的应用 #### 模板系统 在模板系统中,当还没有可用的 DOM 节点时,使用备忘录机制: ```javascript return ko.memoization.memoize(function (domNode) { ko.renderTemplate(template, dataOrBindingContext, options, domNode, "replaceNode"); }); ``` #### 绑定处理 在处理绑定时,先应用绑定再执行备忘录: ```javascript 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]); }); ``` ### 优化方案(针对现代浏览器) 针对现代浏览器,我们可以简化备忘录模块的实现: ```javascript 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 ``; }, 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); ``` #### 优化要点 1. **使用现代数据结构** - 使用 `Map` 替代普通对象存储备忘录 2. **使用现代 ID 生成** - 利用 `crypto.randomUUID` API 3. **简化代码** - 使用 `const[/](file:///Users/xianhao/jvy/nodejs/gitee/@licence/Apache-2.0/dist/index.d.ts)let` 和箭头函数 4. **使用现代数组方法** - 使用展开语法和 `forEach` 5. **可选链操作符** - 使用 `?.` 安全地访问属性 ### 使用示例 #### 基本用法 ```javascript // 创建备忘录 const memoHtml = ko.memoization.memoize(function(domNode, context) { console.log('Memo executed on node:', domNode); // 执行一些需要 DOM 节点的操作 }); // memoHtml 现在包含类似 的字符串 console.log(memoHtml); // 执行备忘录(通常由 Knockout.js 内部处理) // ko.memoization.unmemoize(memoId, [domNode, context]); ``` #### 实际应用场景 ```javascript // 在自定义模板引擎中使用 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; }; }; ``` #### 与组件系统的集成 ```javascript // 在组件加载中使用备忘录 ko.components.register('my-component', { template: '

', 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 设计的灵活性和强大功能。

相关推荐
两个西柚呀2 小时前
未在props中声明的属性
前端·javascript·vue.js
SteveJrong5 小时前
面试题 - JavaScript
前端·javascript·面试·ecmascript·基础·找工作·红宝书
阿金要当大魔王~~5 小时前
uniapp 页面标签 传值 ————— uniapp 定义 接口
前端·javascript·uni-app·1024程序员节
一个处女座的程序猿O(∩_∩)O6 小时前
Vue-Loader 深度解析:原理、使用与最佳实践
前端·javascript·vue.js
还是大剑师兰特7 小时前
TypeScript 面试题及详细答案 100题 (91-100)-- 工程实践与框架集成
前端·javascript·typescript·1024程序员节
可触的未来,发芽的智生7 小时前
触摸未来2025-10-25:蓝图绘制
javascript·python·神经网络·程序人生·自然语言处理
用户47949283569157 小时前
typeof null === 'object':JavaScript 最古老的 bug 为何 30 年无法修复?
前端·javascript·面试
非凡ghost8 小时前
By Click Downloader(下载各种在线视频) 多语便携版
前端·javascript·后端
非凡ghost8 小时前
VisualBoyAdvance-M(GBA模拟器) 中文绿色版
前端·javascript·后端
非凡ghost8 小时前
K-Lite Mega/FULL Codec Pack(视频解码器)
前端·javascript·后端