Knockout.js 备忘录模块详解 - 实践

[memoization.js]是 Knockout.js 框架中用于处理 DOM 模板备忘录的核心模块。它提供了一种机制,允许将 JavaScript 函数与 DOM 注释节点关联起来,在适当的时机执行这些函数。这种机制主要用于模板系统中,处理那些需要延迟执行的绑定和逻辑。

核心概念

什么是备忘录(Memoization)?

在 Knockout.js 中,备忘录是一种将函数与 DOM 节点关联的技术。通过在 DOM 中插入特殊的注释节点作为占位符,将需要稍后执行的函数存储起来,等到合适的时机再执行这些函数。

应用场景

  1. 模板渲染 - 在模板渲染过程中,某些绑定需要在 DOM 节点插入后再执行
  2. 延迟绑定 - 对于还没有 DOM 节点的绑定,可以先备忘录化,等节点可用时再执行
  3. 复杂绑定处理 - 处理嵌套或条件绑定时的复杂逻辑

核心实现

备忘录存储

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);

优化要点

  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. 可选链操作符 - 使用 ?. 安全地访问属性

使用示例

基本用法

// 创建备忘录
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 设计的灵活性和强大功能。

posted on 2025-11-11 14:16  slgkaifa  阅读(0)  评论(0)    收藏  举报

导航