虛擬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