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 "<!--[ko_memo:" + memoId + "]-->";
}

将函数存储到备忘录中,并返回对应的注释节点 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 `<!--[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. 可选链操作符 - 使用 ?. 安全地访问属性

使用示例

基本用法

javascript 复制代码
// 创建备忘录
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]);

实际应用场景

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

相关推荐
dsyyyyy11015 小时前
JavaScript变量
开发语言·javascript·ecmascript
kyriewen5 小时前
手写 Promise.all、race、any:不到 30 行代码,解决并发异步的所有姿势
前端·javascript·面试
胡志辉的博客6 小时前
深入浅出理解浏览器事件循环:从一道输出题讲到 Chrome 源码
前端·javascript·chrome·chromium·event loop
代码不加糖7 小时前
js中不会冒泡的事件有哪些?
前端·javascript·vue.js
懂懂tty7 小时前
Vue2与Vue3之间API差异
前端·javascript·vue.js
小二·8 小时前
Next.js 15 全栈开发实战
开发语言·javascript·ecmascript
Rain5099 小时前
2.1 Nest.js 项目初始化与模块化架构
开发语言·前端·javascript·后端·架构·数据分析·node.js
拾年27510 小时前
从零手写 Ajax:用原生 XHR 搭建前后端交互全流程
前端·javascript·ajax
拉勾科研工作室10 小时前
区块链工程毕业论文题目【249个】
开发语言·javascript
小林ixn10 小时前
你以为你懂 + 号?看完这篇 Bun + TS 实战,才发现以前全写错了
前端·javascript·typescript