虚拟dom概念最早是facebook提出的, 并运用于react框架. 在dom元素更新的环节上, 使用虚拟dom, 结合diff算法. 能够很大的提高性能. 在vue2.0上, 也一样使用了虚拟dom. 虚拟dom, 能够是一个比较独立的, 能够不依赖于任何的框架. 目前市面上也有一些虚拟dom的类库. 那么, 它最核心的API有哪些呢?javascript
/** * 定义, 虚拟节点的类型. */
export const vNodeType = {
HTML:'HTML',
TEXT: 'TEXT',
COMPONENT: 'COMPONENT'
};
/** * 定义子元素的类型. */
export const vChildType = {
EMPTY: 'EMPTY',
SINGLE: 'SINGLE',
MULTI: 'MULTI'
}
复制代码
import { vNodeType, vChildType } from './strings';
/** * 建立文本元素. * @param {String} text */
const createTextVNode = text => {
return {
// 虚拟dom的类型. TEXT, HTML. COMPONENT等.
nodeType: vNodeType.TEXT,
// 节点标签: div, p等
tag: null,
// 虚拟dom节点的属性: {style: {color: 'red'}, key: 'xxx'}
props: null,
// 虚拟dom渲染后的真实的dom节点.
el: null,
children: text,
childType: vChildType.EMPTY
};
}
/** * 建立虚拟dom. * @param {String} tag 标签名称. div, function, null等 * @param {Object} props 虚拟元素的属性对象. * @param {Array} children 虚拟元素的子元素. */
const createElement = (tag, props, children) => {
let nodeType;
let childType;
// 根据传入的tag, 设置虚拟元素的类型.
switch (typeof tag) {
case 'string': {
nodeType = vNodeType.HTML;
break;
}
case 'function': {
nodeType = vNodeType.COMPONENT;
break;
}
default: {
nodeType = vNodeType.TEXT;
break;
}
}
// 根据传入的children, 设置子元素的标志, 方便后期使用.
if (!children) {
childType = vChildType.EMPTY;
} else if (Array.isArray(children)) {
if (!children.length) {
childType = vChildType.EMPTY;
} else {
childType = children.length > 1 ? vChildType.MULTI : vChildType.SINGLE;
}
} else {
// 文本
childType = vChildType.SINGLE;
children = createTextVNode(children);
}
return {
// 虚拟dom渲染后的真实的dom节点.
el: null,
// 虚拟dom的类型. TEXT, HTML. COMPONENT等.
nodeType,
// 节点标签: div, p等
tag,
// 虚拟dom节点的属性: {style: {color: 'red'}, key: 'xxx'}
props,
// 虚拟dom的子节点
children,
// 虚拟dom子节点的类型: empty, single, multipy。
// 不一样的类型,在挂载和更新时, 会有不一样的处理逻辑.
childType
};
}
export default createElement;
复制代码
区分首次渲染仍是更新操做.html
/** * 渲染或更新虚拟dom * @param {Object} vNode * @param {HTMLElement} container */
const render = (vNode, container) => {
const isFirstRender = !container.vNode;
// 首次渲染
if (isFirstRender) {
mount(vNode, container);
} else {
// 更新操做
patch(container.vNode, vNode, container);
}
// 保存起来, 用来区分是否为首次渲染
container.vNode = vNode;
};
复制代码
render.js的完整代码实现: 重点要关注: patchProps和patchChildren两个方法.vue
import { vNodeType, vChildType } from './strings';
/**
* 更新子节点. 是虚拟dom更新时, 最核心的方法. 涉及到diff比较.
* @param {String} preChildType 上一个子节点的类型
* @param {String} nextChildType 待更新的子节点的类型
* @param {Object} preChildren 上一个子节点的虚拟dom
* @param {Object} nextChildren 待更新子节点的虚拟dom
* @param {HTMLElement} container 挂载的容器.
*/
const patchChildren = (preChildType, nextChildType, preChildren, nextChildren, container) => {
// 更新的场景.
// - 1. 老的节点
// - 老的是一个
// - 老的是空
// - 老的是多个
// 2. 新的节点.
// - 新的是一个
// - 新的是空
// - 新的是多个
// 组合起来, 共有9中状况.
switch (preChildType) {
case vChildType.SINGLE: {
switch (nextChildType) {
case vChildType.SINGLE: {
// 都是单个. 执行更新操做
patch(preChildren, nextChildren, container);
break;
}
case vChildType.EMPTY: {
// 新的是空. 移除老的节点.
container.removeChild(preChildren.el);
break;
}
case vChildType.MULTI: {
// 老的是单个. 新的是多个.
// 先删除老的节点. 而后在逐个挂载每个新的节点.
container.removeChild(preChildren.el);
for (let i = 0; i < nextChildren.length; i++) {
mount(nextChildren[i], container);
}
break;
}
}
break;
}
case vChildType.EMPTY: {
switch (nextChildType) {
case vChildType.SINGLE: {
// 老的是空, 新的是单个. 直接挂载.
mount(nextChildren, container);
break;
}
case vChildType.EMPTY: {
// 两个都是空的状况. 无需任何操做.
break;
}
case vChildType.MULTI: {
// 老的是空,新的是多个.
// 逐个挂载新的每个节点.
for (let i = 0; i < nextChildren.length; i++) {
mount(nextChildren[i], container);
}
break;
}
}
break;
}
case vChildType.MULTI: {
switch (nextChildType) {
case vChildType.SINGLE: {
// 老的是多个, 新的是单个.
// 先逐个删除老的, 而后挂载新的.
for (let i = 0; i < preChildren.length; i++) {
container.removeChild(preChildren[i].el);
}
mount(nextChildren, container);
break;
}
case vChildType.EMPTY: {
// 老的是多个, 新的是空.
// 先逐个删除老的.
for (let i = 0; i < preChildren.length; i++) {
container.removeChild(preChildren[i].el);
}
break;
}
case vChildType.MULTI: {
// 不一样的虚拟dom实现, 就在这里区分, 不一样的类库优化策略不同.
// 老的是数组, 新的也是数组.
// 实现策略. 查看相对位置.
// - 老的是[a,b,c],新的也是[a,b,c]:节点的相对位置是递增的. 元素不须要移动.
// - 老的是[a,b,c], 新的是[x,e,a,h,b,e,c]: 节点a,b,c的相对位置也是递增的.元素不须要移动.
// - 老的是[a,b,c], 新的是[b,a,c]: 那么节点b和a的相对位置, 发生改变,但接到a和c的相对位置仍是递增的.
let lastIndex = 0;
for (let i = 0; i < nextChildren.length; i++) {
let isFind = false;
let nextVNode = nextChildren[i];
let j = 0;
for (j; j < preChildren.length; j++) {
let preVNode = preChildren[j];
// 1. 若是key相同, 咱们认为是同一个元素.
if (preVNode.props.key === nextVNode.props.key) {
isFind = true;
patch(preVNode, nextVNode, container);
// 若是j小于lastIndex, 则相对位置发生变化.
// 认为须要移动.
if (j < lastIndex) {
// insertBefore移动元素.
// abc, a想移动到b以后. abc的父元素.insertBefore()
const flagElement = nextChildren[i - 1].el.nextSibling;
container.insertBefore(preVNode.el, flagElement);
break;
} else {
lastIndex = j;
}
}
}
// 在老的中没有找到. 须要新增.
if (!isFind) {
const flagNode = i == 0 ? preChildren[0].el : nextChildren[i - 1].el.nextSibling;
mount(nextVNode, container, flagNode);
}
}
// 删除老的中存在, 新的中不存在的节点.
for (let i = 0; i < preChildren.length; i++) {
const preVNode = preChildren[i];
const has = !!nextChildren.find(m => m.props.key === preVNode.props.key);
if (!has) {
container.removeChild(preVNode.el);
}
}
break;
}
}
break;
}
default: {
break
}
}
};
/**
* 更新HTML类型的虚拟节点.
*/
const patchHTML = (pre, next, container) => {
// pre是div, next是p
if (pre.tag !== next.tag) {
return replaceVNode(pre, next, container);
}
// 1. 更新节点的props.
const { el, props: preProps } = pre;
const { props, children } = next;
// 更新新的props.
for (const key in props) {
if (props.hasOwnProperty(key)) {
patchProps(el, key, preProps[key], props[key]);
}
}
// 删除老的props中存在, 但新的props中不存在的属性
for (const key in preProps) {
if (preProps.hasOwnProperty(key) && !props.hasOwnProperty(key)) {
// 第四个参数, 表示新的props中值没有.
patchProps(el, key, preProps[key], null);
}
}
// 2. 更新子节点
patchChildren(pre.childType, next.childType, pre.children, next.children, el);
next.el = el;
};
/**
* 更新文本类型的虚拟节点.
*/
const patchText = (pre, next) => {
const { el } = pre;
// 更新文本节点的值
if (next.children !== pre.children) {
el.nodeValue = next.children;
}
// 保存真实节点到虚拟dom中.
next.el = el;
};
/**
* 替换虚拟dom节点
*/
const replaceVNode = (pre, next, container) => {
// 删除原来的
container.removeChild(pre.el);
// 挂载最新的.
mount(next, container);
};
/**
* 更新元素. 是虚拟dom中最核心的方法.
* @param {Object} preVNode 上一次的虚拟dom
* @param {Object} nextVNode 最新的虚拟dom
* @param {HTMLElement} container 要挂载的节点容器.
*/
const patch = (preVNode, nextVNode, container) => {
const {
nodeType: preNodeType
} = preVNode;
const {
nodeType
} = nextVNode;
// 1. prv是文本, next是html(好比div). 直接替换操做. 没有优化的空间.
if (preNodeType !== nodeType) {
replaceVNode(preVNode, nextVNode, container);
} else if (nodeType === vNodeType.HTML) {
patchHTML(preVNode, nextVNode, container);
} else if (nodeType === vNodeType.TEXT) {
patchText(preVNode, nextVNode, container);
}
}
/**
* 更新节点属性.
* @param {HTMLElement} el
* @param {any} key
* @param {Object} pre 上一次的属性对象
* @param {Object} next 待更新的属性对象
*/
const patchProps = (el, key, pre, next) => {
switch (key) {
case 'style': {
// 更新新的props
for (const k in next) {
if (next.hasOwnProperty(k)) {
el.style[k] = next[k];
}
}
// 删除老的props上有, 但在新的props上没有的属性
for (const k in pre) {
if (pre.hasOwnProperty(k) && next && !next.hasOwnProperty(k)) {
el.style[k] = '';
}
}
break;
}
case 'class': {
el.className = next;
break;
}
default: {
// 事件
if (key[0] === '@') {
const eventType = key.slice(1);
if (pre) {
el.removeEventListener(eventType, pre);
}
if (next) {
el.addEventListener(eventType, next);
}
} else {
el.setAttribute(key, next);
}
break;
}
}
};
/**
* 挂载虚拟dom到指定的容器上.
* @param {Object} vNode 虚拟dom对象
* @param {HTMLElement} container 挂载的容器
* @param {HTMLElement} flagNode 元素挂载时调用insertBefore方法时的参考元素. 主要用于元素更新时.
*/
const mountElement = (vNode, container, flagNode) => {
const {
nodeType,
tag,
props,
el,
children,
childType
} = vNode;
// 建立dom节点
const dom = document.createElement(tag);
vNode.el = dom;
// 挂载props
if (props) {
for (const key in props) {
if (props.hasOwnProperty(key)) {
const data = props[key];
// 节点, key, 老值, 新值.
patchProps(dom, key, null, data);
}
}
}
// 挂载子元素.
if (childType !== vChildType.EMPTY) {
if (childType === vChildType.SINGLE) {
mount(children, dom);
} else if (childType === vChildType.MULTI) {
children.forEach(node => {
mount(node, dom);
})
}
}
flagNode ? container.insertBefore(dom, flagNode) : container.appendChild(dom);
};
/**
* 挂载文本类型的虚拟dom
* @param {Object} vNode
* @param {HTMLElement} container
*/
const mountText = (vNode, container) => {
vNode.el = document.createTextNode(vNode.children);
container.appendChild(vNode.el);
};
/**
* 首次渲染.
*/
const mount = (vNode, container, flagNode) => {
const { nodeType } = vNode;
switch (nodeType) {
case vNodeType.HTML: {
mountElement(vNode, container, flagNode);
break;
}
case vNodeType.TEXT: {
mountText(vNode, container);
break;
}
default: break;
}
};
/**
* 渲染或更新虚拟dom
* @param {Object} vNode
* @param {HTMLElement} container
*/
const render = (vNode, container) => {
const isFirstRender = !container.vNode;
// 首次渲染
if (isFirstRender) {
mount(vNode, container);
} else {
// 更新操做
patch(container.vNode, vNode, container);
}
// 保存起来, 用来区分是否为首次渲染
container.vNode = vNode;
};
export default render;
复制代码
codejava