概述
virtualElements.js是 Knockout.js 框架中的一个核心模块,它实现了"虚拟元素"的概念。这个模块解决了在使用容器无关模板(containerless templates)时的 DOM 操作问题,比如 <!-- ko if: someCondition -->...<!-- /ko -->
这样的注释语法。
核心概念
什么是虚拟元素?
虚拟元素是一种抽象概念,它允许使用 HTML 注释节点来表示 DOM 层次结构。传统的 DOM 层次结构由实际的 HTML 元素构成,而虚拟元素扩展了这个概念,允许注释节点也参与层次结构的定义。
容器无关模板
容器无关模板是 Knockout.js 的一个重要特性,它允许我们在不添加额外 DOM 元素的情况下创建绑定。例如:
html
<!-- ko if: isVisible -->
<div>Some content</div>
<!-- /ko -->
在这个例子中,<!-- ko if: isVisible -->
和 <!-- /ko -->
注释节点定义了一个虚拟容器,中间的内容只有在 isVisible
为 true 时才会显示。
核心实现
注释节点处理
javascript
var commentNodesHaveTextProperty = document && document.createComment("test").text === "<!--test-->";
var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+([\s\S]+))?\s*-->$/ : /^\s*ko(?:\s+([\s\S]+))?\s*$/;
var endCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/;
由于 IE9 的兼容性问题,代码需要处理不同浏览器对注释节点文本内容的访问方式。IE9 使用 text
属性,而其他浏览器使用 nodeValue
。
虚拟子元素管理
getVirtualChildren 函数
这个函数用于获取虚拟元素的子节点:
javascript
function getVirtualChildren(startComment, allowUnbalanced) {
var currentNode = startComment;
var depth = 1;
var children = [];
while (currentNode = currentNode.nextSibling) {
if (isEndComment(currentNode)) {
ko.utils.domData.set(currentNode, matchedEndCommentDataKey, true);
depth--;
if (depth === 0)
return children;
}
children.push(currentNode);
if (isStartComment(currentNode))
depth++;
}
if (!allowUnbalanced)
throw new Error("Cannot find closing comment tag to match: " + startComment.nodeValue);
return null;
}
该函数通过跟踪嵌套深度来正确识别虚拟元素的边界,支持嵌套的虚拟元素结构。
主要 API
childNodes
javascript
childNodes: function(node) {
return isStartComment(node) ? getVirtualChildren(node) : node.childNodes;
}
根据节点类型返回子节点:对于虚拟元素起始注释,返回虚拟子节点;对于普通元素,返回实际的 childNodes。
emptyNode
javascript
emptyNode: function(node) {
if (!isStartComment(node))
ko.utils.emptyDomNode(node);
else {
var virtualChildren = ko.virtualElements.childNodes(node);
for (var i = 0, j = virtualChildren.length; i < j; i++)
ko.removeNode(virtualChildren[i]);
}
}
清空节点内容,对虚拟元素和普通元素分别处理。
setDomNodeChildren
javascript
setDomNodeChildren: function(node, childNodes) {
if (!isStartComment(node))
ko.utils.setDomNodeChildren(node, childNodes);
else {
ko.virtualElements.emptyNode(node);
var endCommentNode = node.nextSibling;
for (var i = 0, j = childNodes.length; i < j; i++)
endCommentNode.parentNode.insertBefore(childNodes[i], endCommentNode);
}
}
设置节点的子元素,将新子元素插入到结束注释之前。
firstChild 和 nextSibling
这些方法确保在遍历 DOM 时正确处理虚拟元素:
javascript
firstChild: function(node) {
if (!isStartComment(node)) {
if (node.firstChild && isEndComment(node.firstChild)) {
throw new Error("Found invalid end comment, as the first child of " + node);
}
return node.firstChild;
} else if (!node.nextSibling || isEndComment(node.nextSibling)) {
return null;
} else {
return node.nextSibling;
}
}
特殊处理
IE 兼容性问题处理
代码中特别处理了 IE 浏览器的兼容性问题,包括注释节点的文本属性访问和 DOM 结构解析问题。
HTML 结构规范化
javascript
normaliseVirtualElementDomStructure: function(elementVerified) {
// Workaround for https://github.com/SteveSanderson/knockout/issues/155
// (IE <= 8 or IE 9 quirks mode parses your HTML weirdly, treating closing </li> tags as if they don't exist, thereby moving comment nodes
// that are direct descendants of <ul> into the preceding <li>)
if (!htmlTagsWithOptionallyClosingChildren[ko.utils.tagNameLower(elementVerified)])
return;
// Scan immediate children to see if they contain unbalanced comment tags. If they do, those comment tags
// must be intended to appear *after* that child, so move them there.
var childNode = elementVerified.firstChild;
if (childNode) {
do {
if (childNode.nodeType === 1) {
var unbalancedTags = getUnbalancedChildTags(childNode);
if (unbalancedTags) {
// Fix up the DOM by moving the unbalanced tags to where they most likely were intended to be placed - *after* the child
var nodeToInsertBefore = childNode.nextSibling;
for (var i = 0; i < unbalancedTags.length; i++) {
if (nodeToInsertBefore)
elementVerified.insertBefore(unbalancedTags[i], nodeToInsertBefore);
else
elementVerified.appendChild(unbalancedTags[i]);
}
}
}
} while (childNode = childNode.nextSibling);
}
}
此函数处理 IE 浏览器在解析 HTML 时的特殊行为,确保注释节点被正确放置。
使用示例
基本用法
javascript
// 获取虚拟元素的子节点
var children = ko.virtualElements.childNodes(startCommentNode);
// 清空虚拟元素内容
ko.virtualElements.emptyNode(startCommentNode);
// 设置虚拟元素的子节点
ko.virtualElements.setDomNodeChildren(startCommentNode, newChildren);
遍历虚拟元素
javascript
var firstChild = ko.virtualElements.firstChild(parentNode);
var nextSibling = ko.virtualElements.nextSibling(currentNode);
总结
virtualElements.js\]是 Knockout.js 实现容器无关模板的关键模块。它通过抽象 DOM 操作,使得框架可以统一处理普通 DOM 元素和虚拟元素,为开发者提供了更灵活的模板编写方式。该模块还充分考虑了各种浏览器的兼容性问题,确保在不同环境下都能正常工作。