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

相关推荐
GISer_Jing2 小时前
明天好好总结汇总分析博客
前端·javascript·面试
做运维的阿瑞5 小时前
Windows 环境下安装 Node.js 和 Vue.js 框架完全指南
前端·javascript·vue.js·windows·node.js
谢尔登9 小时前
【Nest】日志记录
javascript·中间件·node.js
Andytoms11 小时前
Android geckoview 集成,JS交互,官方demo
android·javascript·交互
知识分享小能手12 小时前
微信小程序入门学习教程,从入门到精通,微信小程序开发进阶(7)
前端·javascript·学习·程序人生·微信小程序·小程序·vue3
liangshanbo121515 小时前
React 18 的自动批处理
前端·javascript·react.js
sunbyte15 小时前
每日前端宝藏库 | tinykeys ✨
前端·javascript
Demoncode_y16 小时前
Vue3 + Three.js 实现 3D 汽车个性化定制及展示
前端·javascript·vue.js·3d·汽车·three.js
细节控菜鸡17 小时前
【2025最新】ArcGIS for JS 实现地图卷帘效果,动态修改参数(进阶版)
开发语言·javascript·arcgis
南屿im18 小时前
把代码变成“可改的树”:一文读懂前端 AST 的原理与实战
前端·javascript