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

相关推荐
aduzhe5 分钟前
关于在嵌入式中打印float类型遇到的bug
前端·javascript·bug
鹏多多32 分钟前
vue过滤器filter的详解及和computed的区别
前端·javascript·vue.js
孟陬33 分钟前
在浏览器控制台中优雅地安装 npm 包 console.install('lodash')
javascript·node.js
Moment38 分钟前
LangChain 1.0 发布:agent 框架正式迈入生产级
前端·javascript·后端
晓得迷路了1 小时前
栗子前端技术周刊第 106 期 - pnpm 10.21、Node.js v25.2.0、Bun v1.3.2...
前端·javascript·html
码上成长1 小时前
<script setup> 实战模式:大型组件怎么拆?
开发语言·javascript·vue.js
花归去1 小时前
【Vue3】 中的 【unref】:详解与使用
前端·javascript·vue.js
小霖家的混江龙2 小时前
巧用辅助线,轻松实现类拼多多的 Tab 吸顶效果
前端·javascript·react.js
艾小码2 小时前
还在为异步组件加载烦恼?这招让你的Vue应用更丝滑!
前端·javascript·vue.js
清沫8 小时前
VSCode debugger 调试指南
前端·javascript·visual studio code