前言
上一篇《关于Virtual Dom的那些事(二)》主要是介绍了VNode。其实生成VNode的方式在snabbdom中除了VNode自身的最基本的构造函数vnode()外,还有两种方式来生成。一种是toVNode(即把原生的Dom转化成为VNode),还有一种是非常有名的h函数(hyperscript最原始的定义是“Create HyperText with JavaScript.”)。本节主要是分析一下这两种函数在snabbdom中的源码。
toVNode函数的源码
import vnode, {VNode} from './vnode'; // VNode的生成函数及接口 import htmlDomApi, {DOMAPI} from './htmldomapi'; // 由dom操作的一些工具函数 export function toVNode(node: Node, domApi?: DOMAPI): VNode { const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi; let text: string; if (api.isElement(node)) { // 如果是node或者element节点元素 const id = node.id ? '#' + node.id : ''; const cn = node.getAttribute('class'); const c = cn ? '.' + cn.split(' ').join('.') : ''; const sel = api.tagName(node).toLowerCase() + id + c; // 生成VNode所需的第一个参数sel,元素选择器 const attrs: any = {}; const children: Array<VNode> = []; let name: string; let i: number, n: number; const elmAttrs = node.attributes; const elmChildren = node.childNodes; for (i = 0, n = elmAttrs.length; i < n; i++) { // 生成VNode所需的data里面的attrs属性 name = elmAttrs[i].nodeName; if (name !== 'id' && name !== 'class') { attrs[name] = elmAttrs[i].nodeValue; } } for (i = 0, n = elmChildren.length; i < n; i++) { // 对children进行递归生成 children.push(toVNode(elmChildren[i], domApi)); } return vnode(sel, {attrs}, children, undefined, node); // 正式生成VNode } else if (api.isText(node)) { // 如果是文本节点 text = api.getTextContent(node) as string; return vnode(undefined, undefined, undefined, text, node); // 对应生成文本VNode节点 } else if (api.isComment(node)) { // 如果是注释,相应生成注释VNode节点 text = api.getTextContent(node) as string; return vnode('!', {}, [], text, node as any); } else { // 其他情况为空VNode节点 return vnode('', {}, [], undefined, node as any); } } export default toVNode;
h函数的源码
import {vnode, VNode, VNodeData} from './vnode'; // 引入vnode的构造函数、VNode定义接口及VNodeData的定义接口
export type VNodes = Array<VNode>;
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>
import * as is from './is';
function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void { // 增加对svg的支持
data.ns = 'http://www.w3.org/2000/svg';
if (sel !== 'foreignObject' && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
let childData = children[i].data;
if (childData !== undefined) {
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
}
}
}
}
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
export function h(sel: any, b?: any, c?: any): VNode { // 上面是h函数的的四种接口定义,本函数正式实现
var data: VNodeData = {}, children: any, text: any, i: number;
if (c !== undefined) { // 如果传3个参数,且第三个参数有值
data = b; // 直接把第二个参数作为data
if (is.array(c)) { children = c; } // 第3个参数是数组,直接作为children
else if (is.primitive(c)) { text = c; } // 如果是原始文本,则为文本
else if (c && c.sel) { children = ; } // 如果为单个节点元素,则变为数组作为children
} else if (b !== undefined) { // 如果有传第二个有效参数
if (is.array(b)) { children = b; } // 且第2个参数是数组,直接作为children
else if (is.primitive(b)) { text = b; } // 如果为文本,则作为文本
else if (b && b.sel) { children = [b]; } // 如果为单个节点元素,则变为数组作为children
else { data = b; } // 否则,直接作为data
}
if (children !== undefined) { // 如果有children,先把所有的文本节点转化成为VNode
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) { // 如果是svg节点,单独处理
addNS(data, children, sel);
}
return vnode(sel, data, children, text, undefined); // 最后返回VNode
};
export default h;
其实h函数(即hyperscript,最原始的定义是“Create HyperText with JavaScript.”)的应用非常广泛,各种Virtual Dom的实现方式都看到h函数的身影。最热的两个前端框架React及Vue框架都直接支持在其jsx或者.vue文件的template里面使用h函数来定义dom结构。其表现方式非常直接,如:h(sel, data, children),而children也可以递归表示为h函数,那么,还可以这么表示:h(sel, data [h(sel1, data1, children1),h(sel2, data2, children2),…]支持更多层递归表示。使用h函数来表示或者生成虚拟节点相对于VNode的接口定义更为简洁,因为它只接受最多3个参数,然后可以支持无限嵌套。
总而言之
我们可以使用toVNode函数把现有的dom树结构转化成为OldVNode(这种方式特别适用于服务器端渲染的情况,一些框架如果想实现服务器端渲染,渲染后则需把其生成的Dom转化成为VNode,此时就可以使用此方式来进行渲染),后面如果输入的数据有变化可以使用h函数结合数据创建NewVNode(h函数也是推荐使用且最常用的生成VNode的方式),然后通过snabbdom的patch方法对旧VNode与新VNode通过diff算法进行比较然后进行局部更新dom节点,而不需要重新渲染整棵dom树,这就是Virtual Dom的工作原理。
好文章!666,学习了
多谢