Knockout.js 备忘录模块详解

发布于:2025-09-15 ⋅ 阅读:(20) ⋅ 点赞:(0)

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