关于Virtual Dom的那些事(三)

前言

上一篇《关于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的工作原理。

作者: 博主

Talk is cheap, show me the code!

《关于Virtual Dom的那些事(三)》有2个想法

发表评论

电子邮件地址不会被公开。

Captcha Code